RecyclerView源碼解析(二)LinearLayoutManager繪製篇

語言: CN / TW / HK

「這是我參與11月更文挑戰的第21天,活動詳情查看:2021最後一次更文挑戰

前言

上一篇介紹了RecyclerView的繪製框架,瞭解到RecyclerView及其子view的具體繪製工作是通過具體的LayoutManager中的onLayoutChildren和setMeasuredDimension實現的。

LayoutManager作為RecyclerView的一個組件,它的任務是負責item的佈局繪製,item的回收複用。前者是我們這篇文章要梳理的內容,後者涉及到滑動相關的內容,會在交互那條線上梳理。LayoutManager是一個抽象類,系統提供了繼承它的LinearLayoutManager,GridViewLayoutManager,StaggeredGridLayoutManager三種LayoutManager。首先來看看LinearLayoutManager是怎麼實現繪製的。

實現

onLayoutChildren

``` @Override

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {     // layout algorithm:     // 1) by checking children and other variables, find an anchor coordinate and an anchor     //  item position.     // 2) fill towards start, stacking from bottom     // 3) fill towards end, stacking from top     // 4) scroll to fulfill requirements like stack from bottom.     ...     ensureLayoutState();     mLayoutState.mRecycle = false;     // resolve layout direction     resolveShouldLayoutReverse();     ...     if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION             || mPendingSavedState != null) {         mAnchorInfo.reset();         mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;         // calculate anchor position and coordinate         updateAnchorInfoForLayout(recycler, state, mAnchorInfo);         mAnchorInfo.mValid = true;     }     ...     if (mAnchorInfo.mLayoutFromEnd) {         ...     } else {         // fill towards end         updateLayoutStateToFillEnd(mAnchorInfo);         mLayoutState.mExtraFillSpace = extraForEnd;         fill(recycler, mLayoutState, state, false);         endOffset = mLayoutState.mOffset;         final int lastElement = mLayoutState.mCurrentPosition;         if (mLayoutState.mAvailable > 0) {             extraForStart += mLayoutState.mAvailable;         }         // fill towards start         updateLayoutStateToFillStart(mAnchorInfo);         mLayoutState.mExtraFillSpace = extraForStart;         mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;         fill(recycler, mLayoutState, state, false);         startOffset = mLayoutState.mOffset;

if (mLayoutState.mAvailable > 0) {             extraForEnd = mLayoutState.mAvailable;             // start could not consume all it should. add more items towards end             updateLayoutStateToFillEnd(lastElement, endOffset);             mLayoutState.mExtraFillSpace = extraForEnd;             fill(recycler, mLayoutState, state, false);             endOffset = mLayoutState.mOffset;         }     }     ... } ``` 關於如何佈局,onLayoutChildren在一開始註釋中就給出了實現算法: - 1根據子控件和一些變量,找到錨點位置和座標 - 2從錨點位置開始填充子控件 - 3滑動到滿足要求的位置(本文重點關注前兩步,第三步將在交互部分梳理。)

我對onLayoutChildren的代碼做了部分忽略,使得結構看起來更清晰些。先來看第一步,確定錨點。

確定錨點

所謂錨點,在這裏就是指最先定位的那一個item,錨點相關信息在LinearLayoutManager中用AnchorInfo類表示 static class AnchorInfo {     OrientationHelper mOrientationHelper; //輔助類,用於獲取item view佈局相關的數據     int mPosition; //anchor所對應的item位置     int mCoordinate; //anchor對應的item位置距頂部的距離     boolean mLayoutFromEnd; //是否從底部往上佈局,在本文討論的場景中,值都為false     boolean mValid; //anchor信息是否設置完畢     ... }

LinearLayoutManager中確定錨點的方法是updateAnchorInfoForLayout(),代碼如下,updateAnchorInfoForLayout通過三種判斷來獲取anchor信息,首先從updateAnchorFromPendingData()中獲取anchor信息,如果獲取到了,直接返回;如果沒有獲取到,再從updateAnchorFromChildren()中獲取anchor信息,如果獲取到的話,也是直接返回;如果前兩個方法都沒有獲取到anchor信息,代碼會走到最後兩行,獲取anchor的mCoordinate和mPosition。 private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,         AnchorInfo anchorInfo) {     if (updateAnchorFromPendingData(state, anchorInfo)) {         ...         return;     }     if (updateAnchorFromChildren(recycler, state, anchorInfo)) {         ...         return;     }     anchorInfo.assignCoordinateFromPadding();     anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0; }

