Android仿新浪微博雷达搜索动画效果

BasilHoppe 8年前
   <h2>前言</h2>    <p>在应用中使用动画,可以给用户带来良好的交互体验。 </p>    <p>虽然只有一个Activity,但使用到了很多知识。包括</p>    <ul>     <li> <p>属性动画(雷达效果图)</p> </li>     <li> <p>Android touch 事件传递机制</p> </li>     <li> <p>Android 6.0 动态权限判断</p> </li>     <li> <p>百度LBS/POI 搜索</p> </li>     <li> <p>EventBus</p> </li>    </ul>    <p>先看看效果图。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/0c206671fb625e0d7298ce28517f5bdd.gif"></p>    <p>至于真实的微博雷达效果是怎样,玩微博的同学可以对比一下。</p>    <h2>功能分析</h2>    <p>这里主要从实现的几个功能点做一下分析。</p>    <p>雷达效果图</p>    <p>总的来说,这个雷达效果图应该是整个微博雷达页面模仿效果相似度最高的一个View。使用属性动画实现这个雷达扫描效果非常简单。</p>    <p>动画初始化</p>    <pre>  <code class="language-java">   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();             }         });     }  </code></pre>    <p>这里首先定义了一些动画效果,并在他们各自的Update 回调方法里实现了 属性值 的更新。这里只有在mRotateAnimator的Update回调了执行了invalidateView(),避免了过渡绘制,浪费资源;属性值每次更新后,就会调用onDraw 方法,会通过canvas绘制视图,这样不断刷新,就会呈现出雷达扫描的效果。</p>    <p>canvas 绘制动画</p>    <pre>  <code class="language-java">   @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);     }  </code></pre>    <p>滑动推荐或不喜欢</p>    <p>这里上拉推荐,下拉不感兴趣的滑动效果和真实效果有一定差距。实现方案是借鉴下拉刷新和下拉加载框架的内容。只是修改了头部和底部的隐藏View。同时,也需要实现在滑动时,对头部和底部tab的隐藏效果。因此在touch事件的ACTION_DOWN 和ACTION_UP 环节,添加了回调单独处理。</p>    <p>监听滑动状态</p>    <pre>  <code class="language-java">   /**      * 监听当前是否处于滑动状态      */     public interface OnPullListener {         /**          * 手指正在屏幕上滑动          */         void pull();         /**          * 手指已从屏幕离开,结束滑动           */         void pullDone();     }  </code></pre>    <p>处理滑动</p>    <pre>  <code class="language-java">   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);     }  </code></pre>    <p>处理卡片切换</p>    <pre>  <code class="language-java">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();         }     }  </code></pre>    <p>这里我们在上下拉刷新的执行回调中,立即完成相应的刷新流程,并执行一张卡片隐藏和下一张卡片显示的动画,这样无论是上拉推荐还是下拉不感兴趣,都会去更新一次卡片内容。</p>    <p>卡片显示隐藏动画</p>    <pre>  <code class="language-java">   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();             }         });     }  </code></pre>    <p>这里cardHideAnim和cardShowAnim分别是两个属性 动画的组合,二者内容刚好相反,使用了卡片Scale和alpha的属性动画的组合;具体可查看源码。</p>    <p>LBS定位和POI 搜索</p>    <p>通过上面的内容,完成了所有动画相关的操作。接下来就是展示内容的实现了。</p>    <p>这里的展示内容是根据当前位置的经纬度坐标,按关键字去搜索周边的兴趣点,而关键字就是底部几个tab所标示的内容。点击底部tab即可以实现关键字的更新,重新发起搜索请求,实现UI更新。</p>    <p>这个过程分为两步,首先是进行定位(这里当然首先要确保获取到定位权限),获取到当前位置;然后根据当前位置和关键字进行POI搜索,将搜索结果呈现出来即可。</p>    <p>关于如何使用百度地图SDK配置AndroidManifest文件,申请key等相关操作,这里不再赘述,具体细节可参考官网</p>    <p>定位实现</p>    <pre>  <code class="language-java">   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);  </code></pre>    <p>配置完成后,就可以开始定位操作了,当然不能忘了申请权限</p>    <pre>  <code class="language-java">   if (ContextCompat.checkSelfPermission(mContext,                 Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {             //没有定位权限则请求             ActivityCompat.requestPermissions(this, permissons, MY_PERMISSIONS_REQUEST_LOCATION);     } else {          mLocationClient.start();     }  </code></pre>    <p>这样,就会开始调用手机的定位功能开始定位,定位成功后,会执行onReceiveLocation回调方法,在这个方法里可以获取到定位后的详细信息。</p>    <pre>  <code class="language-java">   @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();     }  </code></pre>    <p>这个方法回调成功后,因及时关闭定位操作;这里我们只是简单的获取了当前的区域位置,并设置在了顶部,同时获得了当前的经纬度信息。之后通过movie.performClick便开始了POI搜索的内容。</p>    <p>POI搜索实现</p>    <p>和定位功能类似,POI搜索功能开始之前,也需要进行相应的配置</p>    <pre>  <code class="language-java">mPoiSearch = PoiSearch.newInstance();  mPoiSearch.setOnGetPoiSearchResultListener(new MyPoiSearchListener());  mNearbySearchOption = new PoiNearbySearchOption()                 .radius(5000)                 .pageNum(1)                 .pageCapacity(20)                 .sortType(PoiSortType.distance_from_near_to_far);  </code></pre>    <p>接着我们就会按照刚才的movie.performClick 方法,开始执行POI 搜索功能。</p>    <pre>  <code class="language-java">if (latLng != null && mNearbySearchOption != null && keyWord != null) {     mNearbySearchOption.location(latLng).keyword(keyWord);     mPoiSearch.searchNearby(mNearbySearchOption);  }  </code></pre>    <p>这里将刚才获取到的Latlng 位置信息和keyword关键字信息注入到NearbySearchOption(POI 搜索中,附近位置搜索的配置对象)中,并使用这个NearbySearchOption开始POI搜索。同样,在POI搜索完成后执行一个回调方法,在回调方法里我们可以获取到POI的搜索结果。</p>    <pre>  <code class="language-java">@Override  public void onGetPoiResult(PoiResult poiResult) {      Log.e("onGetPoiResult", "the poiResult " + poiResult.describeContents());      EventBus.getDefault().post(poiResult);  }  </code></pre>    <p>顾名思义,返回的参数poiResult 就是POI搜索结果。这里为了减少Activity中代码量,使用EventBus将搜索 发送 到了Activity中相应的Subscribe方法中。</p>    <pre>  <code class="language-java">   @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();         }     }  </code></pre>    <p>这里,根据搜索结果再次实现最终的UI更新。</p>    <p>到这里,就完成了所有功能。</p>    <h2>总结</h2>    <p>关于这个微博雷达效果的模仿,从最开始只是模仿雷达扫描效果,最终到整体效果的实现。尝试了不同的方案;不得不承认模仿效果和实际功能差很多。但也算是一个学习的过程中,也踩到了一些一些没注意的坑,也算是有点收获吧。</p>    <p> </p>    <p style="text-align:center"> </p>    <p style="text-align:center"> </p>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s?__biz=MzI2OTQxMTM4OQ==&mid=2247484456&idx=1&sn=6bc6a4fa9efb796d90c25501ef4be356&chksm=eae1f17add96786cd241e212ed1fc3322fa4060d236dd0864be01a6dff9cf5cfb837aea66ab6#rd</p>    <p> </p>