RecyclerView之itemView滚动杂谈

很久之前写过了几篇android滚动原理的文章,在读本篇博文之前,可以先阅读下博主以下的几篇博文:
View的滚动原理简单解析
View的滚动原理简单解析(二)
ViewDragHelper的简单分析(一)
ViewDragHelper的简单分析及应用(二)
因为本篇博文会用到上面几篇博客的一些知识点。
本篇是对RecycleView源码解读的开篇,如题所述,准备对其滚动原理来简单分析下,当然也会涉及到RecycleView的其他知识点。

最近看周围的一些朋友在玩抖音,其首页加载的模式不像RecycleView那样随手指的滑动在抬起手指的一刹那,还会有fling动作连续滚动好几页;而抖音的滑动是一页一页的滑动,当滑动到最新页面的时候视频自动播放。

其实这个功能用RecycleView也能实现,但是要对滑动原理要有一定的了解,废话说了这么多,开始发车。

通过文章开头的四篇博客我们知道实现View滚动的方式有四种:
1)调用View的layout方法,设置View的布局位置

2)修改View的layoutParam参数(结合属性动画,效果刚刚滴)

3)ParentView调用scrollTo/scrollBy方法改动childView的位置

4)调用View的offsetLeftAndRight和offsetTopAndBottom方法来实现竖屏或者竖直方向的滚动(详细说明见《ViewDragHelper的简单分析(一)》)。

那么在RecycleView里面实现item滚动的方式什么呢?通过阅读源码发现用的就是offsetLeftAndRight/offsetTopAndBottom!

滚动的过程说起来也很简单:就是手指的三种动作而作出相应的相应而已:在RecyclerView的onTouchEvent里面

ACTION_DOWN: 初始化手指按下的位置。

ACTION_MOVE:随着手指的移动调用scrollByInternal方法实现滚动,并且调用相关的监听通知RecyclerView的ScorllListener
ACTION_UP: 手指抬起时,随着滚动的惯性调用fling方法处理惯性运动。在这里需要注意的是RevyclerView提供了mOnFlingListener,如果客户端配置了这个监听,并且该监听的fling方法返回true,那么处理惯性运动就交给客户端自己处理,否则就交给RecyclerView自己处理:

  //客户端(程序员)自己处理惯性运动
  if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
          return true;
     }

   if (canScroll) {
      velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
      velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
      //RecyclerView自己处理惯性运动
      mViewFlinger.fling(velocityX, velocityY);
     return true;
  }

所以为了使得RecyclerView在抬起手指的时候不连续滚动,就调用RecyclerView的setOnFlingListener方法设置mOnFlingListener并将mOnFlingListener的onFling返回true即可。到这里距离抖音首页的效果在思路上迈出了一小步。

RecyclerView的onTouchEvent通过上面的说明,关于滚动的大致逻辑如下:

public boolean onTouchEvent(MotionEvent e) {

    //省略部与本文无关的分代码

    //根据布局的方向来决定滚动的方向
    final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
    final boolean canScrollVertically = mLayout.canScrollVertically();

    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
     //省略部与本文无关的分代码

    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            mScrollPointerId = e.getPointerId(0);

            mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
            mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
          //省略部分代码
        } break;

       //省略部与本文无关的分代码

        case MotionEvent.ACTION_MOVE: {
            //省略部与本文无关的分代码

            final int x = (int) (e.getX(index) + 0.5f);
            final int y = (int) (e.getY(index) + 0.5f);
            //计算滚动的距离    
            int dx = mLastTouchX - x;
            int dy = mLastTouchY - y;

            //省略部与本文无关的分代码            
            if (mScrollState == SCROLL_STATE_DRAGGING) {
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];

                //真正处理滚动的逻辑
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        vtev)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                if (mGapWorker != null && (dx != 0 || dy != 0)) {
                    mGapWorker.postFromTraversal(this, dx, dy);
                }
            }
        } break;

       // 省略部与本文无关的分代码

        case MotionEvent.ACTION_UP: {
            //省略部与本文无关的分代码
            //处理惯性滚动
            if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                setScrollState(SCROLL_STATE_IDLE);
            }
           //省略部与本文无关的分代码
        } break;

        //省略部与本文无关的分代码
    }

   //省略部与本文无关的分代码

    //默认返回true说明默认由RecycleView来处理事件
    return true;
}

