步步前行(RecyclerView拆分解析)-LayoutManager之Layout篇

不喜勿噴,有錯請留言(以下源碼均來自recyclerView-27.1.1版本)

對於RecyclerView這個ViewGroup而言,我們已經初步瞭解了ViewGroup的各個基本方法,下面我們進入layoutManager這個必需的控件中,藉助LinearLayoutManager這個實例重點分析LayoutManger,爲避免篇幅過長,而且layout與scroll相對獨立,故將拆成兩篇,一篇layout篇和一篇scroll篇

LayoutManager組成

LayoutManager位於RecyclerView中,是一個靜態抽象類方法,在其中有兩個靜態內部類,分別是:

在這裏插入圖片描述

在這裏插入圖片描述

由Properties我們一眼可瞭解對於LayoutManger而言,其提供的自定義樣式有對應的四種,而LayoutPrefetchRegistry而言,其作用是我們layoutManager的預取策略制定,對於它們倆我們不着重分析,我麼進入LinearLayoutManager中進行分析

public class LinearLayoutManager extends RecyclerView.LayoutManager implements 
ItemTouchHelper.ViewDropHandler,RecyclerView.SmoothScroller.ScrollVectorProvider

有經驗的童鞋發現的在繼承RecyclerView.LayoutManager自定義LayoutManager時候,必須要實現一個叫generateDefaultLayoutParams的方法

public class MyLinearLayoutManager extends RecyclerView.LayoutManager {
    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }
}
由於LayoutManager抽象了generateDefaultLayoutParams方法,子類必須實現該方法
在LinearLayoutManager中其提供還是RecyclerView的LayoutParams,我們照樣返回,
可能有人對generateDefaultLayoutParams有些疑問感覺沒啥卵用,這裏只做簡單介紹,有興趣的童鞋可自行深入瞭解,對於generateDefaultLayoutParams的意義是如果childView的佈局參數爲null就是調用這個方法生成一個默認的佈局參數

從上一文的ViewGroup篇,我們知道RecyclerView通過LayoutManager的onLayoutChildren,對子view進行擺放,在我們分析onLayoutChildren之前,LinearLayoutManager中有幾個狀態記錄類

LayoutState: 一個佈局狀態幫助類

SavedState:保存一些滾動或者掛起的信息

AnchorInfo:重新佈局時保留定位信息

那接着我們通過分析onLayoutChildren這個方法來完善我們自己的LayoutManager

在我們分析前我們想象下,如果是我們自己實現LayoutManager的時候該怎麼實現

  • onMeasure測量子view,測量自身
  • 獲取padding等獲取有效擺放範圍
  • 滿足一行時或擺不下,自動換行

注意對於LayoutManager而言不僅僅這點功能,其還有reverse和橫豎向變化,那我們考慮橫豎向變化,我們的LayoutManager又該如何實現呢,我們先簡單介紹下android的LinearLayoutManager是幾個概念:

LayoutManager應對layout佈局變化時,引入了一個anchor這個佈局錨點信息的概念(也就是我們之前說的AnchorInfo)和一個layoutfromEnd佈局方向的概念,用於根據佈局的錨點信息,按照layoutfromEnd的佈局方向去填充itemview

接下來我們看看LinearLayoutManager是onLayoutChildren實現的

onLayoutChildren:官方的對layout algorithm描述如下:
  • 找到定位點座標和定位點項目位置

在這裏插入圖片描述

首先在ensureLayoutState中創建一個LayoutState用於後續記錄使用

然後resolveShouldLayoutReverse中設置佈局方向

若錨點信息已過期或者滾動位置不是初始位置,或者預存儲狀態不爲null,則重置錨點

注意此處 mShouldReverseLayout ^ mStackFromEnd mStackFromEnd若未有人爲操作一直爲false,此處位運算,mLayoutFromEnd根據mShouldReverseLayout值走向,即VERTICAL方向爲false,HORIZONAL方向爲true

最後計算填充的錨點位置和偏移量,保存繪製錨點信息即(updateAnchorInfoForLayout)

對於updateAnchorInfoForLayout內

private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
            AnchorInfo anchorInfo) {
 //存在掛起的滾動位置或保存的狀態,則從中更新定位信息並返回true 否則返回false
        if (updateAnchorFromPendingData(state, anchorInfo)) {
            if (DEBUG) {
                Log.d(TAG, "updated anchor info from pending information");
            }
            return;
        }
//定位從存在的view中找出一個作爲錨點的view,這個view要麼是最靠近開始的位置要麼在最後,如果一個子view有焦點,則優先給它
        if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
            if (DEBUG) {
                Log.d(TAG, "updated anchor info from existing children");
            }
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "deciding anchor info for fresh state");
        }
  //根據RecyclerView的填充指定定位的座標
        anchorInfo.assignCoordinateFromPadding();
        anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
    }
