RecyclerView源碼學習筆記(四)滑動

前幾篇學習了RecyclerView的初始化和繪製過程,主要情景都是在靜止狀態下,沒有手動操作,這篇開始就學習在人爲操作下的代碼流程,先從滑動開始
- RecyclerView源碼學習筆記(一)構造函數和setLayoutManager方法
- RecyclerView源碼學習筆記(二)setAdapter
- RecyclerView源碼學習筆記(三)RecycleView的繪製過程onMeasure,onLayout,onDraw

onInterceptTouchEvent

當一個觸摸事件傳遞到RecyclerView的時候,會調用onInterceptTouchEvent方法,判斷當前viewGroup需不需要攔截這個touch事件,如果要攔截就返回true,否則false,onInterceptTouchEvent的源碼

public boolean onInterceptTouchEvent(MotionEvent e) {
        if (mLayoutFrozen) {
            // When layout is frozen,  RV does not intercept the motion event.
            // A child view e.g. a button may still get the click.
            return false;
        }
        if (dispatchOnItemTouchIntercept(e)) {
            cancelTouch();
            return true;
        }

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

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

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

                // 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:
                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);
                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: {
                cancelTouch();
            }
        }
        return mScrollState == SCROLL_STATE_DRAGGING;
    }

首先判斷當前RecyclerView有沒有被凍住,如果是則直接返回false,也就是說在frozen的情況下,RecyclerView的item還是可以收到touch事件。接下來調用dispatchOnItemTouchIntercept(e)方法,查看其返回值,如果是true,則取消當前touch事件,並返回true,看一下這個dispatchOnItemTouchIntercept(e)方法做了什麼

    private boolean dispatchOnItemTouchIntercept(MotionEvent e) {
        final int action = e.getAction();
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) {
            mActiveOnItemTouchListener = null;
        }

        final int listenerCount = mOnItemTouchListeners.size();
        for (int i = 0; i < listenerCount; i++) {
            final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
            if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) {
                mActiveOnItemTouchListener = listener;
                return true;
            }
        }
        return false;
    }

從代碼可以看出,其實就是去調用了所有註冊的OnItemTouchListeneronInterceptTouchEvent()方法,如果有一個返回true,則直接返回true,這說明OnItemTouchListener的實現者可以攔截RecyclerView的touch事件不再往下傳遞。我們這裏假設沒有註冊OnItemTouchListener,代碼繼續往下走。

直接跳到switch部分,我們假設現在的操作是一次從底部往頂部的拖動動作,那麼事件序列,首先是ACTION_DOWN事件,在ACTION_DOWN這個case裏主要記錄了觸摸點的位置,然後調用startNestedScroll方法,這個方法是嵌套滑動體系裏的,讀者有興趣可以另外研究,這裏不再說明。然後就跳出switch,返回值由mScrollState == SCROLL_STATE_DRAGGING是否成立確定,這裏是false,因爲我們在點擊之前,RecycleView是靜止的,沒有滑動,所以mScroolStateSCROLL_STATE_IDLE

從onInterceptTouchEvent返回後,會調到onTouchEvent方法,這裏再把源碼貼上來

public boolean onTouchEvent(MotionEvent e) {
        if (mLayoutFrozen || mIgnoreMotionEventTillDown) {
            return false;
        }
        if (dispatchOnItemTouch(e)) {
            cancelTouch();
            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 MotionEvent vtev = MotionEvent.obtain(e);
        final int action = e.getActionMasked();
        final int actionIndex = e.getActionIndex();

        if (action == MotionEvent.ACTION_DOWN) {
            mNestedOffsets[0] = mNestedOffsets[1] = 0;
        }
        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

        switch (action) {
            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;

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

            case MotionEvent.ACTION_POINTER_UP: {
                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))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetTouch();
            } break;

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

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

        return true;
    }

