RecyclerView 源码分析(二) - RecyclerView的滑动机制

1,718 阅读10分钟

  RecyclerView作为一个列表View,天生就可以滑动。作为一个使用者,我们可以不去了解它是怎么进行滑动,但是我们作为一个学习源码的人,必须得知道RecyclerView的滑动机制,所以,我们今天来看看RecyclerView滑动部分的代码。

  本文参考资料:

  1. Android 源码分析 - 嵌套滑动机制的实现原理
  2. 深入 RecyclerView 源码探究三:绘制和滑动

  同时,从RecyclerView的类结构上来看,我们知道RecyclerView实现了NestedScrollingChild接口,所以RecyclerView也是一个可以产生滑动事件的View。我相信大家都有用过CoordinatorLayoutRecyclerView这个组合,这其中原理的也是嵌套滑动。本文在介绍普通滑动中,可能会涉及到嵌套滑动的知识,所以在阅读本文时,需要大家掌握嵌套滑动的机制,具体可以参考我上面的文章:Android 源码分析 - 嵌套滑动机制的实现原理,此文专门从RecyclerView的角度上来理解嵌套滑动的机制。

  本文打算从如下几个方面来分析RecyclerView

  1. 正常的TouchEvent
  2. 嵌套滑动(穿插着文章各个地方,不会专门的讲解)
  3. 多指滑动
  4. fling滑动

1. 传统事件

  现在,我们正式分析源码,首先我们来看看onTouchEvent方法,来看看它为我们做了那些事情:

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        // ······
        if (dispatchOnItemTouch(e)) {
            cancelTouch();
            return true;
        }
        // ······
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                // ······
            } break;

            case MotionEvent.ACTION_POINTER_DOWN: {
                // ······
            } break;
            case MotionEvent.ACTION_MOVE: {
                // ······
            } break;
            case MotionEvent.ACTION_POINTER_UP: {
                // ······
            } break;
            case MotionEvent.ACTION_UP: {
                // ······
            } break;
            case MotionEvent.ACTION_CANCEL: {
                cancelTouch();
            } break;
        }
        // ······
        return true;
    }

  如上就是RecyclerViewonTouchEvent方法,我大量的简化了这个方法,先让大家对它的结构有一个了解。

  其中ACTION_DOWNACTION_MOVEACTION_UPACTION_CANCEL这几个事件,我相信各位同学都比较熟悉,这是View最基本的事件。

  可能有人对ACTION_POINTER_DOWNACTION_POINTER_UP事件比较陌生,这两个事件就跟多指滑动有关,也是本文重点分析之一。


  好了,我们现在开始正式分析源码。在分析源码之前,我先将上面的代码做一个简单的概述。

  1. 如果当前的mActiveOnItemTouchListener需要消耗当前事件,那么优先交给它处理。
  2. 如果mActiveOnItemTouchListener不消耗当前事件,那么就走正常的事件分发机制。这里面有很多的细节,稍后我会详细的介绍。

  关于第一步,这里不用我来解释,它就是一个Listener的回调,非常的简单,我们重点的在于分析第二步。

(1). Down 事件

  我们先来看看这部分的代码吧。

            case MotionEvent.ACTION_DOWN: {
                mScrollPointerId = e.getPointerId(0);
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            } break;

  这里主要是做了两件事。

  1. 记录下Down事件的x、y坐标。
  2. 调用startNestedScroll方法,询问父View是否处理事件。

  Down事件还是比较简单,通常来说就一些初始化的事情。

  接下来,我们来看看重头戏--move事件