处理滚动的时候当然要知道滚动的方向,我们知道RecyclerView是可以水平和竖直布局的,且设置水平或者竖直布局的时候要调用如下方法(以竖直方向为例):

setLayoutManager(LinearLayoutManager(contentView.context, LinearLayoutManager.VERTICAL, false))

以线性布局LinearLayoutManager为例:

 public boolean canScrollHorizontally() {
        return mOrientation == HORIZONTAL;
    }


    @Override
    public boolean canScrollVertically() {
        return mOrientation == VERTICAL;
    }

如果配置了mOrientation为VERTICAL的则只能竖直滚动。

然后就是在ACTION_MOVE里计算手指移动的距离,然后调用scrollByInternal方法让item随着手指的移动而滚动。所以核心方法是scrollByInternal,让我们看看这个方法都做了些什么:

boolean scrollByInternal(int x, int y, MotionEvent ev) {
    int unconsumedX = 0, unconsumedY = 0;
    int consumedX = 0, consumedY = 0;

    //省略与本文无关的代码
     if (mAdapter != null) {
          //省略与本文无关的代码
           //实现水平滚动
           if (x != 0) {
                consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                unconsumedX = x - consumedX;
            }
           //实现竖直滚动
            if (y != 0) {
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                unconsumedY = y - consumedY;
            }
            //省略与本文无关的代码
        }

    if (!mItemDecorations.isEmpty()) {
        invalidate();
    }

    //省略与本文无关的代码


    // 滚动回调
    if (consumedX != 0 || consumedY != 0) {
       //调用dispatchOnScrolled
        dispatchOnScrolled(consumedX, consumedY);
    }

   //省略与本文无关的代码
    return consumedX != 0 || consumedY != 0;
}

上面的代码从本文的角度来说主要作了两件事:
1、调用mLayout对象的scrollHorizontallyBy/scrollVerticallyBy实现了RecyclerView的滚动(注:mLayout为LayoutManager对象,此处以竖直滚动为例)
2、调用dispatchOnScrolled方法来通知客户端的滚动监听

简单分析下scrollVerticallyBy方法

本篇博文以竖直滚动为例,来分析分析scrollVerticallyBy方法,而地球人都知道竖直滚动的mLayout引用对象就是LinearLayoutManager。所以直接进入LinearLayoutManager的LinearLayoutManager方法观察下:


 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
            //如果RecyclerView是水平布局,则竖直不滚动
        if (mOrientation == HORIZONTAL) {
            return 0;
        }
        return scrollBy(dy, recycler, state);
    }

如果是水平布局的话,则不滚动;否则调用scrollBy方法实现(竖直)滚动:

 int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || dy == 0) {
            return 0;
        }
        //省略部分无关代码
        final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
        //调用mOrientationHelper的offsetChildren实现滚动
        mOrientationHelper.offsetChildren(-scrolled);
        }
        mLayoutState.mLastScrollDelta = scrolled;
        return scrolled;
    }

可以看出scrollBy方法对滚动距离稍作处理后,就调用了mOrientationHelper的offsetChildren来实现滚动的。在进入这个方法之前,现总结下上面的所讲的东西:
1、RecyclerView通过事件处理机制,在处理ACTION_MOVE的时候开始item的滚动
2、通过设置LayoutManager来决定滚动的方向
3、在处理ACTION_UP的时候通过fling方法处理惯性滚动

下面再来分析mOrientationHelper的offsetChildren方法,mOrientationHelper是通过OrientationHelper的createOrientationHelper方法来完成初始化的:

  public static OrientationHelper createOrientationHelper(....) {
        switch (orientation) {
            case HORIZONTAL:
                return createHorizontalHelper(layoutManager);
            case VERTICAL:
                return createVerticalHelper(layoutManager);
        }

    }

因为分析的是竖直滚动,所以看看createVerticalHelper这个方法返回的OrientationHelper对象,其offsetChild都做了啥:

public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {

    public void offsetChildren(int amount) {
          mLayoutManager.offsetChildrenVertical(amount);
     }
     //省略与本文无关的代码

   };
 }

所以仅需进入LayoutManager 的offsetChildrenVertical方法:

  public void offsetChildrenVertical(int dy) {
        if (mRecyclerView != null) {
           mRecyclerView.offsetChildrenVertical(dy);
        }
  }

