前言:做人如果沒有夢想,那和鹹魚有什麼區別。 ——《少林足球》
一、概述
上一篇文章對RecyclerView中實現瞭如何高度自定義點擊事件、萬能ViewHolder、萬能適配器的封裝和使用。最開始就提到,RecyclerView支持各種各樣的佈局效果,其核心關鍵在於RecyclerView.LayoutManager中,使用時我們是需要setLayoutManager()
設置佈局管理器的。RecyclerView已經將一部分功能抽離出來,在佈局管理器中另外處理,也方便開發者自行拓展。LayoutManager就是負責RecyclerView的測量和佈局以及itemView的回收和複用。今天這裏主要結合LinearLayoutManager來分析RecyclerView的繪製流程。
RecyclerView提供了三中佈局管理器:
- LinearLayoutManager 以列表的方式展示item,有水平方向RecyclerView.HORIZONTAL和垂直方向RecyclerView.VERTICAL;
- GridLayoutManager 以網格的方式展示item,有水平方向和垂直方向;
- StaggeredGridLayoutManager 以瀑布流的方式展示item,有水平方向和垂直方向。
這裏就不一一分析了,前面的文章已經做了詳細的介紹,不瞭解的同學可以回頭看一下。這裏以LinearLayoutManager爲例來分析RecyclerView的繪製流程。
二、RecyclerView的繪製三個步驟
RecyclerView設置佈局管理器,這一步是必要的,用什麼樣的LayoutManager來繪製RecyclerView,不然RecyclerView也不知道怎麼繪製。
recyclerView.setLayoutManager(manager);
從設置佈局管理器方法入手,setLayoutManager()
設置佈局管理器給RecyclerView使用:
public void setLayoutManager(@Nullable LayoutManager layout) {
if (layout == mLayout) {//和之前的管理器一樣則直接return
return;
}
stopScroll();//停止滾動
if (mLayout != null) {//每次設置layoutManager都重新設置recyclerView的初始參數,動畫回收view等
if (mItemAnimator != null) {
mItemAnimator.endAnimations();//結束動畫
}
mLayout.removeAndRecycleAllViews(mRecycler);//移除回收所有itemView
mLayout.removeAndRecycleScrapInt(mRecycler);//移除回收所有已經廢棄的itemView
mRecycler.clear();//清除所有緩存
mLayout.setRecyclerView(null);//重置RecyclerView
mLayout = null;
} else {
mRecycler.clear();
}
·······
mLayout.setRecyclerView(this);//LayoutManager與RecyclerView關聯
mRecycler.updateViewCacheSize();//更新緩存大小
requestLayout();//請求重繪
}
這裏首先做了重置回收工作,然後LayoutManager與RecyclerView關聯起來,最後請求重繪。這裏調用了請求重繪requestLayout()
方法,那麼說明每次設置layoutManager都會執行View樹的繪製,那麼就會重走RecyclerView的onMeasure()
、onLayout()
、onDraw()
繪製三部曲。
public void requestLayout() {
if (mRecyclerView != null) {
mRecyclerView.requestLayout();//請求重繪
}
}
2.1 onMeasure()
我們來看看RecyclerView的onMeasure()
方法:
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {//如果mLayout爲空則採用默認測量,然後結束
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.mAutoMeasure) {//如果爲自動測量,默認爲true
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);//測量RecyclerView的寬高
//當前RecyclerView的寬高是否爲精確值
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {//如果RecyclerView的寬高爲精確值或者mAdapter爲空,則結束
return;
}
//RecyclerView的寬高爲wrap_content時,即measureSpecModeIsExactly = false則進行測量
//因爲RecyclerView的寬高爲wrap_content時,需要先測量itemView的寬高才能知道RecyclerView的寬高
if (mState.mLayoutStep == State.STEP_START) {//還沒測量過
dispatchLayoutStep1();//1.適配器更新、動畫運行、保存當前視圖的信息、運行預測佈局
}
dispatchLayoutStep2();//2.最終實際的佈局視圖,如果有必要會多次運行
//根據itemView得到RecyclerView的寬高
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
}
onMeasure()
主要是RecyclerView寬高測量工作,主要有兩種情況:
- (1)當RecyclerView的寬高爲match_parent或者精確值時,即
measureSpecModeIsExactly = true
,此時只需要測量自身的寬高就知道RecyclerView的寬高,測量方法結束; - (2)當RecyclerView的寬高爲wrap_content時,即
measureSpecModeIsExactly = false
,會往下執行dispatchLayoutStep1()
和dispatchLayoutStep2()
,就是遍歷測量ItemView的大小從而確定RecyclerView的寬高,這種情況真正的測量操作都是在dispatchLayoutStep2()
中完成。
dispatchLayoutStep1()
和dispatchLayoutStep2()
下面會講解到。
2.2 onLayout()
在onLayout()
方法中, 直接調用dispatchLayout()
方法佈局:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout(); //直接調用dispatchLayout()方法佈局
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
dispatchLayout()
是layoutChildren()
的包裝器,它處理由佈局引起的動態變化:
void dispatchLayout() {
······
mState.mIsMeasuring = false;//設置RecyclerView佈局完成狀態,前面已經設置預佈局完成了。
if (mState.mLayoutStep == State.STEP_START) {//如果沒在OnMeasure階段提前測量子ItemView
dispatchLayoutStep1();//佈局第一步:適配器更新、動畫運行、保存當前視圖的信息、運行預測佈局
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {//前兩步完成測量,但是因爲大小改變不得不再次運行下面的代碼
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();//佈局第二步:最終實際的佈局視圖,如果有必要會多次運行
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();//佈局第三步:最後一步的佈局,保存視圖動畫、觸發動畫和不必要的清理。
}
可以看到dispatchLayout()
和onMeasure()
階段中一樣選擇性地進行測量佈局的三個步驟:
- 1、如果沒在onMeasure階段提前測量子ItemView,即RecyclerView寬高爲
match_parent
或者精確值時,調用dispatchLayoutStep1()
和dispatchLayoutStep2()
測量itemView寬高; - 2、如果在onMeasure階段提前測量子ItemView,但是子視圖發生了改變或者期望寬高和實際寬高不一致,則會調用
dispatchLayoutStep2()
重新測量; - 3、最後都會執行
dispatchLayoutStep3()
方法。
(1)我們來看看dispatchLayoutStep1、2、3分發佈局的三個步驟:dispatchLayoutStep1()
主要是進行預佈局,適配器更新、動畫運行、保存當前視圖的信息等工作;
private void dispatchLayoutStep1() {
mState.assertLayoutStep(State.STEP_START);
fillRemainingScrollValues(mState);
mState.mIsMeasuring = false;
startInterceptRequestLayout();//攔截佈局請求
mViewInfoStore.clear();//itemView信息清除
onEnterLayoutOrScroll();
//測量和分派佈局時,更新適配器和計算那種類型要運行的動畫
processAdapterUpdatesAndSetAnimationFlags();
saveFocusInfo();//保存焦點信息
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
mItemsAddedOrRemoved = mItemsChanged = false;
mState.mInPreLayout = mState.mRunPredictiveAnimations;
mState.mItemCount = mAdapter.getItemCount();
findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);//找到可繪製itemView最小最大position
if (mState.mRunSimpleAnimations) {
//獲得界面上可以顯示的個數
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
//動畫信息
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
//保存holder和動畫信息到預佈局中
mViewInfoStore.addToPreLayout(holder, animationInfo);
}
}
//運行與佈局,將會使用舊的item的position,佈局管理器佈局所有
if (mState.mRunPredictiveAnimations) {
//保存舊的管理器可以運行的邏輯
saveOldPositions();
final boolean didStructureChange = mState.mStructureChanged;
mState.mStructureChanged = false;
//佈局itemView
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = didStructureChange;
}
stopInterceptRequestLayout(false);//回覆繪製鎖定
mState.mLayoutStep = State.STEP_LAYOUT;
}
(2)dispatchLayoutStep2()
表示對最終狀態的視圖進行實際佈局:
private void dispatchLayoutStep2() {
startInterceptRequestLayout();//攔截請求佈局
onEnterLayoutOrScroll();
//設置佈局狀態和動畫狀態
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
//預佈局完成,開始佈局itemView
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
······
stopInterceptRequestLayout(false);//停止攔截佈局請求
}
(3)dispatchLayoutStep3()
是佈局的最後一步,保存view的動畫信息,執行動畫,和一些必要的清理工作:
private void dispatchLayoutStep3() {
mState.assertLayoutStep(State.STEP_ANIMATIONS);
startInterceptRequestLayout();//開始攔截佈局請求
mState.mLayoutStep = State.STEP_START;//佈局開始狀態
if (mState.mRunSimpleAnimations) {
//步驟3:找出事情現在的位置,並處理更改動畫。
//反向遍歷列表,因爲我們可能會在循環中調用animateChange,這可能會刪除目標視圖持有者。
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
//運行一個變更動畫。如果一個項目被更改,但是更新後的版本正在消失,則會產生衝突的情況。
//由於標記爲正在消失的視圖可能會超出界限,所以我們運行一個change動畫。兩個視圖都將在動畫完成後自動清除。
//另一方面,如果是相同的視圖持有者實例,我們將運行一個正在消失的動畫,因爲我們不會重新綁定更新的VH,除非它是由佈局管理器強制執行的。
//運行消失動畫而不是改變
mViewInfoStore.addToPostLayout(holder, animationInfo);
final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(oldChangeViewHolder);
//我們添加和刪除,這樣任何的佈置信息都是合併的
mViewInfoStore.addToPostLayout(holder, animationInfo);
ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
//處理視圖信息列表和觸發動畫
mViewInfoStore.process(mViewInfoProcessCallback);
}
//回收廢棄的視圖
mLayout.removeAndRecycleScrapInt(mRecycler);
//重置狀態
mState.mPreviousLayoutItemCount = mState.mItemCount;
mDataSetHasChangedAfterLayout = false;
//清除mChangedScrap中的數據
mRecycler.mChangedScrap.clear();
mRecycler.updateViewCacheSize();//更新緩存大小
mLayout.onLayoutCompleted(mState);//佈局完成狀態
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);//停止攔截佈局請求
mViewInfoStore.clear();//itemView信息清除
recoverFocusFromState();//回覆焦點
resetFocusInfo();//重置焦點信息
}
總結一下這分發佈局的三個步驟:
- dispatchLayoutStep1() 表示進行預佈局,適配器更新、動畫運行、保存當前視圖的信息等工作;
- dispatchLayoutStep2() 表示對最終狀態的視圖進行實際佈局,有必要時會多次執行;
- dispatchLayoutStep3() 表示佈局最後一步,保存和觸發有關動畫的信息,相關清理等工作。
2.3 onDraw()
來到最後一步的繪製onDraw()
方法中,如果不需要一些特殊的效果,在TextView、ImageView控件中已經繪製完了。
@Override
public void onDraw(Canvas c) {
super.onDraw(c);//所有itemView先繪製
//分別繪製ItemDecoration
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
2.4 RecyclerView的繪製三個步驟總結:
1、RecyclerView的itemView可能會被測量多次,如果RecyclerView的寬高是固定值或者match_parent,那麼在onMeasure()
階段是不會提前測量ItemView佈局,如果RecyclerView的寬高是wrap_content,由於還沒有知道RecyclerView的實際寬高,那麼會提前在onMeasure()
階段遍歷測量itemView佈局確定內容顯示區域的寬高值來確定RecyclerView的實際寬高;
2、dispatchLayoutStep1()
、 dispatchLayoutStep2()
、 dispatchLayoutStep3()
這三個方法一定會執行,在RecyclerView的實際寬高不確定時,會提前多次執行dispatchLayoutStep1()
、 dispatchLayoutStep2()
方法,最後在onLayout()
階段執行 dispatchLayoutStep3()
,如果有itemView發生改變會再次執行dispatchLayoutStep2()
;
3、正在的測量和佈局itemView實際在dispatchLayoutStep2()
方法中。
RecyclerView的繪製三個步驟流程圖:
三、LinearLayoutManager填充、測量、佈局過程
RecyclerView的繪製經過measure、layout、draw三個步驟,但是itemView的真正佈局時委託給各個的LayoutManager中處理,上面LinearLayoutManager可以知道dispatchLayoutStep2()
是實際佈局視圖步驟,通過LayoutManager調用onLayoutChildren()
方法進行佈局itemView,它是繪製itemView的核心方法,表示從給定的適配器中列出所有相關的子視圖。
3.1 onLayoutChildren()佈局itemView
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 1) 檢查子類和其他變量找到描點座標和描點位置
// 2) 從開始填補,從底部堆積
// 3) 從底部填補,從頂部堆積
// 4) 從底部堆積來滿足需求
// 創建佈局狀態
if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);//移除所有子View
return;
}
}
ensureLayoutState();
mLayoutState.mRecycle = false;//禁止回收
//顛倒繪製佈局
resolveShouldLayoutReverse();
final View focused = getFocusedChild();//獲取目前持有焦點的child
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();//重置錨點信息
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
//1. 計算更新描點位置和座標
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
·······
//計算第一佈局的方向
int startOffset;
int endOffset;
final int firstLayoutDirection;
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
detachAndScrapAttachedViews(recycler);//暫時分離已經附加的view,即將所有child detach並通過Scrap回收
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
mLayoutState.mNoRecycleSpace = 0;
//2.開始填充,從底部開始堆疊;
if (mAnchorInfo.mLayoutFromEnd) {
//描點位置從start位置開始填充ItemView佈局
updateLayoutStateToFillStart(mAnchorInfo);
fill(recycler, mLayoutState, state, false);//填充所有itemView
//描點位置從end位置開始填充ItemView佈局
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);//填充所有itemView
endOffset = mLayoutState.mOffset;
}else { //3.向底填充,從上往下堆放;
//描點位置從end位置開始填充ItemView佈局
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
//描點位置從start位置開始填充ItemView佈局
updateLayoutStateToFillStart(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
//4.計算滾動偏移量,如果有必要會在調用fill方法去填充新的ItemView
layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
}
首先是狀態判斷和一些準備工作,對描點信息選擇和更新, detachAndScrapAttachedViews(recycler)
暫時將已經附加的view分離,緩存Scrap中,下次重新填充時直接拿出來複用。然後計算是從哪個方向開始佈局。佈局算法如下:
- 1.通過檢查子元素和其他變量,找到一個錨點座標和一個錨點項的位置;
- 2.開始填充,從底部開始堆疊;
- 3.向底填充,從上往下堆放;
- 4.滾動以滿足要求,如堆棧從底部。
3.2 fill()開始填充itemView
填充佈局交給了fill()
方法,表示填充由layoutState定義的給定佈局。爲什麼要fill兩次呢,我們來看看fill()
方法:
//填充方法,返回的是填充itemView的像素,方便後續滾動時使用
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
recycleByLayoutState(recycler, layoutState);//回收滑出屏幕的view
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//核心 == while()循環 ==
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {//一直循環,知道沒有數據
layoutChunkResult.resetInternal();
//填充itemView的核心方法
layoutChunk(recycler, state, layoutState, layoutChunkResult);
······
if (layoutChunkResult.mFinished) {//佈局結束,退出循環
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;//根據添加的child高度偏移計算
}
······
return start - layoutState.mAvailable;//返回這次填充的區域大小
}
fill()
核心就是一個while()循環,循環執行layoutChunk()
填充一個itemView到屏幕,同時返回這次填充的區域大小。首先根據屏幕還有多少剩餘空間remainingSpace,根據這個數值減去子View所佔的空間大小,小於0時佈局子View結束,如果當前所有子View還沒有超過remainingSpace時,調用layoutChunk()
安排View的位置。
3.3 layoutChunk()對itemView創建、填充、測量、佈局
layoutChunk()
作爲最終填充佈局itemView的方法,對itemView創建、填充、測量、佈局,主要有以下幾個步驟:
- 1.
layoutState.next(recycler)
從緩存中獲取itemView,如果沒有則創建itemView; - 2.根據實際情況來添加itemView到RecyclerView中,最終調用的還是ViewGroup的
addView()
方法; - 3.
measureChildWithMargins()
測量itemView大小包括父視圖的填充、項目裝飾和子視圖的邊距; - 4.根據計算好的left, top, right, bottom通過
layoutDecoratedWithMargins()
使用座標在RecyclerView中佈局給定的itemView。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//1.從緩存中獲取或者創建itemView
View view = layoutState.next(recycler);//獲取當前postion需要展示的View
······
//2.根據實際情況來添加itemView到RecyclerView中,最終調用的還是ViewGroup的addView()方法
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
}
//3.測量子View大小包括父視圖的填充、項目裝飾和子視圖的邊距
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
//計算一個ItemView的left, top, right, bottom座標值
int left, top, right, bottom;
······
//4.使用座標在RecyclerView中佈局給定的itemView
//計算正確的佈局位置,減去margin,計算所有視圖的邊界框(包括margin和裝飾)
layoutDecoratedWithMargins(view, left, top, right, bottom);//調用child.layout進行佈局
}
通過layoutState.next()
從緩存中獲取itemView如果沒有就創建一個新的itemView,然後addView()
根據實際情況來添加itemView到RecyclerView中,最終調用的還是ViewGroup的addView()
方法,接着通過 measureChildWithMargins()
測量子View大小包括父視圖的填充、項目裝飾和子視圖的邊距;最後getDecoratedMeasuredWidth()
通過計算好的left, top, right, bottom值在RecyclerView座標中佈局給定的itemView,注意這裏的寬度是item+decoration的總寬度。
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
獲取itemView,並且如果mScrapList
中有緩存的View 則使用緩存的view,如果沒有mScrapList 就創建view,並添加到mScrapList 中。接下來getViewForPosition()
方法主要是RecyclerView的緩存機制,後續的文章會講解到。
3.4 LinearLayoutManager填充、測量、佈局過程總結:
onLayoutChildren()
表示從給定的適配器中列出所有相關的子視圖,填充佈局交給了fill()
方法,填充由layoutState定義的給定佈局,while()循環執行layoutChunk()
填充一個itemView到屏幕,作爲最終填充佈局itemView的方法,layoutState.next(recycler)
從緩存中獲取或者創建itemView,通過addView()添加itemView到RecyclerView中,其實最終調用的還是ViewGroup的addView()
方法,measureChildWithMargins()
測量itemView大小包括父視圖的填充、項目裝飾和子視圖的邊距,最後layoutDecoratedWithMargins()
根據計算好的left, top, right, bottom通過使用座標在RecyclerView中佈局給定的itemView。
流程圖如下:
至此!本文結束。
請尊重原創者版權,轉載請標明出處:https://blog.csdn.net/m0_37796683/article/details/104864318 謝謝!
相關文章:
理解RecyclerView(五)
● RecyclerView的繪製流程
理解RecyclerView(六)
● RecyclerView的滑動原理
理解RecyclerView(七)
● RecyclerView的嵌套滑動機制
理解RecyclerView(八)
● RecyclerView的回收複用緩存機制詳解
理解RecyclerView(九)
● RecyclerView的自定義LayoutManager