(2). Move事件

  我们先来看看这部分的代码:

            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id "
                            + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                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 (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                    // Updated the nested offsets
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                }

                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    boolean startScroll = false;
                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                        if (dx > 0) {
                            dx -= mTouchSlop;
                        } else {
                            dx += mTouchSlop;
                        }
                        startScroll = true;
                    }
                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                        if (dy > 0) {
                            dy -= mTouchSlop;
                        } else {
                            dy += mTouchSlop;
                        }
                        startScroll = true;
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }

                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;

  这部分代码非常的简单,我将它分为如下几步:

  1. 根据Move事件产生的x、y坐标来计算dx、dy。
  2. 调用dispatchNestedPreScroll询问父View是否优先处理滑动事件,如果要消耗,dx和dy分别会减去父View消耗的那部分距离。
  3. 然后根据情况来判断RecyclerView是垂直滑动还是水平滑动,最终是调用scrollByInternal方法来实现滑动的效果的。
  4. 调用GapWorkerpostFromTraversal来预取ViewHolder。这个过程会走缓存机制部分的逻辑,同时也有可能会调用AdapteronBindViewHolder方法来提前加载数据。

  其中第一步和第二步都是比较简单的,这里就直接省略。

  而scrollByInternal方法也是非常的简单,在scrollByInternal方法内部,实际上是调用了LayoutManagerscrollHorizontallyBy方法或者scrollVerticallyBy方法来实现的。LayoutManager这两个方法实际上也没有做什么比较骚的操作,归根结底,最终调用了就是调用了每个ChildoffsetTopAndBottom或者offsetLeftAndRight方法来实现的,这里就不一一的跟踪代码了,大家了解就行了。在本文的后面,我会照着RecyclerView滑动相关的代码写一个简单的Demo。

  在这里,我们就简单的分析一下GapWorker是怎么进行预取的。我们来看看postFromTraversal方法:

    void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
        if (recyclerView.isAttachedToWindow()) {
            if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
                throw new IllegalStateException("attempting to post unregistered view!");
            }
            if (mPostTimeNs == 0) {
                mPostTimeNs = recyclerView.getNanoTime();
                recyclerView.post(this);
            }
        }

        recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
    }

  在postFromTraversal方法内部也没有做多少事情,最核心在于调用了post方法,向任务队列里面添加了一个Runnable。看来重点的分析还是GapWorkerrun方法:

    @Override
    public void run() {
        try {
            TraceCompat.beginSection(RecyclerView.TRACE_PREFETCH_TAG);

            if (mRecyclerViews.isEmpty()) {
                // abort - no work to do
                return;
            }

            // Query most recent vsync so we can predict next one. Note that drawing time not yet
            // valid in animation/input callbacks, so query it here to be safe.
            final int size = mRecyclerViews.size();
            long latestFrameVsyncMs = 0;
            for (int i = 0; i < size; i++) {
                RecyclerView view = mRecyclerViews.get(i);
                if (view.getWindowVisibility() == View.VISIBLE) {
                    latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
                }
            }

            if (latestFrameVsyncMs == 0) {
                // abort - either no views visible, or couldn't get last vsync for estimating next
                return;
            }

            long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;

            prefetch(nextFrameNs);

            // TODO: consider rescheduling self, if there's more work to do
        } finally {
            mPostTimeNs = 0;
            TraceCompat.endSection();
        }
    }

  run方法的逻辑也是非常简单,首先计算获得下一帧的时间,然后调用prefetch方法进行预取ViewHolder

    void prefetch(long deadlineNs) {
        buildTaskList();
        flushTasksWithDeadline(deadlineNs);
    }

  prefetch方法也简单,显示调用buildTaskList方法生成任务队列,然后调用flushTasksWithDeadline来执行task,这其中会调用RecyclerViewtryGetViewHolderForPositionByDeadline方法来获取一个ViewHolder,这里就不一一分析了。

  不过需要提一句的是,tryGetViewHolderForPositionByDeadline方法是整个RecyclerView缓存机制的核心,RecyclerView缓存机制在这个方法被淋漓尽致的体现出来。关于这个方法,如果不出意外的话,在下一篇文章里面我们就可以接触到,在这里,先给大家卖一个关子😂。

  最后就是Up事件和Cancel事件,这两个事件更加的简单,都进行一些清理的操作,这里就不分析了。不过在Up事件里面,有一个特殊事件可能会产生--fling事件,待会我们会详细的分析。

