RecyclerView源码分析(五)滑动机制分析 和 预加载

1,483 阅读11分钟

本篇文章分析下RecyclerView的滑动机制

有几个问题关于滑动机制
Q1: RecyclerView是怎么实现滑动的?
Q2: RecyclerView是多指触摸和fling是怎么处理的?
Q3: 滑动过程中新的View是怎么填充进去的?
Q4: 滑动过程中的会进行回收吗?规则如何?
Q5: 滑动会进行预加载吗?

我们可以带着上面的问题和自己的问题看这篇文章,效率会高很多。

安卓的触摸事件

关于安卓的触摸事件,也就是那几个方法才起作用 分别是

方法作用
dispatchTouchEvevt做点击事件的分发
onInterceptTouchEventviewGroup做点击事件的拦截
onTouchEvevt处理点击事件

RecycleView也是一个view,所以我们从这几个方法讲起。RecycleView对于事件的触发,没有进行自己的额定制,直接继承ViewGroup。所以我们直接分析他的onInterceptTouchEvent,看他是怎么处理拦截事件的,和onTouchEvevt怎么处理点击事件的。
之后会写一篇文章从源码分析整个触摸的传递,虽然是老生常谈,但是有一些细节还需注意。

onInterceptTouchEvent

public boolean onInterceptTouchEvent(MotionEvent e) {
        if (mLayoutSuppressed) {
            //事件拦截模式下,不拦截任何事件和
            return false;
        }

        if (findInterceptingOnItemTouchListener(e)) {
            //处理外部的拦截事件listener
            cancelScroll();
            return true;
        }
        if (mLayout == null) {
            return false;
        }
        。。。
        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
        final boolean canScrollVertically = mLayout.canScrollVertically();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                。。。
                mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
                break;
            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;

            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    final int dx = x - mInitialTouchX;
                    final int dy = y - mInitialTouchY;
                    boolean startScroll = false;
                    // 判断是否应该拦截,滑动超过了临界值
                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                        mLastTouchX = x;
                        startScroll = true;
                    }
                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                        mLastTouchY = y;
                        startScroll = true;
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }
            } break;
            case MotionEvent.ACTION_POINTER_UP: {
                onPointerUp(e);
            } break;
            case MotionEvent.ACTION_UP: {
                mVelocityTracker.clear();
                stopNestedScroll(TYPE_TOUCH);
            } break;
            case MotionEvent.ACTION_CANCEL: {
                cancelScroll();
            }
        }
        return mScrollState == SCROLL_STATE_DRAGGING;
    }

这个方法主要判断是否拦截触摸事件。
代码中可以看到是否拦截事件是通过mScrollState判断的,如果是SCROLL_STATE_DRAGGING,就表示拦截这个事件。 mScrollState里面有几个状态比较重要,mScrollState表示RecyclerView的滑动状态:

mScrollState含义
SCROLL_STATE_IDLERecyclerView 当前未滚动
SCROLL_STATE_DRAGGINGRecyclerView 当前正被外部输入(例如用户触摸输入)拖动。
SCROLL_STATE_SETTLINGRecyclerView 当前正在动画到最终位置,而不受外部控制。

提前拦截

根据外部状态提前处理事件,可以看到进行了3次提前的拦截

  1. 首先我们看到mLayoutSuppressed的使用,他是通过suppressLayout()进行控制,会阻止RecyclerView布局和滚动。具体感兴趣可以看源码。
  2. findInterceptingOnItemTouchListener()方法会找到外部通过addOnItemTouchListener()传入的事件处理回掉,看看他们要不要拦截这个事件。相当于外部拦截器的功能。
  3. 在LayoutManager为空的情况下,是不会拦截任何点击事件的。

ACTION_DOWN事件

这里主要是记录起始点的信息,包括起点的X、Y坐标。逻辑比较简单这。有一个地方可以看下,在mScrollState为SCROLL_STATE_SETTLING,也就是脱离拖动正在运动到最终位置时,如果这时我们进行触摸事件,触发DOWN事件,会调用setScrollState()方法,进而调用stopScrollersInternal()方法,这里会停止当前的滑动。这也印证了一个现象,就是在RecyclerView运动时,我们点击屏幕,会出现滑动停止的效果。

   
    if (mScrollState == SCROLL_STATE_SETTLING) {
        getParent().requestDisallowInterceptTouchEvent(true);
        setScrollState(SCROLL_STATE_DRAGGING);
    }

    void setScrollState(int state) {
        if (state == mScrollState) {
            return;
        }
        mScrollState = state;
        if (state != SCROLL_STATE_SETTLING) {
            stopScrollersInternal();
        }
        dispatchOnScrollStateChanged(state);
    }

