仿VPGAME客户端跟RecyclerView联动指针控件

先看VPGAME客户端的这个效果:

2017-08-28-10mzvp.gif

接着是我实现的效果:

2017-08-28-10mzdemo.gif

转成gif图质量不太好,实际效果比这个好很多,可以去运行demo看看实际效果。链接:https://github.com/DarkSherlock/DateViewWithRvDemo

我们可以看到这个效果,当recyclerview滑动的时候,这个控件里的那个时钟指针
会跟着转动,后面的文字也会跟着item的值 有一个滑进滑出动画。

我本以为这是一个自定义View,然而当我用打开DDMS用HierachyView查看它的布局的时候。

VPGAME布局分析

我们可以看到他这个不是用一个自定义View来完成的,而是多个自定义View
来组合在RelativieLayout里来实现的。那么我们就可以借鉴他的这个思路。

Studio打开HierachyView的步骤:

ddms.png
dump.png

那么接下来就来分析下实现的思路:

1.首先要和RecyclerView完成交互,那么就需要添加OnScrollListener来监听
RV的滑动,根据滑动距离来算出滑动了几个Item,根据Item的某字段(它这里是时间月份)来传给自定义控件,让其完成UI更新。
2.那个滑进滑出的控件,觉得不需要再去自定义,只需要用TextView加位移动画就能实现。
3.自定义指针转动控件,根据OnScrollListener监听到的dy滑动距离,来设置转动的角度。

具体实现:

  //为了和dateview 完成联动,添加滑动监听
  rv.addOnScrollListener(new MyScrollListener());

在onScrollListener()里着重关注onScrolled();


        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            if ( mRvItemHeight != 0 ) {
                y += dy;
                //将累计的滑动距离 跟一个item的高度 比较,判断滑动了相当于几个item的距离。
                float position = y / mRvItemHeight;

                //将每次滑动了相当于多少个Item高度的值传给指针控件,
                //滑动一个item高度指针就转动一圈,按比例转动角度。
                dateview.setProcess(position);

                mBean = mList.get((int) position);//拿到对应的item的javabean

                //只要有轻微的滑动onScrolled就会调用,但是我们不需要这么频繁的去更新滑进滑出的UI
                //所以我们这里判断只有当2个item的月份字段不一样的时候,这时候需要执行滑进滑出的
                //动画,并且将月份更新显示。
                if (mBean.getMonth()!= Integer.parseInt(mTvMonth.getText().toString())) {
                    mCurrentMonth = mBean.getMonth();
                    if (dy > 0) { //判断执行向上还是向下滑动动画
                        startUpAnim( );
                    } else {
                        startDownAnim();
                    }

                }
            }
        }

接着看看动画:
由于位移动画我们需要拿到执行动画的textview的Y轴起始坐标和高度,所以我们post一个runnable(直接在activity的oncreat()中去拿的话因为控件可能还未layout完毕,所以可能取到的值为0);
动画分为:1.向上滑出动画2.向上滑进动画3.向下滑出动画4.向下滑进动画。
textview向上滑出顶部不可见后再从底部向上滑进(1执行完毕后执行2)
textview向下滑出底部不可见后再从顶部向下滑进(3执行完毕后执行4)

        //post 一个runnable 待 view layout 完毕后测量 rcyclerview item的高度 并且初始化动画
        rv.post(new Runnable() {
            @Override
            public void run() {
                View childAt = rv.getLayoutManager().findViewByPosition(0);
                if (childAt != null) {
                    mRvItemHeight = (float) childAt.getHeight();
                    initAnimation();
                }
            }
        });
