仿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中看。

猜你喜欢

转载自blog.csdn.net/tinderliang/article/details/77647993