ACTION_POINTER_DOWN 事件

这里主要处理多指点击的情况,当新手指落下时触发。会用新手指的ID刷新正在处理的手指ID,在后面的Move事件中,正是拿这个ID进行处理的滑动。所以如果放下两个手指,第一个手指的运动时不起作用的现象。这里也得到了印证。内部处理的逻辑和ACTION_DOWN事件一致。

ACTION_MOVE 事件

移动手指时触发,我们会拿生效的那个手指(最后落下的那个手指)的最新位置进行处理。这里的逻辑比较简单,主要是判断是否构成了滑动,如果能水平滚动,看水平的方向上产生的位移是否大于mTouchSlop。垂直方向也一样。如果有任一个方向生效,就设置状态为SCROLL_STATE_DRAGGING,这时就表示需要拦截事件,RecycleView来自己处理整套的事件。

onInterceptTouchEven的逻辑比较简单,我们只要知道什么情况下进行拦截就可以了。在Down事件中,是不进行拦截的,如果在Move中产生了一定的滑动距离,会进行拦截。

下面看下拦截后的onTouchEvent()是怎么实现的

onTouchEvent

public boolean onTouchEvent(MotionEvent e) {
        if (mLayoutSuppressed || mIgnoreMotionEventTillDown) {
            return false;
        }
        if (dispatchToOnItemTouchListeners(e)) {
            cancelScroll();
            return true;
        }

        if (mLayout == null) {
            return false;
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                  。。。
            } break;

            case MotionEvent.ACTION_POINTER_DOWN: {
                  。。。
            } break;
            case MotionEvent.ACTION_MOVE: {
                //和onInterceptTouchEvent确定SCROLL_STATE_DRAGGING状态逻辑一致
                。。。
                if (mScrollState == SCROLL_STATE_DRAGGING) {
                   // 处理嵌套滑动
                    if (dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                       。。。
                    }
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            e)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        // 预加载
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;

            case MotionEvent.ACTION_POINTER_UP: {
                onPointerUp(e);
            } break;

            case MotionEvent.ACTION_UP: {
                。。。
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetScroll();
            } break;

            case MotionEvent.ACTION_CANCEL: {
                cancelScroll();
            } break;
        }

        return true;
    }

起始也是几个方法进行提前的拦截,我们看到和onInterceptTouchEvent的判断基本一致,这里就不做分析了。 接下来就是对各个事件的分析,down事件做了初始位置的赋值,move事件查看是否构成了滑动,知识后面有点独特的逻辑。up事件里,我们看到了先算出了速度,接着执行了fling。所以我们分析move和up事件即可。

为什么onTouchEvent也需要也要判断move事件查看是否构成了滑动呢,在什么场景下会执行呢?

ACTION_DOWN事件

我们看到了dispatchNestedPreScroll方法,这个方法主要是处理嵌套滑动的。嵌套滑动也是一个比较重要的知识点,我们后面会专门讲这个。这里可以先要理解成自己产生的滑动事件,询问外部是否要先消耗一部分。
可以看到在执行完成dispatchNestedPreScroll方法后,同时也对dx和dy进行了削减,也就是表示外部已经消耗了部分滑动事件。接着会调用scrollByInternal方法,这个方法内部处理了真正的滑动事件。

    boolean scrollByInternal(int x, int y, MotionEvent ev) {
        consumePendingUpdateOperations();
        if (mAdapter != null) {
            scrollStep(x, y, mReusableIntPair);
            。。。
        }
        if (!mItemDecorations.isEmpty()) {
            //滑动过程中刷新decorations
            invalidate();
        }
        。。。
        // 处理嵌套滑动相关
        return consumedNestedScroll || consumedX != 0 || consumedY != 0;
    }
    
    

方法内部调用了scrollStep,并进行了嵌套滑动的处理。在滑动过程中如果有mItemDecorations,还会触发invalidate()进行刷新。接下来看下scrollStep方法。

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
        startInterceptRequestLayout();
        onEnterLayoutOrScroll();

        int consumedX = 0;
        int consumedY = 0;
        if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if (dy != 0) {
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        }
    }
    
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
        if (mOrientation == VERTICAL) {
            return 0;
        }
        return scrollBy(dx, recycler, state);
    }
       

