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

来源:互联网 发布:java 如何encode 编辑:程序博客网 时间:2024/06/03 22:41

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

阅读全文
0 0
原创粉丝点击