這裏會先進到ACTION_DOWN這個case,這個裏面做的事情也是記錄了座標,調用startNestedScroll方法。接下來再回到onInterceptTouchEvent方法,因爲我們的動作是一個拖動的動作,所以在ACTION_DOWN之後會緊跟着ACTION_MOVE事件,在onInterceptTouchEvent的ACTION_MOVE case中主要進行了滑動距離的判斷,如果距離大於touchSlop則認爲是拖動事件,則將mScrollState設置爲SCROLL_STATE_DRAGGING,這時候因爲mScrollState已經設置成SCROLL_STATE_DRAGGING。所以onInterceptTouchEvent將會返回true,這樣touch事件就不會再傳給item

接下來又會調到onTouchEvent方法,並進入case ACTION_MOVE,在case ACTION_MOVE裏最重要的就是如下代碼段

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

首先判斷當前是不是在拖動,是的,然後就是調用scrollByInternal方法,這個方法內部會根據拖動的距離,添加item到RecyclerView,那麼重點就研究這部分吧(省略部分代碼)

 boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0, unconsumedY = 0;
        int consumedX = 0, consumedY = 0;

        consumePendingUpdateOperations();
        if (mAdapter != null) {
            eatRequestLayout();
            onEnterLayoutOrScroll();
            TraceCompat.beginSection(TRACE_SCROLL_TAG);
            fillRemainingScrollValues(mState);
            if (x != 0) {
                consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                unconsumedX = x - consumedX;
            }
            if (y != 0) {
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                unconsumedY = y - consumedY;
            }
      。。。。
    }

做了如下事情

consumePendingUpdateOperations()將沒有處理的item操作處理完,我們這裏假設他沒有,直接return
接下來直接跳到如下代碼,因爲是縱向滑動,所以y不等於0,然後調到mLayout.scrollVerticallyBy

 if (y != 0) {
    consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
    unconsumedY = y - consumedY;
}

mLayout.scrollVerticallyBy在LinearLayoutManager的實現主要就是調用了以下方法

    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || dy == 0) {
            return 0;
        }
        mLayoutState.mRecycle = true;
        ensureLayoutState();
        final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDy = Math.abs(dy);
        updateLayoutState(layoutDirection, absDy, 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 = absDy > consumed ? layoutDirection * consumed : dy;
        mOrientationHelper.offsetChildren(-scrolled);
        if (DEBUG) {
            Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
        }
        mLayoutState.mLastScrollDelta = scrolled;
        return scrolled;
    }

這裏的dy是上次的點擊位置減去這次的點擊位置,如果大於0說明是從底部往頂部滑。所以這裏layoutDirection的值就是LayoutState.LAYOUT_ENDupdateLayoutState()方法是在放item前做一些準備工作,代碼如下

    private void updateLayoutState(int layoutDirection, int requiredSpace,
            boolean canUseExistingSpace, RecyclerView.State state) {
        // If parent provides a hint, don't measure unlimited.
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mExtra = getExtraLayoutSpace(state);
        mLayoutState.mLayoutDirection = layoutDirection;
        int scrollingOffset;
        if (layoutDirection == LayoutState.LAYOUT_END) {
            mLayoutState.mExtra += mOrientationHelper.getEndPadding();
            // get the first child in the direction we are going
            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;
            mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
            // calculate how much we can scroll without adding new children (independent of layout)
            scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                    - mOrientationHelper.getEndAfterPadding();

        } else {
           ...
        }
        mLayoutState.mAvailable = requiredSpace;
        if (canUseExistingSpace) {
            mLayoutState.mAvailable -= scrollingOffset;
        }
        mLayoutState.mScrollingOffset = scrollingOffset;
    }
  • mLayoutState.mInfinite表示當前RecyclerView對加載的item有沒有數量限制
  • mLayoutState.mExtra表示提前加載那些還沒有顯示到屏幕上的item,一般是提前一個屏幕的長度,單位是px
  • 因爲我們這裏layoutDirection等於LayoutState.LAYOUT_END,所以進入第一個if,先是給mLayoutState.mExtra加上底部padding的長度,因爲有時候我們會給RecyclerView設置padding,接着找到屏幕上最靠近底部的那個item。
  • mLayoutState.mItemDirection的意思是指我們在添加itemview的時候,從adapter去取對應數據的時候,是從頭開始取還是倒着取,我們這裏不做特殊處理,所以是順着取,mLayoutState.mItemDirection = LayoutState.ITEM_DIRECTION_TAIL
  • mLayoutState.mCurrentPosition是指當前要添加的item對應到adapter中的位置
  • mLayoutState.mOffset表示開始畫item的座標位置,單位是像素
  • scrollingOffset表示的值是屏幕上最後一個item的下邊緣的座標減去RecycleView的下邊緣座標再減去底部padding的值,這個值表示的意思是,如果我們不添加item,那我們還可以滑動多少距離,如下圖,藍色代表RecyclerView,紅色代表item,綠色代表RecyclerView的底部座標減去buttom padding後的座標,scrollingOffse的值就是第二個item的下邊緣座標減去綠色線的座標的差。
    這裏寫圖片描述
  • mLayoutState.mAvailable代表滑動的距離,如果canUseExistingSpacetrue,則mLayoutState.mAvailable再減去scrollingOffset,說明需要繪製填充的距離更短了,最後將scrollingOffset賦值給mLayoutState.mScrollingOffset