2. 多指滑动

  大家千万不会误会这里多指滑动的意思,这里的多指滑动不是指RecyclerView能够相应多根手指的滑动,而是指当一个手指还没释放时,此时另一个手指按下,此时RecyclerView就不相应上一个手指的手势,而是相应最近按下手指的手势。

  我们来看看这部分的代码:

            case MotionEvent.ACTION_POINTER_DOWN: {
                mScrollPointerId = e.getPointerId(actionIndex);
                mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
            } break;

  当另一个手指按下时,此时就会立即更新按下的坐标,同时会更新mScrollPointerId,表示后面只会响应最近按下手指的手势。

  其次,我们来看看多指松开的情况:

            case MotionEvent.ACTION_POINTER_UP: {
                onPointerUp(e);
            } break;
    private void onPointerUp(MotionEvent e) {
        final int actionIndex = e.getActionIndex();
        if (e.getPointerId(actionIndex) == mScrollPointerId) {
            // Pick a new pointer to pick up the slack.
            final int newIndex = actionIndex == 0 ? 1 : 0;
            mScrollPointerId = e.getPointerId(newIndex);
            mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
            mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
        }
    }

  在这里也没有比较骚的操作,就是普通的更新。这里就不详细的解释了。本文后面会有一个小Demo,让大家看看根据RecyclerView依葫芦画瓢做出来的效果。

  接下来,我们来最后一个滑动,也是本文最重点分析的滑动--fling滑动。为什么需要重点分析fling事件,因为在我们平常自定义View,fling事件是最容易被忽视的。

3. fling滑动

  我们先来看看fling滑动产生的地方,也是Up事件的地方:

            case MotionEvent.ACTION_UP: {
                mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally
                        ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
                final float yvel = canScrollVertically
                        ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetTouch();
            } break;

  从上面的代码中,我们可以看出来,最终是调用fling方法来是实现fling效果的,我们来看看fling方法:

    public boolean fling(int velocityX, int velocityY) {
        // ······
        if (!dispatchNestedPreFling(velocityX, velocityY)) {
            final boolean canScroll = canScrollHorizontal || canScrollVertical;
            dispatchNestedFling(velocityX, velocityY, canScroll);

            if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
                return true;
            }

            if (canScroll) {
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontal) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertical) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);

                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方法里面,显示调用dispatchNestedPreFling方法询问父View是否处理fling事件,最后调用ViewFlingerfling方法来实现fling效果,所以真正的核心在于ViewFlingerfling方法里面,我们继续来看:

        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);
            postOnAnimation();
        }

  在ViewFlingerfling方法里面,先是调用了OverScrollerfling来计算fling相关的参数,包括fling的距离和fling的时间。这里就不深入的分析计算相关的代码,因为这里面都是一些数学和物理的计算。最后就是调用了postOnAnimation方法。

        void postOnAnimation() {
            if (mEatRunOnAnimationRequest) {
                mReSchedulePostAnimationCallback = true;
            } else {
                removeCallbacks(this);
                ViewCompat.postOnAnimation(RecyclerView.this, this);
            }
        }

  可能大家有可能看不懂上面的代码,其实跟Viewpost差不多,所以最终还是得看ViewFlingerrun方法。

  ViewFlingerrun方法比较长,这里我将它简化了一下:

        public void run() {
            // ······
            // 第一步,更新滚动信息,并且判断当前是否已经滚动完毕
            // 为true表示未滚动完毕
            if (scroller.computeScrollOffset()) {
                //······

                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;
                    }
                    // ······
                }
                // ······
                // 如果滚动完毕,就是调用finish方法;
                // 如果没有滚动完毕,就调用postOnAnimation方法继续递归
                if (scroller.isFinished() || (!fullyConsumedAny
                        && !hasNestedScrollingParent(TYPE_NON_TOUCH))) {
                    // setting state to idle will stop this.
                    setScrollState(SCROLL_STATE_IDLE);
                    if (ALLOW_THREAD_GAP_WORK) {
                        mPrefetchRegistry.clearPrefetchPositions();
                    }
                    stopNestedScroll(TYPE_NON_TOUCH);
                } else {
                    postOnAnimation();
                    if (mGapWorker != null) {
                        mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
                    }
                }
            }
            // ······
        }

  整个fling核心就在这里,通过上面的三步,最终就是实现了fling的效果,上面的注意已经非常的清晰了,这里就不继续分析了。

  我们分析了RecyclerViewfling事件,有什么帮助呢?在日常的开发中,如果需要fling的效果,我们可以根据RecyclerView实现方式来实现,是不是就觉得非常简单呢?对的,这就是我们学习源码的目的,不仅要理解其中的原理,还需要学以致用😂。