updateAnchorFromPendingData和updateAnchorFromChildren都是發生在已經有item view的情況下,前者和滑動相關,判斷依據是mPendingScrollPosition,這個值是由scrollToPosition()設置的,它會將mPendingScrollPosition當作anchor的mPosition,再根據mPosition對應的item  view得到mCoordinate,後續討論到滑動時,會具體説明;後者是通過子控件來獲取anchor的,代碼如下,先通過getFocusedChild()和isViewValidAsAnchor()找滿足錨點要求的焦點子控件,如果不存在的話,再通過findReferenceChildClosestToStart()找離開始位置最近的子控件,找到之後,調用assignFromView設置錨點的相關信息。 private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler,         RecyclerView.State state, AnchorInfo anchorInfo) {     ...     final View focused = getFocusedChild();     if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {         anchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));         return true;     }     ...     View referenceChild = anchorInfo.mLayoutFromEnd             ? findReferenceChildClosestToEnd(recycler, state)             : findReferenceChildClosestToStart(recycler, state);     if (referenceChild != null) {         anchorInfo.assignFromView(referenceChild, getPosition(referenceChild));         ...         return true;     }     return false; }

如果updateAnchorFromPendingData()和updateAnchorFromChildren()都返回false,沒有獲取到anchor信息。在這種情況下,就會執行第三種方式獲取anchor信息,通過assignCoordinateFromPadding(),設置mCoordinate的值,本文討論的場景中,值為mOrientationHelper.getStartAfterPadding(), mPosition的值為0。 anchorInfo.assignCoordinateFromPadding(); anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;

至此,已經獲取到錨點信息,下一步就是填充子控件了。

填充子控件

填充子控件的關鍵代碼fill()如下,可以看到,是通過while循環填充子控件的,結束條件是沒有可用空間了,或者沒有需要填充的子控件了。 int fill(RecyclerView.Recycler recycler, LayoutState layoutState,         RecyclerView.State state, boolean stopOnFocusable) {     ...     int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;     LayoutChunkResult layoutChunkResult = mLayoutChunkResult;     while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {         layoutChunkResult.resetInternal();         ...         layoutChunk(recycler, state, layoutState, layoutChunkResult);         ...         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;         }         ...          }     return start - layoutState.mAvailable; }

fill()中的核心代碼是layoutChunk(),在layoutChunk()中具體實現了子控件的測量和佈局。 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,         LayoutState layoutState, LayoutChunkResult result) {     View view = layoutState.next(recycler);     if (view == null) {         ...         result.mFinished = true;         return;     }     RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();     if (layoutState.mScrapList == null) {         if (mShouldReverseLayout == (layoutState.mLayoutDirection                 == LayoutState.LAYOUT_START)) {             addView(view);         } else {             addView(view, 0);         }     } else {         ...     }     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);     ...     // Consume the available space if the view is not removed OR changed     if (params.isItemRemoved() || params.isItemChanged()) {         result.mIgnoreConsumed = true;     }     result.mFocusable = view.hasFocusable(); } layoutChunk做了下面幾點事:

一,獲取待佈局的子view

具體是通過LayoutState的next()獲取待佈局子view,而next()內部使用了Recycler的getViewForPosition()方法獲取到view,後續分析到Recycler的時候,會詳細分析。獲取到子view後,使用addView()方法添加到父容器RecyclerView中。

二,測量子view

體現在measureChildWithMargins()方法中,measureChildWidthMargins()將padding,margin,Decoration部分去掉,剩餘的作為父容器分配給子view的尺寸,通過measure()方法傳入子view,開始子view的測量。 public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {     final LayoutParams lp = (LayoutParams) child.getLayoutParams();     final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);     widthUsed += insets.left + insets.right;     heightUsed += insets.top + insets.bottom;     final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),             getPaddingLeft() + getPaddingRight()                     + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,             canScrollHorizontally());     final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),             getPaddingTop() + getPaddingBottom()                     + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,             canScrollVertically());     if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {         child.measure(widthSpec, heightSpec);     } } 三,佈局子view

佈局用到的是layoutDecoratedWithMargins()方法,可以看到調用到了layout()方法,在這裏進入到子view的佈局了。 public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,         int bottom) {     final LayoutParams lp = (LayoutParams) child.getLayoutParams();     final Rect insets = lp.mDecorInsets;     child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,             right - insets.right - lp.rightMargin,             bottom - insets.bottom - lp.bottomMargin); }

