阅读 718

AndroidX RecyclerView总结-滑动处理

概述

RecyclerView作为一个灵活的在有限窗口显示大量数据集的视图组件,继承自ViewGroup,需要处理触摸事件产生时子View的滚动。同时RecyclerView实现了NestedScrollingChild接口,也支持嵌套在支持Nested的父容器中。

这里结合LinearLayoutManager,以垂直方向滑动为例,从源码浅析RecyclerView是如何进行滑动事件处理的。

源码探究

文中源码基于 'androidx.recyclerview:recyclerview:1.1.0'

RecyclerView中的处理

RecyclerView和常规事件处理方式一样,重写了onInterceptTouchEventonTouchEvent。RecyclerView也实现了NestedScrollingChild接口,在关键事件节点也会通知实现了NestedScrollingParent接口的父容器。

关于NestedScrollingChild和NestedScrollingParent的简要用法和说明,可参考《关于NestedScrollingParent2、NestedScrollingChild2接口》

onInterceptTouchEvent

[RecyclerView#onInterceptTouchEvent]

public boolean onInterceptTouchEvent(MotionEvent e) {
    // 判断是否抑制布局滚动,可通过suppressLayout方法设置为true,当重新设置Adapter或托管item动画时不拦截。
    if (mLayoutSuppressed) {
        // When layout is suppressed,  RV does not intercept the motion event.
        // A child view e.g. a button may still get the click.
        return false;
    }

    // 省略OnItemTouchListener部分,设置FastScroller或ItemTouchHelper时涉及 ···

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

    // 获取支持滚动的方向。以垂直排列的LinearLayoutManager为例,canScrollVertically为true。
    final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
    final boolean canScrollVertically = mLayout.canScrollVertically();

    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(e);

    final int action = e.getActionMasked();
    final int actionIndex = e.getActionIndex();

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            // mIgnoreMotionEventTillDown默认为false,调用suppressLayout抑制布局滚动时会将其置为true
            if (mIgnoreMotionEventTillDown) {
                mIgnoreMotionEventTillDown = false;
            }
            // 获取第一个触摸点的ID
            mScrollPointerId = e.getPointerId(0);
            // 保存DOWN时X、Y坐标,用于计算滑动偏移量
            mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
            mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

            // 判断当前滑动状态是否是惯性滑动或其他非用户触摸滑动,mScrollState默认为SCROLL_STATE_IDLE
            if (mScrollState == SCROLL_STATE_SETTLING) {
                // 请求父布局不拦截事件
                getParent().requestDisallowInterceptTouchEvent(true);
                // 更新滑动状态为SCROLL_STATE_DRAGGING
                setScrollState(SCROLL_STATE_DRAGGING);
                // 通知父布局停止滑动,类型为TYPE_NON_TOUCH
                stopNestedScroll(TYPE_NON_TOUCH);
            }

            // Clear the nested offsets
            mNestedOffsets[0] = mNestedOffsets[1] = 0;

            // 获取当前支持的滑动方向
            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;

        case MotionEvent.ACTION_POINTER_DOWN:
            // 有新的触摸点,更新触摸点ID和初始X、Y坐标以新的为准
            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);
            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);
            // 判断当前滑动状态是否是SCROLL_STATE_DRAGGING
            if (mScrollState != SCROLL_STATE_DRAGGING) {
                // 计算滑动偏移量
                final int dx = x - mInitialTouchX;
                final int dy = y - mInitialTouchY;
                // 标记是否有任一方向可以滑动
                boolean startScroll = false;
                // 判断是否构成滑动,mTouchSlop为最小滑动距离
                if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                    // 保存刚开始滑动时的坐标
                    mLastTouchX = x;
                    startScroll = true;
                }
                if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                    // 保存刚开始滑动时的坐标
                    mLastTouchY = y;
                    startScroll = true;
                }
                if (startScroll) {
                    // 可以构成滑动,更新状态为SCROLL_STATE_DRAGGING,
                    setScrollState(SCROLL_STATE_DRAGGING);
                }
            }
        } break;

        case MotionEvent.ACTION_POINTER_UP: {
            // 有一个触摸点离开,若该触摸点是mScrollPointerId,会更新触摸点ID和坐标为其他触摸点
            onPointerUp(e);
        } break;

        case MotionEvent.ACTION_UP: {
            mVelocityTracker.clear();
            // 通知父容器停止滑动
            stopNestedScroll(TYPE_TOUCH);
        } break;

        case MotionEvent.ACTION_CANCEL: {
            // 停止滑动,重置状态,mScrollState置为SCROLL_STATE_IDLE
            cancelScroll();
        }
    }
    // 若当前mScrollState为SCROLL_STATE_DRAGGING,则返回true,表示拦截事件
    return mScrollState == SCROLL_STATE_DRAGGING;
}
复制代码

