博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android 动画实战-仿微博雷达功能
阅读量:6799 次
发布时间:2019-06-26

本文共 13333 字,大约阅读时间需要 44 分钟。

前言

在应用中使用动画,可以给用户带来良好的交互体验。通过之前对Android动画的分类总结,尝试了使用属性动画实现支付宝支付效果及购物车添加动画的效果,今天在这里模仿一下微博雷达页面效果

对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();            }        });    }    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的属性动画的组合;具体可查看源码。

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

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

总结

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


最后,再次给出源码地址,欢迎star & fork。

转载地址:http://mdywl.baihongyu.com/

你可能感兴趣的文章
python下的MySQLdb使用
查看>>
CCNP路由实验---4、配置EIGRP不等价均衡
查看>>
Fedora20下安装vim
查看>>
CentOS 6.5 使用docker 容器
查看>>
pl/sql中的exception
查看>>
Android开发:通过AdbWireless,不用数据线连接到安卓手机进行调试
查看>>
组策略对应于注册表位置汇总
查看>>
Java虚拟机参数配置
查看>>
RHCE 学习笔记(31) - 防火墙 (中)
查看>>
XSS研究4-来自外部的XSS攻击的防范
查看>>
Spring知识点总结-1
查看>>
微软私有云分享(R2)21 BMC提升B格
查看>>
MDSF:如何使用GMF来做TOGAF建模工具
查看>>
Spring Security简介
查看>>
打造一流的研发中心
查看>>
MCollective架构篇3-Puppet插件的部署及测试
查看>>
配置GNS使用CRT连接
查看>>
Java:集合类性能分析
查看>>
创建Server 2012 VHDX虚拟磁盘模板
查看>>
IE调试网页之五:使用 F12 开发人员工具调试 JavaScript 错误 (Windows)
查看>>