抱歉,你查看的文章不存在

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

前言:在应用中使用动画,可以给用户带来良好的交互体验。本文来自【IAM四十二】授权本公众号独家分享。IAM四十二的简书地址:http://www.jianshu.com/u/c6f7cfa366d9,本文案例已开源到github上,值得一提的时,不光只有本案例,还有很多其他Android动画案例,点击【阅读原文】,可下载,如果觉得好,不妨star下他。以下是正文

虽然只有一个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的搜索结果。

@Overridepublic 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更新。

到这里,就完成了所有功能。

总结

关于这个微博雷达效果的模仿,从最开始只是模仿雷达扫描效果,最终到整体效果的实现。尝试了不同的方案;不得不承认模仿效果和实际功能差很多。但也算是一个学习的过程中,也踩到了一些一些没注意的坑,也算是有点收获吧。

原文发布于微信公众号 - 何俊林(DriodDeveloper)

原文发表时间:2017-01-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

编辑于

码农突围

0 篇文章93 人订阅

相关文章

来自专栏非著名程序员

Android Material Design系列之FloatingActionButton和Snackbar

今天主讲的Material Design系列的两个控件都不难,所以一起讲了,分别是FloatingActionButton和Snackbar。这个系列都是主讲的...

31060
来自专栏Android知识点总结

6-VI--ListView琐碎小知识点汇总

10640
来自专栏Android源码框架分析

仿淘宝、京东拖拽商品详情(可嵌套ViewPager、ListView、WebView、FragmentTabhost)实现效果图实现

23130
来自专栏Android干货

Android项目实战(四十一):游戏和视频类型应用 状态栏沉浸式效果

36260
来自专栏Android知识点总结

Android控件之ImageView

张风捷特烈个人网站,编程笔记请访问:http://www.toly1994.com

9800
来自专栏Sorrower的专栏

动画必须有(二):悬浮菜单了解一下!

29630
来自专栏Android中高级开发

Android开发之漫漫长途 番外篇——自定义View的各种姿势2

该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索...

11930
来自专栏QQ音乐技术团队的专栏

小窗播放视频的原理和实现(下)

本文对小窗视频播放进行了详细的研究,针对几种实现方案进行了深入的对比分析,进而给出实现小窗视频播放的最优解。其中通过对系统源码的分析,详细探究了如何完美地实现移...

995100
来自专栏向治洪

仿大众点评悬浮购买框效果

我之前写了一篇关于美团网,大众点评的购买框效果的文章Android对ScrollView滚动监听,实现美团、大众点评的购买悬浮效果,我自己感觉效果并不是很好,如...

341100
来自专栏葬爱家族

Android高级动画(1)

动画在移动App开发中的重要性不言而喻,通俗点讲,动画可以让我们的App界面不那么死板,可以带来酷炫的交互效果,用Material Design专业点的说法,动...

28010

扫码关注云+社区

领取腾讯云代金券