RecyclerView的onInterceptTouchEvent方法中并没有特殊逻辑,即常规的滑动距离判断拦截。 其中有涉及多点触摸相关说明可参考《ViewGroup事件分发总结-多点触摸事件拆分》

滑动状态

RecyclerView的mScrollState成员表示当前滑动状态,状态有三种:

  • SCROLL_STATE_IDLE:默认状态,当前没有滑动
  • SCROLL_STATE_DRAGGING:用户触摸滑动
  • SCROLL_STATE_SETTLING:非用户触摸滑动,例如fling惯性滑动、smoothScrollBy指定滑动

通过setScrollState方法更新滑动状态,并触发onScrollStateChanged回调

onTouchEvent

[RecyclerView#onTouchEvent]

public boolean onTouchEvent(MotionEvent e) {
    if (mLayoutSuppressed || mIgnoreMotionEventTillDown) {
        return false;
    }
    // 派发OnItemTouchListener,设置FastScroller或ItemTouchHelper时涉及
    if (dispatchToOnItemTouchListeners(e)) {
        cancelScroll();
        return true;
    }

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

    // 获取可滑动方向
    final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
    final boolean canScrollVertically = mLayout.canScrollVertically();

    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    boolean eventAddedToVelocityTracker = false;

    final int action = e.getActionMasked();
    final int actionIndex = e.getActionIndex();

    if (action == MotionEvent.ACTION_DOWN) {
        mNestedOffsets[0] = mNestedOffsets[1] = 0;
    }
    // vtev和mNestedOffsets仅用于加速度追踪,可忽略
    final MotionEvent vtev = MotionEvent.obtain(e);
    vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            // 获取触摸点ID和初始坐标
            mScrollPointerId = e.getPointerId(0);
            // mInitialTouchX记录DOWN时坐标,mLastTouchX记录最后一次触摸坐标
            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;

        case MotionEvent.ACTION_POINTER_DOWN: {
            // 更新触摸点ID和初始坐标
            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);
            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 (mScrollState != SCROLL_STATE_DRAGGING) {
                // 若是刚开始滑动,则根据mTouchSlop微调偏移量
                boolean startScroll = false;
                if (canScrollHorizontally) {
                    if (dx > 0) {
                        dx = Math.max(0, dx - mTouchSlop);
                    } else {
                        dx = Math.min(0, dx + mTouchSlop);
                    }
                    if (dx != 0) {
                        startScroll = true;
                    }
                }
                if (canScrollVertically) {
                    if (dy > 0) {
                        dy = Math.max(0, dy - mTouchSlop);
                    } else {
                        dy = Math.min(0, dy + mTouchSlop);
                    }
                    if (dy != 0) {
                        startScroll = true;
                    }
                }
                if (startScroll) {
                    // 可以进行滑动,则更新状态为SCROLL_STATE_DRAGGING
                    setScrollState(SCROLL_STATE_DRAGGING);
                }
            }

            // 判断当前是否可以滑动
            if (mScrollState == SCROLL_STATE_DRAGGING) {
                // mReusableIntPair用作对象复用,可避免频繁创建数组
                mReusableIntPair[0] = 0;
                mReusableIntPair[1] = 0;
                // 通知父容器优先处理滑动,利用mReusableIntPair保存父容器消耗的滑动距离
                // mScrollOffset保存RecyclerView左上点相较于父容器的偏移坐标
                if (dispatchNestedPreScroll(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        mReusableIntPair, mScrollOffset, TYPE_TOUCH
                )) {
                    // 滑动偏移量减去父布局消耗的距离
                    dx -= mReusableIntPair[0];
                    dy -= mReusableIntPair[1];
                    // Updated the nested offsets
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                    // Scroll has initiated, prevent parents from intercepting
                    // 请求父容器不拦截事件
                    getParent().requestDisallowInterceptTouchEvent(true);
                }

                // 更新最后一次触摸坐标(这里不直接用x、y,是担心父布局处理滑动时可能造成RecyclerView偏移)
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];

                // 进一步执行滑动逻辑,若有进行滑动则会返回true
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        e)) {
                    // 请求父容器不拦截事件
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                // 预取ViewHolder相关,根据滑动距离判断是否需要预取
                if (mGapWorker != null && (dx != 0 || dy != 0)) {
                    mGapWorker.postFromTraversal(this, dx, dy);
                }
            }
        } break;

        case MotionEvent.ACTION_POINTER_UP: {
            // 更新触摸点ID和坐标
            onPointerUp(e);
        } break;

        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))) {
                // 若不需要fling,则更新状态为SCROLL_STATE_IDLE
                setScrollState(SCROLL_STATE_IDLE);
            }
            resetScroll();
        } break;

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

    if (!eventAddedToVelocityTracker) {
        mVelocityTracker.addMovement(vtev);
    }
    vtev.recycle();

    // 进了switch语句并且没出错的话,就都返回true
    return true;
}
复制代码

