在应用中使用动画,能够给用户带来良好的交互体验。经过以前对Android动画的分类总结,尝试了使用属性动画实现支付宝支付效果及购物车添加动画的效果,今天在这里模仿一下微博雷达页面效果。java
对Android动画不太熟悉或遗忘的知识,能够经过下面两篇文章了解。git
Android 动画总结,Android 动画实战github
这次模仿新浪微博雷达页的功能,虽然只有一个Activity,但使用到了不少知识。包括canvas
有兴趣的同窗能够查看Github 源码。框架
老习惯,先看看效果图。ide
至于真实的微博雷达效果是怎样,玩微博的同窗能够对比一下。函数
这里主要从实现的几个功能点作一下分析。post
总的来讲,这个雷达效果图应该是整个微博雷达页面模仿效果类似度最高的一个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();
}
});
}
private void initInnerWhiteAnimator() {
mInnerWhiteAnimator.setFloatValues(0, getMeasuredWidth() / 3);
mInnerWhiteAnimator.setDuration(1000);
mInnerWhiteAnimator.setRepeatCount(-1);
mInnerWhiteAnimator.setInterpolator(new AccelerateInterpolator());
mInnerWhiteAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mInnerWhiteRadius = (Float) animation.getAnimatedValue();
}
});
}
private void initBlackAnimator() {
mBlackAnimator.setFloatValues(0, getMeasuredWidth() / 3);
mBlackAnimator.setDuration(1000);
mBlackAnimator.setRepeatCount(-1);
mBlackAnimator.setInterpolator(new DecelerateInterpolator());
mBlackAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mBlackRadius = (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的属性动画的组合;具体可查看源码。
经过上面的内容,完成了全部动画相关的操做。接下来就是展现内容的实现了。
这里的展现内容是根据当前位置的经纬度坐标,按关键字去搜索周边的兴趣点,而关键字就是底部几个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更新。
到这里,就完成了全部功能。
关于这个微博雷达效果的模仿,从最开始只是模仿雷达扫描效果,最终到总体效果的实现。尝试了不一样的方案;不得不认可模仿效果和实际功能差不少。但也算是一个学习的过程当中,也踩到了一些一些没注意的坑,也算是有点收获吧。
最后,再次给出源码地址Github,欢迎star & fork。