Android仿新浪微博雷達搜索動畫效果
前言
在應用中使用動畫,可以給用戶帶來良好的交互體驗。
雖然只有一個Activity,但使用到了很多知識。包括
-
屬性動畫(雷達效果圖)
-
Android touch 事件傳遞機制
-
Android 6.0 動態權限判斷
-
百度LBS/POI 搜索
-
EventBus
先看看效果圖。
至于真實的微博雷達效果是怎樣,玩微博的同學可以對比一下。
功能分析
這里主要從實現的幾個功能點做一下分析。
雷達效果圖
總的來說,這個雷達效果圖應該是整個微博雷達頁面模仿效果相似度最高的一個View。使用屬性動畫實現這個雷達掃描效果非常簡單。
動畫初始化
private void initRoateAnimator() {
mRotateAnimator.setFloatValues(0, 360);
mRotateAnimator.setDuration(1000);
mRotateAnimator.setRepeatCount(-1);
mRotateAnimator.setInterpolator(new LinearInterpolator());
mRotateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mRotateDegree = (Float) animation.getAnimatedValue();
invalidateView();
}
});
mRotateAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mTipText = "正在探索周邊的...";
//旋轉動畫啟動后啟動掃描波紋動畫
mOutGrayAnimator.start();
mInnerWhiteAnimator.start();
mBlackAnimator.start();
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
//取消掃描波紋動畫
mOutGrayAnimator.cancel();
mInnerWhiteAnimator.cancel();
mBlackAnimator.cancel();
//重置界面要素
mOutGrayRadius = 0;
mInnerWhiteRadius = 0;
mBlackRadius = 0;
mTipText = "未能探索到周邊的...,請稍后再試";
invalidateView();
}
});
}
private void initOutGrayAnimator() {
mOutGrayAnimator.setFloatValues(mBlackRadius, getMeasuredWidth() / 2);
mOutGrayAnimator.setDuration(1000);
mOutGrayAnimator.setRepeatCount(-1);
mOutGrayAnimator.setInterpolator(new LinearInterpolator());
mOutGrayAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mOutGrayRadius = (Float) animation.getAnimatedValue();
}
});
}
這里首先定義了一些動畫效果,并在他們各自的Update 回調方法里實現了 屬性值 的更新。這里只有在mRotateAnimator的Update回調了執行了invalidateView(),避免了過渡繪制,浪費資源;屬性值每次更新后,就會調用onDraw 方法,會通過canvas繪制視圖,這樣不斷刷新,就會呈現出雷達掃描的效果。
canvas 繪制動畫
@Override
protected void onDraw(Canvas canvas) {
//繪制波紋
canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, mBlackRadius, mBlackPaint);
canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, mInnerWhiteRadius, mInnerWhitePaint);
canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, mOutGrayRadius, mOutGrayPaint);
//繪制背景
Bitmap mScanBgBitmap = getScanBackgroundBitmap();
if (mScanBgBitmap != null) {
canvas.drawBitmap(mScanBgBitmap, getMeasuredWidth() / 2 - mScanBgBitmap.getWidth() / 2, getMeasuredHeight() / 2 - mScanBgBitmap.getHeight() / 2, new Paint(Paint
.ANTI_ALIAS_FLAG));
}
//繪制按鈕背景
Bitmap mButtonBgBitmap = getButtonBackgroundBitmap();
canvas.drawBitmap(mButtonBgBitmap, getMeasuredWidth() / 2 - mButtonBgBitmap.getWidth() / 2, getMeasuredHeight() / 2 - mButtonBgBitmap.getHeight() / 2, new Paint(Paint.ANTI_ALIAS_FLAG));
//繪制掃描圖片
Bitmap mScanBitmap = getScanBitmap();
canvas.drawBitmap(mScanBitmap, getMeasuredWidth() / 2 - mScanBitmap.getWidth() / 2, getMeasuredHeight() / 2 - mScanBitmap.getHeight() / 2, new Paint(Paint.ANTI_ALIAS_FLAG));
//繪制文本提示
mTextPaint.getTextBounds(mTipText, 0, mTipText.length(), mTextBound);
canvas.drawText(mTipText, getMeasuredWidth() / 2 - mTextBound.width() / 2, getMeasuredHeight() / 2 + mScanBackgroundBitmap.getHeight() / 2 + mTextBound.height() + 50, mTextPaint);
}
滑動推薦或不喜歡
這里上拉推薦,下拉不感興趣的滑動效果和真實效果有一定差距。實現方案是借鑒下拉刷新和下拉加載框架的內容。只是修改了頭部和底部的隱藏View。同時,也需要實現在滑動時,對頭部和底部tab的隱藏效果。因此在touch事件的ACTION_DOWN 和ACTION_UP 環節,添加了回調單獨處理。
監聽滑動狀態
/**
* 監聽當前是否處于滑動狀態
*/
public interface OnPullListener {
/**
* 手指正在屏幕上滑動
*/
void pull();
/**
* 手指已從屏幕離開,結束滑動
*/
void pullDone();
}
處理滑動
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// onInterceptTouchEvent已經記錄
// mLastMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
if (mPullListener != null) {
mPullListener.pull();
}
int deltaY = y - mLastMotionY;
if (mPullState == PULL_DOWN_STATE) {
// PullToRefreshView執行下拉
Log.i(TAG, " pull down!parent view move!");
headerPrepareToRefresh(deltaY);
// setHeaderPadding(-mHeaderViewHeight);
} else if (mPullState == PULL_UP_STATE) {
// PullToRefreshView執行上拉
Log.i(TAG, "pull up!parent view move!");
footerPrepareToRefresh(deltaY);
}
mLastMotionY = y;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
int topMargin = getHeaderTopMargin();
if (mPullState == PULL_DOWN_STATE) {
if (topMargin >= 0) {
// 開始刷新
headerRefreshing();
} else {
// 還沒有執行刷新,重新隱藏
setHeaderTopMargin(-mHeaderViewHeight);
setHeadViewAlpha(0);
if (mPullListener != null) {
mPullListener.pullDone();
}
}
} else if (mPullState == PULL_UP_STATE) {
if (Math.abs(topMargin) >= mHeaderViewHeight
+ mFooterViewHeight) {
// 開始執行footer 刷新
footerRefreshing();
} else {
// 還沒有執行刷新,重新隱藏
setHeaderTopMargin(-mHeaderViewHeight);
setFootViewAlpha(0);
if (mPullListener != null) {
mPullListener.pullDone();
}
}
}
break;
}
return super.onTouchEvent(event);
}
處理卡片切換
class MyHeadListener implements SmartPullView.OnHeaderRefreshListener {
@Override
public void onHeaderRefresh(SmartPullView view) {
refreshView.onHeaderRefreshComplete();
index = index + 1;
cardAnimActions();
}
}
class MyFooterListener implements SmartPullView.OnFooterRefreshListener {
@Override
public void onFooterRefresh(SmartPullView view) {
refreshView.onFooterRefreshComplete();
index = index + 1;
cardAnimActions();
}
}
這里我們在上下拉刷新的執行回調中,立即完成相應的刷新流程,并執行一張卡片隱藏和下一張卡片顯示的動畫,這樣無論是上拉推薦還是下拉不感興趣,都會去更新一次卡片內容。
卡片顯示隱藏動畫
private void cardAnimActions() {
cardHideAnim.start();
cardHideAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
Log.e(TAG, "onAnimationEnd: the index is " + index);
backFrame.setBackgroundColor(colors[index % 3]);
if (poiInfos != null && poiInfos.size() > 0) {
if (index < poiInfos.size()) {
name.setText(poiInfos.get(index).name);
address.setText(poiInfos.get(index).address);
phoneNum.setText(poiInfos.get(index).phoneNum);
}
}
cardShowAnim.start();
}
});
}
這里cardHideAnim和cardShowAnim分別是兩個屬性 動畫的組合,二者內容剛好相反,使用了卡片Scale和alpha的屬性動畫的組合;具體可查看源碼。
LBS定位和POI 搜索
通過上面的內容,完成了所有動畫相關的操作。接下來就是展示內容的實現了。
這里的展示內容是根據當前位置的經緯度坐標,按關鍵字去搜索周邊的興趣點,而關鍵字就是底部幾個tab所標示的內容。點擊底部tab即可以實現關鍵字的更新,重新發起搜索請求,實現UI更新。
這個過程分為兩步,首先是進行定位(這里當然首先要確保獲取到定位權限),獲取到當前位置;然后根據當前位置和關鍵字進行POI搜索,將搜索結果呈現出來即可。
關于如何使用百度地圖SDK配置AndroidManifest文件,申請key等相關操作,這里不再贅述,具體細節可參考官網
定位實現
mLocationClient = new LocationClient(getApplicationContext()); //聲明LocationClient類
mLocationClient.registerLocationListener(this); //注冊監聽函數
LocationClientOption option = new LocationClientOption();
option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy
);//可選,默認高精度,設置定位模式,高精度,低功耗,僅設備
option.setCoorType("bd09ll");//可選,默認gcj02,設置返回的定位結果坐標系
int span = 1000;
option.setScanSpan(span);//可選,默認0,即僅定位一次,設置發起定位請求的間隔需要大于等于1000ms才是有效的
..... (跟多配置信息可參考官網)
mLocationClient.setLocOption(option);
配置完成后,就可以開始定位操作了,當然不能忘了申請權限
if (ContextCompat.checkSelfPermission(mContext,
Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
//沒有定位權限則請求
ActivityCompat.requestPermissions(this, permissons, MY_PERMISSIONS_REQUEST_LOCATION);
} else {
mLocationClient.start();
}
這樣,就會開始調用手機的定位功能開始定位,定位成功后,會執行onReceiveLocation回調方法,在這個方法里可以獲取到定位后的詳細信息。
@Override
public void onReceiveLocation(BDLocation bdLocation) {
if (mLocationClient != null && mLocationClient.isStarted()) {
mLocationClient.stop();
}
district.setText(bdLocation.getAddress().district);
latLng = new LatLng(bdLocation.getLatitude(), bdLocation.getLongitude());
movie.performClick();
}
這個方法回調成功后,因及時關閉定位操作;這里我們只是簡單的獲取了當前的區域位置,并設置在了頂部,同時獲得了當前的經緯度信息。之后通過movie.performClick便開始了POI搜索的內容。
POI搜索實現
和定位功能類似,POI搜索功能開始之前,也需要進行相應的配置
mPoiSearch = PoiSearch.newInstance();
mPoiSearch.setOnGetPoiSearchResultListener(new MyPoiSearchListener());
mNearbySearchOption = new PoiNearbySearchOption()
.radius(5000)
.pageNum(1)
.pageCapacity(20)
.sortType(PoiSortType.distance_from_near_to_far);
接著我們就會按照剛才的movie.performClick 方法,開始執行POI 搜索功能。
if (latLng != null && mNearbySearchOption != null && keyWord != null) {
mNearbySearchOption.location(latLng).keyword(keyWord);
mPoiSearch.searchNearby(mNearbySearchOption);
}
這里將剛才獲取到的Latlng 位置信息和keyword關鍵字信息注入到NearbySearchOption(POI 搜索中,附近位置搜索的配置對象)中,并使用這個NearbySearchOption開始POI搜索。同樣,在POI搜索完成后執行一個回調方法,在回調方法里我們可以獲取到POI的搜索結果。
@Override
public void onGetPoiResult(PoiResult poiResult) {
Log.e("onGetPoiResult", "the poiResult " + poiResult.describeContents());
EventBus.getDefault().post(poiResult);
}
顧名思義,返回的參數poiResult 就是POI搜索結果。這里為了減少Activity中代碼量,使用EventBus將搜索 發送 到了Activity中相應的Subscribe方法中。
@Subscribe
public void onPoiResultEvent(PoiResult poiResult) {
if (poiResult != null && poiResult.getAllPoi() != null && poiResult.getAllPoi().size() > 0) {
poiInfos = poiResult.getAllPoi();
name.setText(poiInfos.get(0).name);
address.setText(poiInfos.get(0).address);
phoneNum.setText(poiInfos.get(0).phoneNum);
index = 1;
if (refreshView.getVisibility() == View.GONE) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
radar.stopAnim();
radar.setVisibility(View.GONE);
refreshView.setVisibility(View.VISIBLE);
cardShowAnim.start();
}
}, 3000);
}
} else {
radar.stopAnim();
}
}
這里,根據搜索結果再次實現最終的UI更新。
到這里,就完成了所有功能。
總結
關于這個微博雷達效果的模仿,從最開始只是模仿雷達掃描效果,最終到整體效果的實現。嘗試了不同的方案;不得不承認模仿效果和實際功能差很多。但也算是一個學習的過程中,也踩到了一些一些沒注意的坑,也算是有點收獲吧。
來自:http://mp.weixin.qq.com/s?__biz=MzI2OTQxMTM4OQ==&mid=2247484456&idx=1&sn=6bc6a4fa9efb796d90c25501ef4be356&chksm=eae1f17add96786cd241e212ed1fc3322fa4060d236dd0864be01a6dff9cf5cfb837aea66ab6#rd