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視圖進行整體偏移。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章