4. Demo展示

  这里的demo不是很高大上的东西,就是照着RecyclerView的代码实现了一个多指滑动View而已。我们来看看源码:

public class MoveView extends View {

  private int mLastTouchX;
  private int mLastTouchY;
  private int mTouchSlop;
  private boolean mCanMove;
  private int mScrollPointerId;

  public MoveView(Context context) {
    this(context, null);
  }

  public MoveView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public MoveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    final int actionIndex = event.getActionIndex();
    switch (event.getActionMasked()){
      case MotionEvent.ACTION_DOWN:
        mScrollPointerId = event.getPointerId(0);
        mLastTouchX = (int) (event.getX() + 0.5f);
        mLastTouchY = (int) (event.getY() + 0.5f);
        mCanMove = false;
        break;
      case MotionEvent.ACTION_POINTER_DOWN:
        mScrollPointerId = event.getPointerId(actionIndex);
        mLastTouchX = (int) (event.getX(actionIndex) + 0.5f);
        mLastTouchY = (int) (event.getY(actionIndex) + 0.5f);
        break;
      case MotionEvent.ACTION_MOVE:
        final int index = event.findPointerIndex(mScrollPointerId);
        int x = (int) (event.getX(index) + 0.5f);
        int y = (int) (event.getY(index) + 0.5f);
        int dx = mLastTouchX - x;
        int dy = mLastTouchY - y;
        if(!mCanMove) {
          if (Math.abs(dy) >= mTouchSlop) {
            if (dy > 0) {
              dy -= mTouchSlop;
            } else {
              dy += mTouchSlop;
            }
            mCanMove = true;
          }
          if (Math.abs(dy) >= mTouchSlop) {
            if (dy > 0) {
              dy -= mTouchSlop;
            } else {
              dy += mTouchSlop;
            }
            mCanMove = true;
          }
        }
        if (mCanMove) {
          offsetTopAndBottom(-dy);
          offsetLeftAndRight(-dx);
        }
        break;
      case MotionEvent.ACTION_POINTER_UP:
        onPointerUp(event);
        break;
      case MotionEvent.ACTION_UP:
        break;
    }
    return true;
  }

  private void onPointerUp(MotionEvent e) {
    final int actionIndex = e.getActionIndex();
    if (e.getPointerId(actionIndex) == mScrollPointerId) {
      final int newIndex = actionIndex == 0 ? 1 : 0;
      mScrollPointerId = e.getPointerId(newIndex);
      mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
      mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
    }
  }
}

  相信经过RecyclerView源码的学习,对上面代码的理解也不是难事,所以这里我就不需要再解释了。具体的效果,大家可以拷贝Android studio里面去看看😂。

4. 总结

  RecyclerView的滑动机制相比较来说,还是非常简单,我也感觉没有什么可以总结。不过从RecyclerView的源码,我们可以学习两点:

  1. 多指滑动。我们可以根据RecyclerView的源码,来实现自己的多指滑动,这是一种参考,也是学以致用
  2. fling滑动。RecyclerView实现了fling效果,在日常开发过程中,如果我们也需要实现这种效果,我们可以根据RecyclerView的源码来实现。