绕来绕去,最终LayoutManger还是要调用RecyclerView的offsetChildrenVertical方法:

   public void offsetChildrenVertical(int dy) {
        //1、获取RecyclerView的itemview的个数
        final int childCount = mChildHelper.getChildCount();
        //2,循环itemview,然后调用view的offsetTopAndBottom方法进行滚动
        for (int i = 0; i < childCount; i++) {
            mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
        }
    }

最终发现还是RecyclerView是通过循环遍历itemView的offsetTopAndBottom来实现竖直滚动的。(关于offsetTopAndBottom的详细说明,详细说明见《ViewDragHelper的简单分析(一)》,本篇不在赘述)

另外,在随着手指滚动的时候可以设置对滚动的监听,在scrollByInternal()方法中,处理好滚动之后有这么一段:

// 滚动回调
    if (consumedX != 0 || consumedY != 0) {
       //调用dispatchOnScrolled
        dispatchOnScrolled(consumedX, consumedY);
    }

dispatchOnScrolled()方法就是调用滚动监听的:

 void dispatchOnScrolled(int hresult, int vresult) {

        final int scrollX = getScrollX();
        final int scrollY = getScrollY();
        /*调用超类View的onScrollChanged方法*
        onScrollChanged(scrollX, scrollY, scrollX, scrollY);

        //RecyclerView的空方法,可以有其子类实现
        onScrolled(hresult, vresult);

        //通知相关监听对象,告知滚动的水平/竖直距离
        if (mScrollListener != null) {
            mScrollListener.onScrolled(this, hresult, vresult);
        }
        if (mScrollListeners != null) {
            for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
                mScrollListeners.get(i).onScrolled(this, hresult, vresult);
            }
        }

    }

需要注意的是mScrollListener接口不仅仅可以通知客户端RecyclerView的滚动距离,该接口还提供了另外一个方法onScrollStateChanged,来让客户端监听滚动的状态:
SCROLL_STATE_IDLE:RecyclerView不再滚动或者停止滚动
SCROLL_STATE_DRAGGING:RecyclerView随着手指拖动而滚动的状态
SCROLL_STATE_SETTLING:RecyclerView随着手指的离开而发生惯性滚动状态。

当然,不再滚动的状态是在fling方法之后,也就是惯性运动结束之后才设置的:

//fling方法处理手指抬起后的惯性运动
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {     
      //惯性运动结束后,设置滚动状态为停止滚动
       setScrollState(SCROLL_STATE_IDLE);
 }

在setScrollState方法里,当滚动状态发生改变的时候,会调用dispatchOnScrollStateChange方法:

 void setScrollState(int state) {
        /*状态没有发生改变*/
        if (state == mScrollState) {
            return;
        }

        mScrollState = state;
        if (state != SCROLL_STATE_SETTLING) {
            stopScrollersInternal();
        }
        /*调用相关监听方法,通知滚动状态发生改变*/
        dispatchOnScrollStateChanged(state);
    }

/*调用相关监听方法,通知滚动状态发生改变*/
void dispatchOnScrollStateChanged(int state) {
        //1、可以用LayoutManager的onScrollStateChanged方法监听
        if (mLayout != null) {
            mLayout.onScrollStateChanged(state);
        }

        //2、可以重写RecyclerView的onScrollStateChanged方法监听滚动状态
        onScrollStateChanged(state);

        //3、设置监听接口OnScrollListener来监听
        if (mScrollListener != null) {
            mScrollListener.onScrollStateChanged(this, state);
        }
        if (mScrollListeners != null) {
            for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
                mScrollListeners.get(i).onScrollStateChanged(this, state);
            }
        }
    }

观察dispatchOnScrollStateChanged发现,如果我们想知道滚动状态的改变,可以有三个方法:
1、自定义LayoutManager,重写其onScrollStateChanged方法。
2、自定义RecyclerView,重写其onScrollStateChanged方法。
3、设置RecyclerView的滚动监听OnScrollListener

所以如果在使用RecyclerView的时候打算在RecyclerView滚动停止的时候做写什么,方法之一可以在ScrollListener的onScrollStateChanged这么判断:

 public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
      //newState==SCROLL_STATE_IDLE表示滚动停止
       if (newState == SCROLL_STATE_IDLE) {

       }

  }

到此位置只讲了滚动的两个状态:停止状态和拖动状态,还有一个惯性运动状态没有说明,本篇博文的后半部分就是对此进行简单的解析:
上面说到在onTouchEvent处理ACTION_UP的时候,核心代码如下:

 case MotionEvent.ACTION_UP: {
                //省略部分代码
            if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                 //滚动结束
                 setScrollState(SCROLL_STATE_IDLE);
              }

            } break;