我們再回到scrollBy()方法,在調用完updateLayoutState()方法後會調用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;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            if (VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (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 || mLayoutState.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;
                }
                recycleByLayoutState(recycler, layoutState);
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }
        if (DEBUG) {
            validateChildOrder();
        }
        return start - layoutState.mAvailable;
    }

這邊會判斷layoutState.mAvailable是否小於0,如果是,就設置回原來的值,也就是在減去mScrollingOffset之前的值,這裏是什麼目的還不清楚,難道是因爲layoutState.mAvailable不可以爲負數?接下來會調用recycleByLayoutState()方法,將在接下來被滑出屏幕的item回收,就是將item view全部remove,並回收到cache或者pool中。

然後調用while循環添加item,循環繼續的條件是item還沒有加載完或者RecyclerView可以無限加載且當前還沒有加載滿需要填充的空間。具體實施item加載的是layoutChunk()方法,源碼如下:

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        measureChildWithMargins(view, 0, 0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                right = getWidth() - getPaddingRight();
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = layoutState.mOffset - result.mConsumed;
            } else {
                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;
            }
        } else {
            top = getPaddingTop();
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                right = layoutState.mOffset;
                left = layoutState.mOffset - result.mConsumed;
            } else {
                left = layoutState.mOffset;
                right = layoutState.mOffset + result.mConsumed;
            }
        }
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        if (DEBUG) {
            Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
        }
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.hasFocusable();
    }

首先會調用layoutState.next(recycler)方法獲取到一個item view,這個next方法源碼如下:

  View next(RecyclerView.Recycler recycler) {
      if (mScrapList != null) {
          return nextViewFromScrapList();
      }
      final View view = recycler.getViewForPosition(mCurrentPosition);
      mCurrentPosition += mItemDirection;
      return view;
  }

先會判斷mScrapList是不是null,這個對象只有在layoutForPredictiveAnimations方法中會被賦值,而layoutForPredictiveAnimations內部會判斷要不要直接返回,我們這裏現在是沒有PredictiveAnimation所以layoutForPredictiveAnimations方法會直接返回,也就是mScrapList值等於null,這樣的話就是會通過recycler.getViewForPosition方法獲取itemview,getViewForPosition方法最後會調用到tryGetViewHolderForPositionByDeadline方法,此方法的第三個參數是一個long型的,表示需要在多長時間內完成item的創建和bind工作,如果是FOREVER_NS,表示沒有時間限制,否則,就必須在規定時間內完成,否則就有可能返回null或者一個沒有bind過的itemview。這裏我們傳進去的是FOREVER_NS,所以沒有時間限制。tryGetViewHolderForPositionByDeadline方法內部會依次從mChangedScrap,mAttachedScrap,mCachedViews,mViewCacheExtension,RecycledViewPool去獲取itemview,如果都沒有獲取到,則會調用adapter的createViewHolder方法來創建,創建view的代碼如下

 if (holder == null) {
      long start = getNanoTime();
      if (deadlineNs != FOREVER_NS
              && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
          // abort - we have a deadline we can't meet
          return null;
      }
      holder = mAdapter.createViewHolder(RecyclerView.this, type);
      if (ALLOW_THREAD_GAP_WORK) {
          // only bother finding nested RV if prefetching
          RecyclerView innerView = findNestedRecyclerView(holder.itemView);
          if (innerView != null) {
              holder.mNestedRecyclerView = new WeakReference<>(innerView);
          }
      }

      long end = getNanoTime();
      mRecyclerPool.factorInCreateTime(type, end - start);
      if (DEBUG) {
          Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
      }
  }