看到这里直接调用了,LayoutManger的scrollHorizontallyBy或者scrollVerticallyBy,这内部才是真正处理滑动事件的地方。内部会直接调用scrollBy方法。可以看出LayoutManger不光负责测量布局,还负责滚动的处理。scrollHorizontallyBy可以看出还过滤了滚动的方向,如果要让LayoutManger处理滚动,我们还需要设置滑动方向(默认是垂直)。

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || delta == 0) {
            return 0;
        }
        ensureLayoutState();
        mLayoutState.mRecycle = true;
        final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDelta = Math.abs(delta);
        updateLayoutState(layoutDirection, absDelta, true, state);
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
       
        final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
        mOrientationHelper.offsetChildren(-scrolled);
        mLayoutState.mLastScrollDelta = scrolled;
        return scrolled;
    }

我们可以看到这里直接调用了fill方法,这个方法我们比较熟悉,是进行布局填充的,相信这里我们可以找到滑动或称重新填充的答案所在。
滑动事件的处理主要是通过mOrientationHelper.offsetChildren进行的。OrientationHelper封装了很多有用的工具方法,较少了很多的样板代码,我们自己开发时,也可以使用,分为垂直的和水平的。
内部会直接调用每个子View的offsetTopAndBottom方法或者offsetChildrenHorizontal方法进行位移。RecycleView是怎么滑动的我们现在应该很清晰了。

public void offsetChildrenVertical(@Px int dy) {
        final int childCount = mChildHelper.getChildCount();
        for (int i = 0; i < childCount; i++) {
            mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
        }
    }

下面主要看下填充的主要逻辑。

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

这里的逻辑比较简单,计算滑动的速度,再执行fling方法,这里看到了filing,fling方法内部会调用mViewFlinger.fling()处理filing事件,内部调用逻辑比较简单,这里就不细谈。

滑动过程中的填充

可以先看下填充的主要代码,上面谈到过。在LinearLayoutManager#scrollBy中。

final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDelta = Math.abs(delta);
updateLayoutState(layoutDirection, absDelta, true, state);
final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);

就这么几行,delta变量是滚动的距离。正负标志了滑动的方向,正数代表向上滑动(LayoutState.LAYOUT_END),负数代表向下滑动(LayoutState.LAYOUT_START)。可以看出layoutDirection这个参数表示要布局的方向,向上滑动布局底部,向下滑动布局顶部。然后通过调用updateLayoutState设置了一些mLayoutState里面的变量,这里包含了很多布局方面的参数。


requiredSpace:滑动的距离

final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
private void updateLayoutState(int layoutDirection, int requiredSpace,
            boolean canUseExistingSpace, RecyclerView.State state) {
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mExtra = getExtraLayoutSpace(state);
        mLayoutState.mLayoutDirection = layoutDirection;
        int scrollingOffset;
        if (layoutDirection == LayoutState.LAYOUT_END) {
            //向上滑动配置参数
            final View child = getChildClosestToEnd();
            。。。
            scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                    - mOrientationHelper.getEndAfterPadding();
        } else {
            //向下滑动配置参数
            final View child = getChildClosestToStart();
            。。。
            scrollingOffset = -mOrientationHelper.getDecoratedStart(child)
                    + mOrientationHelper.getStartAfterPadding();
        }
        mLayoutState.mAvailable = requiredSpace;
        if (canUseExistingSpace) {
            mLayoutState.mAvailable -= scrollingOffset;
        }
        mLayoutState.mScrollingOffset = scrollingOffset;
    }

这个方法内部主要是计算这次填充需要的各种参数,比如绘制的起始点和可绘制的空间等。通过上一篇的分析。可绘制空间主要是通过mLayoutState.mAvailable字段进行控制的。所以我们主要关心mLayoutState.mAvailable即可。在scrollBy的上下文下,canUseExistingSpace为true。所以mAvailable的取值为 滑动距离 - scrollingOffset。这里我们以从垂直方向,向上滑动为例,也就是填充底部的view,看看是怎么具体计算的。

final View child = getChildClosestToEnd();
int scrollingOffset = mOrientationHelper.getDecoratedEnd(child)- mOrientationHelper.getEndAfterPadding();
                    
private View getChildClosestToStart() {
     return getChildAt(mShouldReverseLayout ? getChildCount() - 1 : 0);
}
    