ACTION_UP事件主要作了两件事:
1、调用fling方法处理惯性运动
2、惯性运动结束后设置状态为SCROLL_STATE_IDLE,通知相关监听
按照惯例进入fling方法:

 public boolean fling(int velocityX, int velocityY) {
        //省略部分代码
        if (!dispatchNestedPreFling(velocityX, velocityY)) {
            //省略部分代码
             //返回true的情况下,由客户端按照需求自己处理惯性运动
             //比如实现类似抖音的一页页滚动
            if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
                return true;
            }

            //RecyclerView自己处理惯性运动
            if (canScroll) {
                //计算水平和竖直方向惯性运动的初始速度
                velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
                velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
                //开始惯性滚动
                mViewFlinger.fling(velocityX, velocityY);
                return true;
            }
        }
        return false;
    }

fling方法主要是先判断有没有惯性速度,有的话则处理的方式有两种:
1、客户端自己处理惯性运动,方式是通过客户端设置OnFlingListener并且其fling的方法返回true,就是说RecyclerView优先处理客户端自己的惯性运动,然后在决定是否有本身继续处理惯性运动
2、由RecyclerView自己处理惯性运动。调用mViewFlinger的fling方法。

mViewFlinger是RecyclerView的一个内部类ViewFlinger,是一个Runnable,其在RecyclerView初始化的时候就得到初始化,该对象内部持有一个ScrollerCompat的引用mScroller:

 private int mLastFlingX;
 private int mLastFlingY;
 private ScrollerCompat mScroller;
 public ViewFlinger() {
            mScroller = ScrollerCompat.create(getContext(), sQuinticInterpolator);
        }

继续进入ViewFlinger的fling方法,分析研究源码就这样,总是在各种方法里面来回杀进杀出:

private ScrollerCompat mScroller;
public void fling(int velocityX, int velocityY) {
   //改变滚动状态为惯性运动状态         
   setScrollState(SCROLL_STATE_SETTLING);
   mLastFlingX = mLastFlingY = 0;
   //开始惯性滚动
   mScroller.fling(0, 0, velocityX, velocityY,
           Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
   //该方法会回调ViewFlinger的run方法
   postOnAnimation();
}

fling方法主要调用了mScroller的fling方法,但是只调用一次肯定不会有平滑惯性运动的效果,所以继续调用了postOnAnimation方法,该方法会继续几经辗转会继续调用Flinger的run方法:

public void run() {
     //省略部分代码
    final ScrollerCompat scroller = mScroller;
    final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
    //如果滚动没结束
    if (scroller.computeScrollOffset()) {
        final int x = scroller.getCurrX();
        final int y = scroller.getCurrY();
        //计算滚动的距离
        final int dx = x - mLastFlingX;
        final int dy = y - mLastFlingY;
        int hresult = 0;
        int vresult = 0;
        mLastFlingX = x;
        mLastFlingY = y;
        int overscrollX = 0, overscrollY = 0;
        if (mAdapter != null) {
            //省略部分代码
            if (dx != 0) {
                //水平滚动
               hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
                overscrollX = dx - hresult;
            }
            if (dy != 0) {
                //竖直滚动
                vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
                overscrollY = dy - vresult;
            }
           //省略部分代码
        }
        //省略部分代码
        //回调客户端滚动监听
        if (hresult != 0 || vresult != 0) {
            dispatchOnScrolled(hresult, vresult);
        }

       //省略部分代码
        if (scroller.isFinished() || !fullyConsumedAny) {
           //滚动结束
            setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
            //省略部分代码
        } else {
            //继续回调run方法
            postOnAnimation();

        }
    }
     //省略部分代码
}

其实上面的代码总体思路也很简单,就是通过判断computeScrollOffset的方法是否返回true来判断滚动是否结束,如果没结束继续调用postOnAnimation方法继续回调run方法。在此不再赘述,具体的思路可参考博主的《View的滚动原理简单解析(二)》,思路是一摸一样的。

到此为止,RecyclerView的滚动原理已经写完。总的思路无非就是博主博文开头列的几篇讲解滚动博文的应用。了解后简单的分析下RecyclerView的滚动原理相关代码其实也没啥难度。

如果错误之处,欢迎批评指正,共同学习

猜你喜欢

转载自blog.csdn.net/chunqiuwei/article/details/79983625