可以看到onTouchEvent中的逻辑和onInterceptTouchEvent大同小异,真正处理滑动在scrollByInternal方法中。在scrollByInternal中先判断延迟的适配器更新操作,然后调用scrollStep方法再进一步处理滑动,之后处理NestedScroll派发、过渡滑动效果、onScrollChanged回调、滚动条等,最后返回是否产生滑动距离消耗。

scrollStep

[RecyclerView#scrollStep]

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
    // ···

    int consumedX = 0;
    int consumedY = 0;
    if (dx != 0) {
        // 若水平滑动量不为0,调用scrollHorizontallyBy方法
        consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
    }
    if (dy != 0) {
        // 若垂直滑动量不为0,调用scrollVerticallyBy方法
        consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }

    // ···

    if (consumed != null) {
        // 保存LayoutManager进行滑动消耗的量
        consumed[0] = consumedX;
        consumed[1] = consumedY;
    }
}
复制代码

该方法中判断水平和垂直滑动偏移量若不为0,则调用LayoutManager的对应的scrollHorizontallyBy、scrollVerticallyBy方法,默认返回0,LayoutManager的具体子类重写对应方法实现自己的滑动逻辑。

这里以LinearLayoutManager为例,看看它的垂直滑动相关的处理。

LinearLayoutManager中的处理

RecyclerView将子View的滑动逻辑交由LayoutManager来处理,在LinearLayoutManager的scrollVerticallyBy方法中又调用了scrollBy方法(scrollHorizontallyBy也会调用该方法,两个方法调用前会判断是否是对应的排列方向),进入该方法看看是如何处理垂直滑动的。

[LinearLayoutManager#scrollBy]

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getChildCount() == 0 || delta == 0) {
        return 0;
    }
    ensureLayoutState();
    // 标记可以回收ViewHolder
    mLayoutState.mRecycle = true;
    // 根据滑动偏移量判断布局方向,delta>0表示手指往上划,对应LAYOUT_END
    final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    final int absDelta = Math.abs(delta);
    // 更新mLayoutState中的成员的值
    updateLayoutState(layoutDirection, absDelta, true, state);
    // 计算布局填充
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    if (consumed < 0) {
        if (DEBUG) {
            Log.d(TAG, "Don't have any more elements to scroll");
        }
        return 0;
    }
    final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
    // 子View滑动偏移
    mOrientationHelper.offsetChildren(-scrolled);
    if (DEBUG) {
        Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
    }
    mLayoutState.mLastScrollDelta = scrolled;
    return scrolled;
}
复制代码

该方法中有三个比较关键的步骤:1)updateLayoutState;2)fill;3)offsetChildren。 依次看看方法。

updateLayoutState