至此,子view是如何測量佈局的,就梳理完了。再回到fill()方法,來看一下結束while循環的幾個判斷條件:

一,remainingSpace小於或等於0, int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace; while (…) {   ...   remainingSpace -= layoutChunkResult.mConsumed;   ... } remainingSpace的初始值是layoutState.mAvailable + layoutState.mExtraFillSpace,在這裏,mAvailable的值是由updateLayoutStateToFillStart()/updateLayoutStateToFillEnd()決定的,具體代碼體現在onLayoutChildren()中,至於具體由哪個方法來決定,分好幾種情況。首先依據錨點信息中的mLayoutFromEnd,通常我們遇到的情況值都為false,代表從頭開始佈局。在這種情況下,會以錨點開始,先填充錨點對應item後面的子控件,調用updateLayoutStateToFillEnd()設置mLayoutState的各種屬性,其中就包含mAvailable;而後填充錨點前面的子控件,調用updateLayoutStateToFillStart()設置mLayoutState的各種屬性。 ``` private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {     updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate); }

private void updateLayoutStateToFillEnd(int itemPosition, int offset) {     mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;     ... }

private void updateLayoutStateToFillStart(AnchorInfo anchorInfo) {     updateLayoutStateToFillStart(anchorInfo.mPosition, anchorInfo.mCoordinate); }

private void updateLayoutStateToFillStart(int itemPosition, int offset) {     mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding();     ... } ``` mExtraFillSpace和滑動相關,為了滑動的時候更順滑,在滑動的時候,mExtraFillSpace會賦值mOrientationHelper.getTotalSpace(),目的是額外填充一個頁面的子view。其他情況,這個值為0。

進入到while循環體後,remainingSpace每次會減去layoutChunkResult.mConsumed,layoutChunkResult.mConsumed是在layoutChunk()中賦值的,值為mOrientationHelper.getDecoratedMeasurement(view)。

二,layoutState.hasMore(state)為false, boolean hasMore(RecyclerView.State state) {     return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount(); } mCurrentPosition的初始值是錨點對應的mPosition,每次layoutState.next(recycler)獲取view時,會依據填充方向+1/-1。 View next(RecyclerView.Recycler recycler) {     ...     final View view = recycler.getViewForPosition(mCurrentPosition);     mCurrentPosition += mItemDirection;     return view; }

三,調用layoutChunk()之後,如果layoutChunkResult.mFinished為true,意味着已經沒有需要填充的子控件了,這時執行跳出while循環操作。

setMeasuredDimension

從上文可知,setMeasuredDimension是用於處理RecyclerView的長寬尺寸中有wrap_content的情況都,這種情況下,RecyclerView的measuredWidth/measuredHeight由子控件們中的最大測量長/寬決定。 void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {     ...     for (int i = 0; i < count; i++) {         View child = getChildAt(i);         final Rect bounds = mRecyclerView.mTempRect;         getDecoratedBoundsWithMargins(child, bounds);         if (bounds.left < minX) {             minX = bounds.left;         }         if (bounds.right > maxX) {             maxX = bounds.right;         }         if (bounds.top < minY) {             minY = bounds.top;         }         if (bounds.bottom > maxY) {             maxY = bounds.bottom;         }     }     mRecyclerView.mTempRect.set(minX, minY, maxX, maxY);     setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec); }

LinearLayoutManager沒有重寫setMeasuredDimension(),使用的是LayoutManager的setMeasuredDimension()。 public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {     int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight();     int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom();     int width = chooseSize(wSpec, usedWidth, getMinimumWidth());     int height = chooseSize(hSpec, usedHeight, getMinimumHeight());     setMeasuredDimension(width, height);  }

總結

本文梳理了LinearLayoutManager繪製相關的代碼。LayoutManager承載了RecyclerView中的子控件繪製(本文的內容),子控件的回收複用,滑動時的相關邏輯和優化。正因為承載的東西太多,所有的代碼又纏在一起,而我又想盡可能的把每條線都梳理清晰,所以寫的時候很痛苦。篇幅不算太長,但是花費的時間還挺長的。希望能把LLM的繪製部分説清楚吧。

靈活的代價就是複雜度啊~