注意最後一句,anchorInfo它的錨點位置 如果mStackFromEnd爲true時,其爲條目總個數,若爲false,則爲-1
  • 錨點維護(updateAnchorFromPendingData和updateAnchorFromChildren是對滾動view的選取即是對錨點的處理)

    可能有朋友還是對錨點不太能理解,我就從使用方來解釋錨點,首先大家都在使用LinearlayoutManager的時候使用過setStackFromEnd這個方法來改變stackFromEn這個值,在updateAnchorInfoForLayout的具體呈現就是

    在這裏插入圖片描述

    這兩種情況的填充是相反的,即一個是自上而下,一個是自下而上,子控件在y軸上起始繪製偏移量,就會不同。

    對於updateLayoutStateToFillEnd和updateLayoutStateToFillStart它們功能很相似分別如下

    updateStateToFillEnd:

    private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
            mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
            mLayoutState.mItemDirection = mShouldReverseLayout ? MyLayoutState.ITEM_DIRECTION_HEAD :
                    MyLayoutState.ITEM_DIRECTION_TAIL;
            mLayoutState.mCurrentPosition = itemPosition;
            mLayoutState.mLayoutDirection = MyLayoutState.LAYOUT_END;
            mLayoutState.mOffset = offset;
            mLayoutState.mScrollingOffset = MyLayoutState.SCROLLING_OFFSET_NaN;
        }
    
    注意這裏第一步是通過OrientationHelper的getEndAfterPadding獲取到佈局的結束爲止減去偏移量
    layout的方向時LAOYOUT_END也就是說自上而下的layout
    

    而updateStateToFillStart

    private void updateLayoutStateToFillStart(int itemPosition, int offset) {
            mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding();
            mLayoutState.mCurrentPosition = itemPosition;
            mLayoutState.mItemDirection = mShouldReverseLayout ? MyLayoutState.ITEM_DIRECTION_TAIL :
                    MyLayoutState.ITEM_DIRECTION_HEAD;
            mLayoutState.mLayoutDirection = MyLayoutState.LAYOUT_START;
            mLayoutState.mOffset = offset;
            mLayoutState.mScrollingOffset = MyLayoutState.SCROLLING_OFFSET_NaN;
        }
    而這裏的第一步是偏移量減去通過OrientationHelper的getStartAfterPadding獲取佈局的起始位置
    layout的方向時LAYOUT_START也就是說自下而上的layout
    

    從這我們已經能理解錨點的存在了吧

  • 接下來是根據不同的layout方向進行layout操作了,當mStackFromEnd爲true時

    在這裏插入圖片描述

  • 當mStackFromEnd爲false時

    在這裏插入圖片描述

首先我們先從錨點信息出對我們layout的佈局範圍以及方向做確定,之前已經介紹過的updateLayoutStateTo的方法,可能有人對此處有疑問,爲什麼此處即有toStart也有toEnd兩種操作,因爲在我們真正使用過程中,我們的layout的方向大致是這樣的(這裏借用網上的例子來說明下)

在這裏插入圖片描述

其實我們後面分析其scroll的時候就會解釋到,當我們RecyclerView處理時邊滾動邊添加,之前添加過的也不是恆久不變的會出現回收等,需要重新創建layout,那麼就會出現兩個方向,所以不僅會有toStart也會有toEnd兩種操作

我們進入layout最重要的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;
            }
          //此處是用於標記Recyclerview進行 緩存回收view 操作
            recycleByLayoutState(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        ...省略LayoutChunk的循環填充操作 後面單獨拎出來分析
          //填充完修改起始位置
        return start - layoutState.mAvailable;
    }

下面是fill方法的核心方法了也就是上面省略的代碼部分

LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
 //先重置layoutChunkResult狀態
            layoutChunkResult.resetInternal();
  //填充空間
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            //...省略部分剩餘空間等代碼
        }

我們關注填充空間的layoutChunk方法

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
  //先嚐試獲取下一個view(mScrapList和mRecyclerPool兩個緩存中優先獲取),獲取不到就創建一個
    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();
  //獲取layoutParams 根據情況addView
    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);
        }
    }
  //重新測量包含margin值
    measureChildWithMargins(view, 0, 0);
    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
  //計算子view的left,top,right,bottom座標
    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;
        }
    }
    // 確定位置後就進行layout
    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();
}

注意,layoutDecoratedWithMargins這裏其實是在RecyclerView中實現的

在這裏插入圖片描述

它將計算出來的左上右下去具體的layout操作

總的來說layoutChunk就是對我們的提供的ItemView進行創建,填充,測量和佈局。
- 先通過LayoutState的next方法從RecylerView的一二級緩存中取itemView沒有得話就創建一個(也就是我們的onCreateView)
- 然後將這個view add進我們的viewGroup中
- 然後對view進行測量跟擺放

至此我們的layout篇就到這結束了,可能有人問不是還有layoutForPredictiveAnimations的方法沒有分析麼,其實layoutForPredictiveAnimations雖然屬於layout部分,但是其和滾動的時機密切相關,所以我把這塊放在scroll篇去分析

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