[LinearLayoutManager#updateLayoutState]

private void updateLayoutState(int layoutDirection, int requiredSpace,
        boolean canUseExistingSpace, RecyclerView.State state) {
    // ···
    mLayoutState.mLayoutDirection = layoutDirection;
    // ···
    int scrollingOffset;
    // 这里以LAYOUT_END(手指上划)为例
    if (layoutToEnd) {
        mLayoutState.mExtraFillSpace += mOrientationHelper.getEndPadding();
        // get the first child in the direction we are going
        // 找到最底下的那个child
        final View child = getChildClosestToEnd();
        // the direction in which we are traversing children
        mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                : LayoutState.ITEM_DIRECTION_TAIL;
        mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
        // 获取最底下那个child的底边界
        mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
        // calculate how much we can scroll without adding new children (independent of layout)
        // 计算child底边界和RecyclerView内容底边界的距离,若滑动距离在这个范围内,不需要获取新的ViewHolder
        scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                - mOrientationHelper.getEndAfterPadding();

    } else {
        // ···
    }
    // requiredSpace即滑动距离绝对值,mAvailable保存滑动空间
    mLayoutState.mAvailable = requiredSpace;
    // 此时canUseExistingSpace为true
    if (canUseExistingSpace) {
        // mAvailable变为滑动距离和无新增范围的差值
        mLayoutState.mAvailable -= scrollingOffset;
    }
    // mScrollingOffset保存无新增滚动范围
    mLayoutState.mScrollingOffset = scrollingOffset;
}
复制代码

该方法中计算了一些滑动范围相关的值。

其中mScrollingOffset表示无新增ViewHolder的滑动范围,即当前最接近底部的child的底边界-RecyclerView内容底边界的值,当滑动距离不大于这个范围时,底部不用新添加一个item。

mAvailable表示滑动距离和无新增范围的差值,若该值大于0则说明底部可能需要补充item。

图例为示:

滑动距离较小
1.手指上划幅度较小⬆️

滑动距离较大
2.手指上划幅度较大⬆️

fill

fill方法在初始布局时也会被调用,通过该方法进行布局填充。可参考《AndroidX RecyclerView总结-测量布局》

回到scrollBy方法,在updateLayoutState中计算了LayoutState中的值后,便传入fill方法进行布局填充:

[LinearLayoutManager#fill]

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    // max offset we should set is mFastScroll + available
    final int start = layoutState.mAvailable;
    // 当滑动时mScrollingOffset会被赋值不等于SCROLLING_OFFSET_NaN
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // TODO ugly bug fix. should not happen
        // mScrollingOffset又被调整为触摸滑动距离
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        // 根据滑动距离进行回收(以往上划为例,回收的是顶部将要滑出视图的ViewHolder)
        recycleByLayoutState(recycler, layoutState);
    }
    // 当RecyclerView已经填满时,通常此时mExtraFillSpace=0,remainingSpace即为mAvailable
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    // 当remainingSpace>0,意味着底部需要补充ViewHolder,且适配器数据集还有item,则不断循环
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        layoutChunkResult.resetInternal();
        if (RecyclerView.VERBOSE_TRACING) {
            TraceCompat.beginSection("LLM LayoutChunk");
        }
        // 获取一个ViewHolder进行布局
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        if (RecyclerView.VERBOSE_TRACING) {
            TraceCompat.endSection();
        }
        if (layoutChunkResult.mFinished) {
            break;
        }
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
        /**
         * Consume the available space if:
         * * layoutChunk did not request to be ignored
         * * OR we are laying out scrap children
         * * OR we are not doing pre-layout
         */
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            // we keep a separate remaining space because mAvailable is important for recycling
            remainingSpace -= layoutChunkResult.mConsumed;
        }

        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            // 回收将滑出视图的ViewHolder
            recycleByLayoutState(recycler, layoutState);
        }
        if (stopOnFocusable && layoutChunkResult.mFocusable) {
            break;
        }
    }
    if (DEBUG) {
        validateChildOrder();
    }
    return start - layoutState.mAvailable;
}
复制代码

该方法即进行ViewHolder的布局填充,其中会判断是否是滚动情况,并且根据LayoutState事先计算的滑动偏移相关的值判断是否需要回收item和补充item。

关于ViewHolder的回收和复用,可参考《AndroidX RecyclerView总结-Recycler》

recycleByLayoutState方法中,会根据滑动距离计算滑动到最后,位置仍在RecyclerView中的最接近边界的child,然后回收该child之上或之下的所有ViewHolder。 图示为例:

上划回收

offsetChildren

回到scrollBy方法中,当完成fill和产生滑动偏移消耗后,会通过mOrientationHelper.offsetChildren并传入滑动消耗量,进行child的整体偏移。

mOrientationHelper.offsetChildren(-scrolled);
复制代码

mOrientationHelper通过OrientationHelper的静态方法createOrientationHelper创建,根据方向创建对应的不同实现。

在offsetChildren方法中回调LayoutManager的offsetChildrenVertical方法,其中又调用RecyclerView的offsetChildrenVertical方法:

[RecyclerView#offsetChildrenVertical]

public void offsetChildrenVertical(@Px int dy) {
    final int childCount = mChildHelper.getChildCount();
    for (int i = 0; i < childCount; i++) {
        // 依次调用child的offsetTopAndBottom进行整体偏移
        mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
    }
}
复制代码

offsetChildren方法最终将滑动偏移量传入RecyclerView的offsetChildrenVertical方法,在其中依次对child进行整体偏移。offsetTopAndBottom方法会根据指定的像素数沿垂直方向整体移动View。

总结

RecyclerView自身的滑动逻辑就是判断方向和滑动距离进行事件拦截和NestedScroll分发,核心逻辑在LayoutManager的具体子类中,LayoutManager子类须重写canScrollHorizontally、canScrollVertically、scrollHorizontallyBy、scrollVerticallyBy完成自身布局的特定逻辑。

在LinearLayoutManager中,会根据滑动方向和距离,对布局两端的ViewHolder进行回收和补充。最后再回调RecyclerView的offsetChildrenVertical方法,对添加的child视图进行整体偏移。