private void initAnimation() {
    // Y轴方向上的坐标
    float translationY = mTvMonth.getTranslationY();
    float tvMonthHeight = mTvMonth.getHeight();
    //向上弹出动画
    //第一个参数是要执行动画的控件,第二个参数是更改的属性字段(需带有setter方法),
    //第三个参数是 动画开始时 要更改的属性字段的起始值,第四个是结束时的值(translationY - tvMonthHeight 相当于滑出边界不可见了。)
    //这里指mTvMonth执行Y轴上的坐标 更改(Y轴位移动画)
    mUpAnimOut = ObjectAnimator.ofFloat(mTvMonth, "translationY", translationY, translationY - tvMonthHeight);
    //向上弹进动画
    mUpAnimIn =ObjectAnimator.ofFloat(mTvMonth, "translationY", translationY + tvMonthHeight, translationY);
    mUpAnimOut.setDuration(ANIMATION_DURATION);
    mUpAnimIn.setDuration(ANIMATION_DURATION);
    //添加动画执行监听
    addUpAnimListener(mUpAnimIn);

    //向下弹出动画
    mDownAnimOut =ObjectAnimator.ofFloat(mTvMonth, "translationY", translationY, translationY + tvMonthHeight);
    //向下弹进动画
    ObjectAnimator downAnimIn =ObjectAnimator.ofFloat(mTvMonth, "translationY", translationY - tvMonthHeight, translationY);

    mDownAnimOut.setDuration(ANIMATION_DURATION);
    downAnimIn.setDuration(ANIMATION_DURATION);
    //添加动画执行监听
    addDownAnimListener(downAnimIn);
}
private void addUpAnimListener(final ObjectAnimator upAnimIn) {
        mUpAnimOut.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (!upAnimIn.isStarted()) {
                    upAnimIn.start();
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

        upAnimIn.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                mTvMonth.setText(String.valueOf(mCurrentMonth));
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                //当recycler滑动速度非常快的时候,当前的动画还未执行,已经滑动到下条数据要执行下一个动画时,
                //因为我们判断了!upAnimIn.isStarted() ,所以下个动画不会执行,这时候就需要以下判断当RecyclerView
                //滑动停止,当前动画结束时将正确的(下一条的数据)设置给mTvMonth,避免数据错乱.
                if (scrollState == RecyclerView.SCROLL_STATE_IDLE && mCurrentMonth != Integer.parseInt(mTvMonth.getText().toString())) {
                    mTvMonth.setText(String.valueOf(mCurrentMonth));
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
    }

动画设置执行时间为50ms,但是由于recyclerview可能会非常快速地滑动,所以如果动画还在执行就跳过,在 RecyclerView滑动停止时即状态等于SCROLL_STATE_IDLE时将要更新的值保存下来,在动画执行完毕的时候去判断 如果数据显示不正确再重新赋值正确的数据给textview

  /**
     * 开始向上滑出的动画
     */
    private void startUpAnim(  ) {
        if (!mUpAnimOut.isStarted()) {
            mUpAnimOut.start();
        }
    }
 @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            if (mBean != null) {
                scrollState = newState;
                //当非常快速滑动的时候 在滑动的最后判断数据是否准确,将正确的数据返回。
                if (scrollState == RecyclerView.SCROLL_STATE_IDLE && mCurrentMonth != Integer.parseInt(mTvMonth.getText().toString())) {
                    mCurrentMonth = mBean.getMonth();
                }
            }
        }

这样动画的部分就实现完了,接着看转动指针的部分

转动指针自定义View分为2部分:1.不动的圆形背景类似于时钟背景
2.转动的指针,类似于时钟指针。
背景直接canvas.drawCircle就行,没什么可说的。
指针转动的角度就需要根据传onScrollListener传进来的值进行一定的计算来算出需要转动多少角度,直接看代码就懂了。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int height = getHeight();
        int width = getWidth();
        int radius = width / 2;//圆形背景半径
        canvas.translate(width / 2, height / 2);

        canvas.save();
        //画灰色圆形背景
        canvas.drawCircle(0, 0, width / 2, mCirclePaint);

        //画12 3 6 9 四个刻度   长度为半径(width/2)的0.25
        mCursor.setColor(Color.parseColor("#FFAAAAAA"));

        canvas.drawLine(0, -height / 2, 0, ((radius * R_QUARTER) - height / 2), mCursor);//12
        canvas.drawLine(width / 2, 0, (width / 2 - (radius * R_QUARTER)), 0, mCursor);//3
        canvas.drawLine(0, height / 2, 0, (height / 2 - (radius * R_QUARTER)), mCursor);//6
        canvas.drawLine(-width / 2, 0, (-width / 2 + (radius * R_QUARTER)), 0, mCursor);//9

        //画根据传进来的process 转动的指针
        int stopX = (int) (0.6 * (width / 2) * Math.sin(mProcess * 2 * Math.PI));
        int stopY = (int) (0.6 * (width / 2) * Math.cos(mProcess * 2 * Math.PI));
        mCursor.setColor(Color.WHITE);
        canvas.drawLine(0, 0, stopX, -stopY, mCursor);
    }
/**
     * 设置指针转动角度比率
     * @param process
     */
    public void setProcess(float process) {
        this.mProcess = process;
        invalidate();
    }

这样就完成了,挺简单的代码,完整的代码可以去githup上的demo中看。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,510评论 25 707
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,708评论 22 664
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,025评论 4 62
  • 20170828-0903号 本周计划 5理想的状况 a制度改革了, 效益提高了 b学习升级了 幸福指数高 c游戏...
    芮涵琪雪阅读 126评论 0 0
  • 忘了怎样的开始 忘了怎样的结束 没有你的日子 总是下着漫天的雨 走不出从前 走不出回忆 无尽的情思 总是缠绕在过去...
    小金甲阅读 150评论 0 0