可以看到最開始會判斷deadlineNs是不是等於FOREVER_NS,如果不是就會先預測在規定時間內是不是可以完成創建,如果不能則直接返回null了,這裏的預測時間是根據前面創建item的平均時間來確定的。在createViewHolder方法內部會調用Adapter子類實現的onCreateViewHolder方法

public final VH createViewHolder(ViewGroup parent, int viewType) {
    TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
    final VH holder = onCreateViewHolder(parent, viewType);
    holder.mItemViewType = viewType;
    TraceCompat.endSection();
    return holder;
}

然後接下來有如下代碼:

   boolean bound = false;
  if (mState.isPreLayout() && holder.isBound()) {
      // do not update unless we absolutely have to.
      holder.mPreLayoutPosition = position;
  } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
      if (DEBUG && holder.isRemoved()) {
          throw new IllegalStateException("Removed holder should be bound and it should"
                  + " come here only in pre-layout. Holder: " + holder
                  + exceptionLabel());
      }
      final int offsetPosition = mAdapterHelper.findPositionOffset(position);
      bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
  }

因爲我們這裏沒有prelayout,所以會走elseif,其中會判斷viewholder是不是需要重新bind,如果是就調用tryBindViewHolderByDeadline方法,需要bind的條件有三個,`.沒有bind過,2.需要更新,3.當前holder已經不可用

private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition,
       int position, long deadlineNs) {
   holder.mOwnerRecyclerView = RecyclerView.this;
   final int viewType = holder.getItemViewType();
   long startBindNs = getNanoTime();
   if (deadlineNs != FOREVER_NS
           && !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) {
       // abort - we have a deadline we can't meet
       return false;
   }
   mAdapter.bindViewHolder(holder, offsetPosition);
   long endBindNs = getNanoTime();
   mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
   attachAccessibilityDelegateOnBind(holder);
   if (mState.isPreLayout()) {
       holder.mPreLayoutPosition = position;
   }
   return true;
}

方法內部也會先判斷能不能在要求的時間內完成bind,如果不能就返回false,我能這裏是FOREVER_NS,所以會調用
mAdapter.bindViewHolder(holder,offsetPosition),在bindViewHolder()內部會調用Adapter子類的實現的onBindViewHolder()方法

 public final void bindViewHolder(VH holder, int position) {
            holder.mPosition = position;
            if (hasStableIds()) {
                holder.mItemId = getItemId(position);
            }
            holder.setFlags(ViewHolder.FLAG_BOUND,
                    ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
                            | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
            TraceCompat.beginSection(TRACE_BIND_VIEW_TAG);
            onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
            holder.clearPayload();
            final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
            if (layoutParams instanceof RecyclerView.LayoutParams) {
                ((LayoutParams) layoutParams).mInsetsDirty = true;
            }
            TraceCompat.endSection();
        }

再回到tryGetViewHolderForPositionByDeadline()方法,最後該方法會給viewholder設置一個LayoutParams,如果viewholder當前沒有LayoutParams,就會調用LayoutManager的generateDefaultLayoutParams()方法來生成一個默認的LayoutParams,此方法需要子類實現。

再回到layoutChunk()方法,再貼一下代碼

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        measureChildWithMargins(view, 0, 0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                right = getWidth() - getPaddingRight();
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = layoutState.mOffset - result.mConsumed;
            } else {
                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;
            }
        } else {
            top = getPaddingTop();
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                right = layoutState.mOffset;
                left = layoutState.mOffset - result.mConsumed;
            } else {
                left = layoutState.mOffset;
                right = layoutState.mOffset + result.mConsumed;
            }
        }
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        if (DEBUG) {
            Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
        }
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.hasFocusable();
    }

此時我們已經從next()方法獲取到了一個view,此view不等於null,然後按照我們現在的場景,會進到addView(view)方法。此方法最終會調用到LayoutManager的addViewInt方法,此方法源碼如下

private void addViewInt(View child, int index, boolean disappearing) {
    final ViewHolder holder = getChildViewHolderInt(child);
    if (disappearing || holder.isRemoved()) {
        // these views will be hidden at the end of the layout pass.
        mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder);
    } else {
        // This may look like unnecessary but may happen if layout manager supports
        // predictive layouts and adapter removed then re-added the same item.
        // In this case, added version will be visible in the post layout (because add is
        // deferred) but RV will still bind it to the same View.
        // So if a View re-appears in post layout pass, remove it from disappearing list.
        mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder);
    }
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (holder.wasReturnedFromScrap() || holder.isScrap()) {
        if (holder.isScrap()) {
            holder.unScrap();
        } else {
            holder.clearReturnedFromScrapFlag();
        }
        mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
        if (DISPATCH_TEMP_DETACH) {
            ViewCompat.dispatchFinishTemporaryDetach(child);
        }
    } else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child
        // ensure in correct position
        int currentIndex = mChildHelper.indexOfChild(child);
        if (index == -1) {
            index = mChildHelper.getChildCount();
        }
        if (currentIndex == -1) {
            throw new IllegalStateException("Added View has RecyclerView as parent but"
                    + " view is not a real child. Unfiltered index:"
                    + mRecyclerView.indexOfChild(child) + mRecyclerView.exceptionLabel());
        }
        if (currentIndex != index) {
            mRecyclerView.mLayout.moveView(currentIndex, index);
        }
    } else {
        mChildHelper.addView(child, index, false);
        lp.mInsetsDirty = true;
        if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
            mSmoothScroller.onChildAttachedToWindow(child);
        }
    }
    if (lp.mPendingInvalidate) {
        if (DEBUG) {
            Log.d(TAG, "consuming pending invalidate on child " + lp.mViewHolder);
        }
        holder.itemView.invalidate();
        lp.mPendingInvalidate = false;
    }
}

做了如下事情:

  1. 判斷需要添加的view的viewholder是需要消失的或者被移除的,兩者滿足一個就將此viewholder添加到ViewInfoState的mLayoutHolderMap中,用於之後的動畫。
  2. 判斷viewholder是不是scrap,如果是則清除相關標誌,並將view重新attach到RecyclerView
  3. 如果當前view的parent是當前RecyclerView,也就是說沒有dettach,則將此view移動到指定的index,這個過程是dettach到attach的過程
  4. 如果不滿足2,3兩種情況,則這個view是新建的view,則將此view add到RecyclerView,如果此view的位置是mSmoothScroller滑動的目標位置,則將targetview設爲此view
  5. 最後如果此view需要刷新,也就是有重新bind,則調用invalidate()

layoutChunk()方法在調用完addview()方法後,會調用measureChildWithMargins()方法,此方法會用標準測量方法測量被添加的view,並將RecyclerView的滑動方向和itemdecoration考慮進去,這裏不再分析源碼。接下來把此item view消耗掉的空間數據保存到result的mConsumed變量中。接下來會根據之前的測量結果,決定view的擺放位置,也就是調用layout()方法,此過程中會將itemdecoration佔用的空間也考慮進去。最後如果此view是被removed或者changed,則會忽略掉它所消耗的空間,因爲remove的item不會消耗空間,反倒會增加空間,change的item所佔空間不變,不會影響後續空間消耗的計算,這裏的空間指的是根據用戶滑動距離,RecyclerView需要填充的空間,也就是mLayoutState.mAvailable。到這裏,·layoutChunk()·就結束了,接下來就又要回到fill()方法。再貼一遍code

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;
       if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
           // TODO ugly bug fix. should not happen
           if (layoutState.mAvailable < 0) {
               layoutState.mScrollingOffset += layoutState.mAvailable;
           }
           recycleByLayoutState(recycler, layoutState);
       }
       int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
       LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
       while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
           layoutChunkResult.resetInternal();
           if (VERBOSE_TRACING) {
               TraceCompat.beginSection("LLM LayoutChunk");
           }
           layoutChunk(recycler, state, layoutState, layoutChunkResult);
           if (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 || mLayoutState.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;
               }
               recycleByLayoutState(recycler, layoutState);
           }
           if (stopOnFocusable && layoutChunkResult.mFocusable) {
               break;
           }
       }
       if (DEBUG) {
           validateChildOrder();
       }
       return start - layoutState.mAvailable;
   }

在調用完layoutChunk()方法之後,fill()繼續做了如下事情:

  1. 判斷此次填充動作是不是結束,也就是看layoutChunkResult.mFinished這個變量,如果結束則break,我們這裏假設它沒有結束
  2. 接下來會根據item的填充方向和item消耗的空間更新layoutState.mOffset
  3. 然後更新layoutState.mAvailable,更新此變量的條件是:1.所添加的view不需要忽略,2.正在佈局scrap的view,3.沒有進行pre-layout.
  4. 更新layoutState.mScrollingOffset,並調用recycleByLayoutState()方法回收暫時會被移除屏幕的item
  5. 返回已經消耗的空間,單位是px

到這裏fill()方法就結束了,再回到scrollBy()方法,判斷返回的consumer是否小於0,小於0說明已經沒有item可以再添加,直接返回0(其實這部分不明白,爲什麼consumer小於0就是沒有item需要再添加),否則就根據scroll的像素個數來移動child views的位置,實現滑動效果,然後將scroll的距離保存在mLayoutState.mLastScrollDelta,最後返回scroll。
scroll結束後再回到scrollByInternal()方法,後續的代碼如下:

    if (y != 0) {
         consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
         unconsumedY = y - consumedY;
     }
     ...
     repositionShadowingViews();
     ...
 }
 if (!mItemDecorations.isEmpty()) {
     invalidate();
 }

 if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
         TYPE_TOUCH)) {
     // Update the last touch co-ords, taking any scroll offset into account
     mLastTouchX -= mScrollOffset[0];
     mLastTouchY -= mScrollOffset[1];
     if (ev != null) {
         ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
     }
     mNestedOffsets[0] += mScrollOffset[0];
     mNestedOffsets[1] += mScrollOffset[1];
 } else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
     if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
         pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
     }
     considerReleasingGlowsOnScroll(x, y);
 }
 if (consumedX != 0 || consumedY != 0) {
     dispatchOnScrolled(consumedX, consumedY);
 }
 if (!awakenScrollBars()) {
     invalidate();
 }
 return consumedX != 0 || consumedY != 0;

接下來會調用repositionShadowingViews方法,此方法裏做的事情和itemChange的動畫有關,我們這裏先不涉及動畫。然後判斷itemdecoration是否有指定,如果有的話就重繪RecyclerView。後面就是嵌套滑動和邊緣陰影的動作,然後再調用各個監聽scroll的listener,不再深究,最後返回是否有滑動被消耗掉。到這裏scrollByInternal()也結束了,終於又回到onTouchEvent了,在onTouchEvent會判斷滑動是否有被消耗,如果有的話,就會調用getParent().requestDisallowInterceptTouchEvent(true)方法來阻止RecyclerView的父控件來攔截touch事件,說明RecyclerView需要繼續處理後續事件。case Move後面的代碼就不講了,比較簡單,都是一些資源回收,狀態重置的動作。

最後就是調用onTouchEvent方法中Case Up的代碼,這裏主要是做了一個fling()的動作也就是快速滑動,這部分代碼這裏不講了,等後面有機會再看,其實做的事情和case move差不多。

好了,滑動部分就到這裏,我也是在學習源碼,有些地方如果有錯誤還請各位讀者提出

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