欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

AndroidX RecyclerView总结-滑动处理

程序员文章站 2022-06-08 12:23:50
...

概述

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。

图例为示:

AndroidX RecyclerView总结-滑动处理
1.手指上划幅度较小⬆️

AndroidX RecyclerView总结-滑动处理
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。
图示为例:
AndroidX RecyclerView总结-滑动处理

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视图进行整体偏移。