public int getDecoratedEnd(View view) {
     final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)view.getLayoutParams();
     return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
}
public int getEndAfterPadding() {
    return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
}
  1. 首先通过getChildClosestToStart通过数据方向mShouldReverseLayout从ViewGroup获取最顶部的一个view。
    这里的操作比较好理解,因为是向上滑动,也就是说填充底部的view,肯定要看最底部view的情况,是否应该继续填充,如果这个view还有空间没有显示出来,看没显示的空间是否小于滑动距离,如果显示的空间大就不应该填充。相反如果滑动距离大应该进行填充,所以下面的计算的逻辑我们大体也可以猜到
  2. 通过getDecoratedEnd计算最底部view的bottom,再计算RecycleView的高度。两数相减这个计算正好是上面我们猜想的,判断是否还有没显示的。通过下面的计算就算出了最终的 mAvailable,可填充区域大小。
    requiredSpace为滑动距离
    
    mLayoutState.mAvailable = requiredSpace;  
    mLayoutState.mAvailable -= scrollingOffset;
    
  3. 到fill方法中,如果可以填充的空间,就填充。 到这里我们可以解答RecycleView是在滑动中是怎么进行填充的,滑动过程中会交给LayoutManger进行执行。并且也会执行上一章讲到的第一次填充的fill方法,内部会对传入的各种配置进行填充。滑动中的配置在updateLayoutState方法里进行。如果顶部或底部的view还有没展示全的,是不会进行填充的。这样我们就回答了Q3。

滑动中的回收

显然滑动肯定会回收离开屏幕的item,那么是怎么实现的呢。我们还是看LinearLayoutManager#fill()

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
    。。。
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        recycleByLayoutState(recycler, layoutState);
    }
    。。。
}

主要逻辑实在fill方法内部通过调用recycleByLayoutState进行回收的。方法内部对mScrollingOffset进行了判断,不为LayoutState.SCROLLING_OFFSET_NaN则执行回收。
mScrollingOffset的赋值主要有两处

  1. 在首次填充的updateLayoutStateToFillEnd/updateLayoutStateToFillStart参数配置方法内
  2. 滚动填充的updateLayoutState参数配置犯法内 updateLayoutStateToFillEnd/updateLayoutStateToFillStart中直接赋值为LayoutState.SCROLLING_OFFSET_NaN。所以不进行回收。
    updateLayout上面分析过。mScrollingOffset的值表示首末的item没有显示出来的高度。

知道他的赋值逻辑之后,看下具体的回收逻辑是如何执行的。

private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        if (!layoutState.mRecycle || layoutState.mInfinite) {
            return;
        }
        int scrollingOffset = layoutState.mScrollingOffset;
        int noRecycleSpace = layoutState.mNoRecycleSpace;
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
        } else {
            recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
        }
    }
    

具体的回收逻辑在recycleByLayoutState中,根据布局的方向进行回收。LayoutState.LAYOUT_START情况下,要填充顶部,那么肯定要回收底部。从代码也可以看出调用了recycleViewsFromEnd。相反则调用recycleViewsFromStart。这里以向上滑动,回收顶部view调用recycleViewsFromStart举例。

private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,
            int noRecycleSpace) {
        final int limit = scrollingOffset - noRecycleSpace;
        final int childCount = getChildCount();
        if (mShouldReverseLayout) {
           。。。
        } else {
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                //是否进行回收
                if (mOrientationHelper.getDecoratedEnd(child) > limit
                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                    recycleChildren(recycler, 0, i);
                    return;
                }
            }
        }
    }

是否进行回收根据主要根据两个条件,mOrientationHelper.getDecoratedEnd(child)和mOrientationHelper.getTransformedEndWithDecoration(child)和limit的比较,后者对应了矩阵变化,这里不做分析。直接看前者的判断即可。如果满足mOrientationHelper.getDecoratedEnd(child) > limit,就会进行回收,我们看下判断的左右。

  • 左 mOrientationHelper.getDecoratedEnd(child) mOrientationHelper.getDecoratedEnd(child)的实现上面也说过,是拿这个View在RecyclerView在的bottom值。

  • 右 limit 这里直接和limit进行比较,limit外部传入的直接是layoutState.mScrollingOffset。layoutState.mScrollingOffset的计算如下。

if (layoutState.mAvailable < 0) {
    layoutState.mScrollingOffset += layoutState.mAvailable;
}

