RecyclerView 源碼淺析測量 布局 繪制 預(yù)布局
前言
上一篇博客內(nèi)容對 RecyclerView 回收復(fù)用機(jī)制相關(guān)源碼進(jìn)行了分析,本博客從自定義 View 三大流程 measure、layout、draw 的角度繼續(xù)對 RecyclerView 相關(guān)部分源碼進(jìn)行分析。
onMeasure
onMeasure 中的邏輯大體上分為三種情況,先來看下源碼:
RecyclerView.java
@Override protected void onMeasure(int widthSpec, int heightSpec) { // 第一種情況:沒有設(shè)置 LayoutManager if (mLayout == null) { defaultOnMeasure(widthSpec, heightSpec); return; } // 第二種情況:設(shè)置的 LayoutManager 開啟自動(dòng)測量 if (mLayout.isAutoMeasureEnabled()) { // ... mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); // ... mLastAutoMeasureSkippedDueToExact = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; if (mLastAutoMeasureSkippedDueToExact || mAdapter == null) { return; } if (mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); } mLayout.setMeasureSpecs(widthSpec, heightSpec); mState.mIsMeasuring = true; dispatchLayoutStep2(); // 需要二次測量 if (mLayout.shouldMeasureTwice()) { // ... dispatchLayoutStep2(); mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); } // ... } else { // 第三種情況:設(shè)置的 LayoutManager 沒有開啟自動(dòng)測量 // ... mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); // ... mState.mInPreLayout = false; // clear } }
源碼只貼出了重要部分,稍微總結(jié)下:
- 沒有設(shè)置 LayoutManager 時(shí),調(diào)用 defaultOnMeasure 方法;
- 設(shè)置 LayoutManager 并且開啟自動(dòng)測量時(shí),調(diào)用 LayoutManager 的 onMeasure 方法,并且會(huì)執(zhí)行 dispatchLayoutStep1()、dispatchLayoutStep2();
- 設(shè)置 LayoutManager 且沒有開始自動(dòng)測量時(shí),僅調(diào)用了 LayoutManager 的 onMeasure 方法;
先來看一下 LayoutManager 的 onMeasure 方法:
RecyclerView.java
public abstract static class LayoutManager{ // ... public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,int heightSpec) { mRecyclerView.defaultOnMeasure(widthSpec, heightSpec); } }
默認(rèn)實(shí)現(xiàn)和第一種情況一樣,調(diào)用了 defaultOnMeasure 方法,而且 sdk 中給我們提供的三種 LayoutManager(LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager)均沒有重寫 onMeasure 方法。
接著就來看看 defaultOnMeasure 的源碼:
RecyclerView.java
void defaultOnMeasure(int widthSpec, int heightSpec) { // 通過 LayoutManager.chooseSize 獲取的寬和高的值 final int width = LayoutManager.chooseSize(widthSpec, getPaddingLeft() + getPaddingRight(), // 橫向內(nèi)邊距 ViewCompat.getMinimumWidth(this)); // 反射獲取是否設(shè)置最小寬度 final int height = LayoutManager.chooseSize(heightSpec, getPaddingTop() + getPaddingBottom(), // 縱向內(nèi)邊距 ViewCompat.getMinimumHeight(this)); // 反射獲取是否設(shè)置最小寬度 // 設(shè)置寬高 setMeasuredDimension(width, height); }
接著看一下 LayoutManager.chooseSize 是如何獲取寬高的:
public static int chooseSize(int spec, int desired, int min) { final int mode = View.MeasureSpec.getMode(spec); final int size = View.MeasureSpec.getSize(spec); switch (mode) { case View.MeasureSpec.EXACTLY: return size; case View.MeasureSpec.AT_MOST: return Math.min(size, Math.max(desired, min)); case View.MeasureSpec.UNSPECIFIED: default: return Math.max(desired, min); } }
這段代碼就不用解釋了吧?自定義 View 時(shí)經(jīng)常會(huì)根據(jù) mode 不同來處理寬高的最終值。
測量這部分到目前為止的代碼都比較簡單,測量對于寬高這部分并沒有特殊處理,剩余重要邏輯都在 dispatchLayoutStep1()、dispatchLayoutStep2() 方法中,這里先不對其進(jìn)行詳細(xì)解釋,因?yàn)橄旅娴?onLayout 中還有一個(gè) dispatchLayoutStep3() 方法。
稍微總結(jié)下,測量部分除非有特殊的自定義 LayoutManager 對寬高有自定義需求,一般情況都會(huì)走默認(rèn)的 defaultOnMeasure 方法,和大部分自定義 View 相同根據(jù) mode 確定寬高。
onLayout
自定義 View 的第二大流程 onLayout,直接看源碼:
RecyclerView.java
protected void onLayout(boolean changed, int l, int t, int r, int b) { TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG); dispatchLayout(); // 只有一處方法調(diào)用 分發(fā)布局 TraceCompat.endSection(); mFirstLayoutComplete = true; } void dispatchLayout() { // ... // 在 onMeasure 中這個(gè)值被設(shè)置為 true mState.mIsMeasuring = false; // ... if (mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); } else if (mAdapterHelper.hasUpdates() || needsRemeasureDueToExactSkip || mLayout.getWidth() != getWidth() || mLayout.getHeight() != getHeight()) { mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); } else { mLayout.setExactMeasureSpecsFrom(this); } dispatchLayoutStep3(); // mState.mLayoutStep 的值在里面被設(shè)置為 State.STEP_START }
onLayout 中的邏輯并不復(fù)雜,邏輯都放在了 dispatchLayout 中,而 dispatchLayout 中又根據(jù)各種判斷確保了 dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3 都會(huì)執(zhí)行。至于這三個(gè)方法在最后一小節(jié)分析。
onDraw
RecyclerView 重寫了 draw 方法,那么就先看一下 draw 方法:
RecyclerView.java
public void draw(Canvas c) { super.draw(c); final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDrawOver(c, this, mState); } // ... }
draw 方法中獲取了所有的 ItemDecoration 也就是“分割線”,調(diào)用了其 onDrawOver 方法。關(guān)于 ItemDecoration 將和 LayoutManager 一起在下一篇博客中分析。
draw 的源碼中會(huì)繼續(xù)調(diào)用 onDraw 方法,繼續(xù)看一下 onDraw 方法:
RecyclerView.java
public void onDraw(Canvas c) { super.onDraw(c); // draw 方法中調(diào)用了 ItemDecoration 的 onDrawOver // onDraw 方法又調(diào)用了 ItemDecoration 的 onDraw final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDraw(c, this, mState); } }
可以看出 draw 和 onDraw 主要對分割線進(jìn)行了繪制,由于 draw 方法先執(zhí)行,那么也就意味著 ItemDecoration 的 onDrawOver 方法會(huì)先繪制,之后再執(zhí)行其 onDraw 方法。
dispatchLayoutStep1、2、3
三大流程的整體代碼流程并不復(fù)雜,核心邏輯都在 dispatchLayoutStep1、2、3 這三個(gè)方法中,字面意思翻譯過來是“分發(fā)布局步驟1、2、3”,下面挨著來分析下。
dispatchLayoutStep1
概述:處理 Adapter 更新,決定哪個(gè)動(dòng)畫應(yīng)該被執(zhí)行,保存當(dāng)前的視圖信息,如果需要的話進(jìn)行預(yù)布局并保存相關(guān)信息。
RecyclerView.java
private void dispatchLayoutStep1() { // 確認(rèn)布局步驟 在 dispatchLayoutStep3 會(huì)設(shè)置為 STEP_START // 并且 onLayout 調(diào)用 dispatchLayoutStep1 之前也進(jìn)行了判斷 mState.assertLayoutStep(State.STEP_START); // 獲取剩余的滾動(dòng)距離(橫豎向) fillRemainingScrollValues(mState); // onMeasure 中標(biāo)記為 true 這里再置為 false mState.mIsMeasuring = false; startInterceptRequestLayout(); // ViewInfoStore 用于保存動(dòng)畫相關(guān)信息 // 清除保存的信息 mViewInfoStore.clear(); // 標(biāo)記進(jìn)入布局或者滾動(dòng)狀態(tài) 內(nèi)部是int類型進(jìn)行++操作 onEnterLayoutOrScroll(); // 適配器更新和動(dòng)畫預(yù)處理 設(shè)置 mState.mRunSimpleAnimations 和 mState.mRunPredictiveAnimations 的值 processAdapterUpdatesAndSetAnimationFlags(); // 保存焦點(diǎn)信息 saveFocusInfo(); // 一些信息保存 mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged; mItemsAddedOrRemoved = mItemsChanged = false; // 預(yù)布局標(biāo)志 和 mRunPredictiveAnimation 有關(guān) 這里先記住 后面會(huì)解釋什么是預(yù)布局 mState.mInPreLayout = mState.mRunPredictiveAnimations; mState.mItemCount = mAdapter.getItemCount(); findMinMaxChildLayoutPositions(mMinMaxLayoutPositions); // 下面兩個(gè) if 都是動(dòng)畫預(yù)處理 保存信息等等 // mRunSimpleAnimations 可以理解為 需要執(zhí)行動(dòng)畫 if (mState.mRunSimpleAnimations) { // ... int count = mChildHelper.getChildCount(); for (int i = 0; i < count; ++i) { // ... // 保存執(zhí)行動(dòng)畫所需的信息 (預(yù)布局時(shí)的信息) mViewInfoStore.addToPreLayout(holder, animationInfo); // ... } } // mRunPredictiveAnimations 可以理解為 需要執(zhí)行動(dòng)畫的情況下需要進(jìn)行預(yù)布局 // 換而言之需要拿到動(dòng)畫執(zhí)行前后的各種信息(坐標(biāo)等等) if (mState.mRunPredictiveAnimations) { // ... // 這里如果需要預(yù)布局就調(diào)用 LayoutManager 的 onLayoutChildren 開始布局 // 注意 mState.mInPreLayout = mRunPredictiveAnimations // 當(dāng) mRunPredictiveAnimations 為 ture 時(shí) mInPreLayout 同樣為 true mLayout.onLayoutChildren(mRecycler, mState); // ... } else { clearOldPositions(); } onExitLayoutOrScroll(); // 和 startInterceptRequestLayout 成對使用 貌似是防止多次 requestLayout stopInterceptRequestLayout(false); // 標(biāo)記 STEP_START 完成 可以執(zhí)行 dispatchLayoutStep2 mState.mLayoutStep = State.STEP_LAYOUT; }
dispatchLayoutStep1 整體上都是預(yù)布局處理,對動(dòng)畫信息的保存等等,ViewInfoStore 是用于存儲(chǔ) item 動(dòng)畫相關(guān)信息,后面的博客中會(huì)分析。注意重點(diǎn),如果需要執(zhí)行動(dòng)畫將會(huì)執(zhí)行預(yù)布局,也就是調(diào)用 mLayout.onLayoutChildren 之前 mInPreLayout 為 true。 我并沒有每一行代碼都研究透徹,看源碼也大可不必讀懂每一行代碼,那樣會(huì)越陷越深。
dispatchLayoutStep2
概述:預(yù)布局狀態(tài)結(jié)束,開始真正的布局。
RecyclerView.java
private void dispatchLayoutStep2() { startInterceptRequestLayout(); // 和 stopInterceptRequestLayout 成對出現(xiàn) onEnterLayoutOrScroll(); // 上面已經(jīng)說過了 // 判斷 State ,在 dispatchLayoutStep1 已經(jīng)標(biāo)記為 STEP_LAYOUT mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS); mAdapterHelper.consumeUpdatesInOnePass(); mState.mItemCount = mAdapter.getItemCount(); mState.mDeletedInvisibleItemCountSincePreviousLayout = 0; if (mPendingSavedState != null && mAdapter.canRestoreState()) { if (mPendingSavedState.mLayoutState != null) { mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState); } mPendingSavedState = null; } // 預(yù)布局標(biāo)記為 fasle mState.mInPreLayout = false; // 開始布局 這里是真正的測量和布局items mLayout.onLayoutChildren(mRecycler, mState); mState.mStructureChanged = false; mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null; // 標(biāo)記 STEP_ANIMATIONS mState.mLayoutStep = State.STEP_ANIMATIONS; onExitLayoutOrScroll(); stopInterceptRequestLayout(false); // 和 startInterceptRequestLayout 成對出現(xiàn) }
dispatchLayoutStep2 方法代碼不多,注意重點(diǎn),在調(diào)用 mLayout.onLayoutChildren(mRecycler, mState) 之前將 mState.mInPreLayout 預(yù)布局標(biāo)記為 false。
到這里可以看出預(yù)布局過程就發(fā)生在 dispatchLayoutStep1、2 之間。
dispatchLayoutStep3
概述:執(zhí)行 item 動(dòng)畫以及布局完成后的收尾工作。
RecyclerView.java
private void dispatchLayoutStep3() { // 判斷狀態(tài)是否為 STEP_ANIMATIONS mState.assertLayoutStep(State.STEP_ANIMATIONS); // 和 stopInterceptRequestLayout 成對出現(xiàn) startInterceptRequestLayout(); // 和 onExitLayoutOrScroll 成對出現(xiàn) onEnterLayoutOrScroll(); // 標(biāo)記為 STEP_START, 在步驟 1 中會(huì)判斷是否為 STEP_START mState.mLayoutStep = State.STEP_START; if (mState.mRunSimpleAnimations) { for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) { ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); // ... // 保存動(dòng)畫信息 // 布局完成后的坐標(biāo)等等 mViewInfoStore.addToPostLayout(holder, animationInfo); // ... } } // 執(zhí)行 item 動(dòng)畫 mViewInfoStore.process(mViewInfoProcessCallback); } // 清理 mAttachedScrap 和 mChangedScraop 緩存 mLayout.removeAndRecycleScrapInt(mRecycler); // item 數(shù)量 mState.mPreviousLayoutItemCount = mState.mItemCount; // 相關(guān)標(biāo)記設(shè)為初始值 mDataSetHasChangedAfterLayout = false; mDispatchItemsChangedEvent = false; mState.mRunSimpleAnimations = false; mState.mRunPredictiveAnimations = false; mLayout.mRequestedSimpleAnimations = false; // ... // 布局完成回調(diào) mLayout.onLayoutCompleted(mState); onExitLayoutOrScroll(); stopInterceptRequestLayout(false); // 清理 mViewInfoStore.clear(); // ... }
mAttachedScrap 和 mChangedScrap
在本系列博客第一篇分析回收復(fù)用源碼時(shí)對 mAttachedScrap 和 mChangedScrap 并沒有詳細(xì)說明,到這里可以對他們倆分析一下了。
在第一篇的回收復(fù)用中,回收部分源碼執(zhí)行時(shí),并沒有用到 mAttachedScrap 和 mChangedScrap,復(fù)用時(shí)卻優(yōu)先在他們倆容器中尋找緩存?,F(xiàn)在在布局步驟3 dispatchLayoutStep3 中也對其進(jìn)行了清空,那么說明在 dispatchLayoutStep3 之前對其肯定有過回收的操作。
布局步驟1、2、3中,大部分邏輯都在 mLayout.onLayoutChildren 中,但其是一個(gè)空實(shí)現(xiàn),所以,就以其開發(fā)中最常用到的實(shí)現(xiàn)類 LinearLayoutManager 源碼來分析看看:
LinearLayoutManager.java
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state){ // ... // detachAndScrapAttachedViews(recycler); // ... // fill 在第一篇博客中提到了 填充布局 算是回收復(fù)用的入口 fill(recycler, mLayoutState, state, false); } public void detachAndScrapAttachedViews(@NonNull Recycler recycler) { // 從這里可以看出將所有的可見的 item 都回收到了 mAttachedScrap 或者 mChangedScrap 中 final int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { final View v = getChildAt(i); scrapOrRecycleView(recycler, i, v); } }
在 onLayoutChildren 中對可見的 item 都進(jìn)行了回收操作,并且緊接著執(zhí)行了 fill 進(jìn)行了填充布局操作。由上述對 dispatchLayout1、2、3 的源碼分析可以得知,dispatchLayout3 對 mAttachedScrap 和 mChangedScrap 進(jìn)行了清空操作,dispatchLayout1 調(diào)用 onLayoutChildren 進(jìn)行預(yù)布局操作,而 dispatchLayout2 調(diào)用 onLayoutChildren 進(jìn)行真正的布局操作。
那么顯而易見,mAttachedScrap 和 mChangedScrap 是對可見 item 的緩存,目的在于預(yù)布局、真正的布局階段復(fù)用,不用重新綁定數(shù)據(jù)。
預(yù)布局
上述內(nèi)容中多次提到過預(yù)布局,到底什么是預(yù)布局?先大概說一下預(yù)布局的使用場景,如下圖所示:
假如屏幕中有一個(gè) RecyclerView 且其有三個(gè) item,當(dāng)刪除 item3 時(shí),item4 會(huì)遞補(bǔ)出現(xiàn)在屏幕內(nèi)。這是開發(fā)中非常常見的情況吧,一般執(zhí)行刪除或者新增操作,我們都會(huì)添加動(dòng)畫讓其顯得不生硬,那么思考下 item4 是什么時(shí)候添加到屏幕上的呢?
回到 LinearLayoutManager 的 fill 方法查看源碼:
LinearLayoutManager.java
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { //... // 可用空間 int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace; // 當(dāng) remainingSpace > 0 會(huì)繼續(xù)循環(huán) while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { // ... // 布局 layoutChunk(recycler, state, layoutState, layoutChunkResult); // 計(jì)算可用空間 // 注意這里的判斷條件 if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null || !state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed; remainingSpace -= layoutChunkResult.mConsumed; } } }
在計(jì)算可用空間時(shí),有三個(gè)判斷條件:
!layoutChunkResult.mIgnoreConsumed
layoutState.mScrapList != null
!state.isPreLayout()
重點(diǎn)看 1 和 3,先說 3 吧,如果是預(yù)布局狀態(tài),也就是 dispatchLayoutStep1 調(diào)用進(jìn)來時(shí)第三個(gè)條件是 false。至于條件 1 還需要看一下 layoutChunk 方法源碼:
LinearLayoutManager.java
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { // ... RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); // ... // 源碼最后部分有這么一處判斷 // 如果 viewholder 被標(biāo)記為了移除或者改變 mIgnoreConsumed 設(shè)為 true if (params.isItemRemoved() || params.isItemChanged()) { result.mIgnoreConsumed = true; } result.mFocusable = view.hasFocusable(); }
看完這段代碼再回到上面圖示中的場景:
當(dāng) item3 被刪除時(shí),在預(yù)布局階段它所占用的空間會(huì)忽略不計(jì),那么 fill 方法中在計(jì)算可用空間時(shí)就會(huì)多走一次 while 循環(huán),從而多添加一個(gè) item。
那么 dispatchStep1 即可稱之為預(yù)布局階段,此時(shí)將要移除的 item3 以及即將添加到屏幕上的 item4 的預(yù)布局階段的位置信息等等保存,在 dispatchStep2 真正布局階段保存完成刪除操作后的位置信息等等,即可在 dispatchStep3 中根據(jù)兩個(gè)信息之間的差異做出對應(yīng)的 item 動(dòng)畫。關(guān)于動(dòng)畫部分后面博客還會(huì)分析,由于篇幅原因暫時(shí)理解到這里。
最后
本篇博客內(nèi)容從自定義 View 的三大流程角度開始分析 RecyclerView 相關(guān)源碼,接著牽連出分發(fā)布局的三個(gè)階段以及對預(yù)布局的理解。關(guān)于動(dòng)畫部分沒有多提,后面動(dòng)畫部分會(huì)單獨(dú)一篇博客分析。
以上就是RecyclerView 源碼淺析測量 布局 繪制 預(yù)布局的詳細(xì)內(nèi)容,更多關(guān)于RecyclerView測量 布局 繪制 預(yù)布局的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Flutter StreamBuilder組件實(shí)現(xiàn)局部刷新示例講解
日常使用最多的局部刷新為Provider狀態(tài)管理 Selector,今天分享flutter框架自帶的StreamBuilder組件,該組件可做到局部刷新,使用簡單且輕便2022-11-11Android List刪除重復(fù)數(shù)據(jù)
這篇文章主要介紹了Android List刪除重復(fù)數(shù)據(jù)的實(shí)例代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2017-06-06Android自定義SwipeRefreshLayout高仿微信朋友圈下拉刷新
這篇文章主要以社交APP的BOSS微信為例,介紹了Android自定義SwipeRefreshLayout高仿微信朋友圈下拉刷新,感興趣的小伙伴們可以參考一下2016-07-07Android 使用AsyncTask實(shí)現(xiàn)多任務(wù)多線程斷點(diǎn)續(xù)傳下載
這篇文章主要介紹了Android 使用AsyncTask實(shí)現(xiàn)多任務(wù)多線程斷點(diǎn)續(xù)傳下載的相關(guān)資料,需要的朋友可以參考下2018-05-05Android實(shí)現(xiàn)動(dòng)態(tài)自動(dòng)匹配輸入的內(nèi)容
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)動(dòng)態(tài)自動(dòng)匹配輸入的內(nèi)容,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08Android EditText限制輸入字符類型的方法總結(jié)
這篇文章主要介紹了Android EditText限制輸入字符類型的方法總結(jié)的相關(guān)資料,需要的朋友可以參考下2017-03-03