这里分了两种情况,layoutState.mAvailable是否大于0。

  1. 小于0,表示当前不能进行填充。所以mScrollingOffset的值在前面updateLayoutState方法中计算为
    requiredSpace为滑动距离
    
    mLayoutState.mAvailable = requiredSpace;  
    mLayoutState.mAvailable -= scrollingOffset;
    
    这里又出现了layoutState.mScrollingOffset += layoutState.mAvailable。所以mScrollingOffset直接变成了滑动距离。
    这种条件下右 limit为滑动距离
  2. 大于0,这是表示可以进行填充。那么scrollingOffset就是可以填充的大小。
    这种条件下右 limit为填充距离。 根据条件判断,如果从上到下,每个view的bottom如果大于limit,就应该被回收。我们根据limit的含义分别看下回收的具体逻辑。
  3. 大于滑动距离,如果第一个view的bottom大于滑动距离,这时候就应该回收吗,显然不是的,可以想象一下,只有这个view不可见了,才应该回收。但是按照判断,这时这个判断是true,随后就会调用recycleChildren(recycler, 0, 0);这时会直接return,不进行回收,是!确实不应该回收,但是这个逻辑就奇怪了。但是答案也比较好想,如果第二个view的bottom大于滑动距离,那么这时候就会调用recycleChildren(recycler, 0, 1),这时会回收第0个,想想这时候的情况,确实是应该回收第0个,因为他的bottom比滑动距离要下,经过滑动,肯定出了显示区域了,需要回收。
private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
    if (startIndex == endIndex) {
        return;
    }
    if (endIndex > startIndex) {
        for (int i = endIndex - 1; i >= startIndex; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    } else {
        for (int i = startIndex; i > endIndex; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    }
}
  1. 大于填充距离,这个和上面的处理逻辑一致。不做分析了。

为什么分填充距离和滑动距离,两种呢? 其实滑动过程中,如果可以进行填充,那么填充距离和滑动距离基本是一致的。

预加载

RecycleView会预加载吗?答案是肯定会的。
是通过GapWorker进行的。在onTouchEvent()中的MOVE事件中,通过调用mGapWorker的postFromTraversal进行预加载,另一个入口在fling处理中,也就是产生滑动会判断是否需要预加载。大体思路就是通过移动的dx和dy,判断需要预加载的postion,提前进行创建。

if (mGapWorker != null && (dx != 0 || dy != 0)) {
    mGapWorker.postFromTraversal(this, dx, dy);
}                     

跟踪发现,处理dx、dy逻辑的在collectAdjacentPrefetchPositions() 方法内。这个方法在LayoutManger里是空实现,如果我们自定义的LayoutManger需要预加载功能的话,需要自己实现这个方法。我们看下LinearLayoutManager的实现。

public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
            LayoutPrefetchRegistry layoutPrefetchRegistry) {
        int delta = (mOrientation == HORIZONTAL) ? dx : dy;
        if (getChildCount() == 0 || delta == 0) {
            return;
        }

        ensureLayoutState();
        final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDy = Math.abs(delta);
        updateLayoutState(layoutDirection, absDy, true, state);
        collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry);
    }
    void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState,
            LayoutPrefetchRegistry layoutPrefetchRegistry) {
        final int pos = layoutState.mCurrentPosition;
        if (pos >= 0 && pos < state.getItemCount()) {
            layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset));
        }
    }

逻辑还算比较简单,先通过上面讲到的updateLayoutState配置LayoutState参数,再在collectPrefetchPositionsForLayoutState内部拿到layoutState要加载的mCurrentPosition。最终传入layoutPrefetchRegistry中,完成要预加载的postion的获取。
看下GapWorker在拿到需要预加载的数据是怎么进行预加载的。

private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
            int position, long deadlineNs) {
        if (isPrefetchPositionAttached(view, position)) {
            return null;
        }

        RecyclerView.Recycler recycler = view.mRecycler;
        RecyclerView.ViewHolder holder;
        try {
            view.onEnterLayoutOrScroll();
            holder = recycler.tryGetViewHolderForPositionByDeadline(
                    position, false, deadlineNs);
           。。。
        } finally {
            view.onExitLayoutOrScroll(false);
        }
        return holder;
    }

看到了我们熟悉的tryGetViewHolderForPositionByDeadline方法,内部会通过RecycleView的四级缓存进行提取。缓存里没有那么就直接调用了onCreateView(),我们熟悉的方法。这样就提前创建了需要预加载的ViewHolder。

总结

相信通过上面的分析,我们可以回答上面的五个问题了,可以尝试回答下,不了解的可以自己阅读源码,并通过这篇文章仔细研究。

下一章我们会分析RecyclerView最重量级的缓存机制了,这也RecyclerView的核心部分。