RecyclerView 原始碼分析(一) - RecyclerView的三大流程
從今天開始,樓主正式開始分析 RecyclerView
的原始碼。為了閱讀 RecyclerView
的原始碼,樓主專門去看了 View
的三大流程,也就是所謂的刷裝備。當然在閱讀 RecyclerView
的原始碼時,也參考了其他大佬的文章,本文儘可能的貼出比較優秀的文章,正所謂他山之石,可以攻玉。
作為系列的第一篇文章,說說樓主為什麼需要來專門的閱讀 RecyclerView
的原始碼,主要從三大方面說起。一是 RecyclerView
在實際開發非常的重要,現在幾乎每個app都會展示很多的資料,列表展示自然是非常好的方式,而在 RecyclerView
在列表中佔據著舉足輕重的作用,所以 RecyclerView
在實際開發中,是經常見的,我們得之魚,還必須得之漁;二是現在網上分析RecyclerView的文章滿篇飛,但是文章大多都比較零碎,沒有系統的分析 RecyclerView
,本文打算系統的分析 RecyclerView
,也可以說是集百家之長;三是樓主本身對 RecycleView
的使用也是非常的頻繁,但是沒有深入的瞭解它的原理,所以這也算是對自身的一個提升。
閱讀原始碼本身是一件非常枯燥和耗時間的事情,對樓主自身來說,也是亞歷山大,害怕自己自身的經驗不足,誤導前來學習的同學,所以如果文章中有錯誤的地方,請各位大佬指點。
本系列文章樓主打算從幾個地方說起。先是將 RecyclerView
當成一個普通的 View
,分別分析它的三大流程、事件傳遞(包括巢狀滑動);然後是分析 RecyclerView
的快取原理,這也是 RecyclerView
的精華所在;然後分析的是 RecyclerView
的 Adapter
、 LayoutManager
、 ItemAnimator
和 ItemDecoration
。最後就是 RecyclerView
的擴充套件,包括 LayoutManager
的自定義和使用 RecyclerView
常見的坑等。
看到上面所寫的列表,自己也不禁留下冷汗,原來 RecyclerView
有這麼多的內容,真擔心自己不能完成任務:joy:。
1. 概述
在分析 RecyclerView
原始碼之前,我們還是對 RecyclerView
有一個初步的瞭解,簡單的瞭解它是什麼,它的基本結構有哪些。
RecyclerView
是Google爸爸在2014年的IO大會提出來(看來 RecyclerView
的年齡還是比較大了:joy:),具體目的是不是用來替代 ListView
的,樓主也不知道,因為那時候樓主還在讀高二。但是在實際開發中,自從有了 RecyclerView
, ListView
和 GridView
就很少用了,所以我們暫且認為 RecyclerView
的目的是替代 ListView
和 GridView
。
RecyclerView
本身是一個展示大量資料的控制元件,相比較 ListView
, RecyclerView
的4級快取(也有人說是3級快取,這些都不重要:joy:)就表現的非常出色,在效能方面相比於 ListView
提升了不少。同時由於 LayoutManager
的存在,讓 RecyclerView
不僅有 ListView
的特點,同時兼有 GridView
的特點。這可能是 RecyclerView
受歡迎的原因之一吧。
RecyclerView
在設計方面上也是非常的靈活,不同的部分承擔著不同的職責。其中 Adapter
負責提供資料,包括建立 ViewHolder
和繫結資料, LayoutManager
負責 ItemView
的測量和佈局, ItemAnimator
負責每個 ItemView
的動畫, ItemDecoration
負責每個 ItemView
的間隙。這種插拔式的架構使得 RecyclerView
變得非常的靈活,每一個人都可以根據自身的需求來定義不同的部分。
正因為這種插拔式的設計,使得 RecyclerView
在使用上相比較於其他的控制元件稍微難那麼一點點,不過這都不算事,誰叫 RecyclerView
這麼惹人愛呢:joy:。
好了,好像廢話有點多,現在我們正式來分析原始碼吧,本文的重點是 RecyclerView
的三大流程。
本文參考文章:
注意,本文 RecyclerView
原始碼均來自於27.1.1
2. measure
不管 RecyclerView
是多麼神奇,它也是一個 View
,所以分析它的三大流程是非常有必要的。同時,如果瞭解過 RecyclerView
的同學應該都知道, RecyclerView
的三大流程跟普通的 View
比較,有很大的不同。
首先,我們來看看measure過程,來看看 RecyclerView
的 onMeasure
方法。
protected void onMeasure(int widthSpec, int heightSpec) { if (mLayout == null) { // 第一種情況 } if (mLayout.isAutoMeasureEnabled()) { // 第二種情況 } else { // 第三種情況 } }
onMeasure
方法還是有點長,這裡我將它分為3種情況,我將簡單解釋這三種情況。
-
mLayout
即LayoutManager
的物件。我們知道,當RecyclerView
的LayoutManager
為空時,RecyclerView
不能顯示任何的資料,在這裡我們找到答案。 -
LayoutManager
開啟了自動測量時,這是一種情況。在這種情況下,有可能會測量兩次。 - 第三種情況就是沒有開啟自動測量的情況,這種情況比較少,因為為了
RecyclerView
支援warp_content
屬性,系統提供的LayoutManager
都開啟自動測量的,不過我們還是要分析的。
首先我們來第一種情況。
(1).當LayoutManager為空時
這種情況下比較簡單,我們來看看原始碼:
if (mLayout == null) { defaultOnMeasure(widthSpec, heightSpec); return; }
直接調了 defaultOnMeasure
方法,我們繼續來看 defaultOnMeasure
方法。
void defaultOnMeasure(int widthSpec, int heightSpec) { // calling LayoutManager here is not pretty but that API is already public and it is better // than creating another method since this is internal. final int width = LayoutManager.chooseSize(widthSpec, getPaddingLeft() + getPaddingRight(), ViewCompat.getMinimumWidth(this)); final int height = LayoutManager.chooseSize(heightSpec, getPaddingTop() + getPaddingBottom(), ViewCompat.getMinimumHeight(this)); setMeasuredDimension(width, height); }
在 defaultOnMeasure
方法裡面,先是通過 LayoutManager
的 chooseSize
方法來計算值,然後就是 setMeasuredDimension
方法來設定寬高。我們來看看:
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); } }
chooseSize
方法表達的意思比較簡單,就是通過 RecyclerView
的測量mode來獲取不同的值,這裡就不詳細的解釋了。
到此,第一種情況就分析完畢了。因為當 LayoutManager
為空時,那麼當 RecyclerView
處於 onLayout
階段時,會呼叫 dispatchLayout
方法。而在 dispatchLayout
方法裡面有這麼一行程式碼:
if (mLayout == null) { Log.e(TAG, "No layout manager attached; skipping layout"); // leave the state in START return; }
所以,當 LayoutManager
為空時,不顯示任何資料是理所當然的。
現在我們來看看第二種情況,也就是正常的情況。
(2). 當LayoutManager開啟了自動測量
在分析這種情況之前,我們先對了解幾個東西。
RecyclerView
的測量分為兩步,分別呼叫 dispatchLayoutStep1
和 dispatchLayoutStep2
。同時,瞭解過 RecyclerView
原始碼的同學應該知道在 RecyclerView
的原始碼裡面還一個 dispatchLayoutStep3
方法。這三個方法的方法名比較接近,所以容易讓人搞混淆。本文會詳細的講解這三個方法的作用。
由於在這種情況下,只會呼叫 dispatchLayoutStep1
和 dispatchLayoutStep2
這兩個方法,所以這裡會重點的講解這兩個方法。而 dispatchLayoutStep3
方法的呼叫在 RecyclerView
的 onLayout
方法裡面,所以在後面分析 onLayout
方法時再來看 dispatchLayoutStep3
方法。
我們在分析之前,先來看一個東西-- mState.mLayoutStep
。這個變數有幾個取值情況。我們分別來看看:
取值 | 含義 |
---|---|
State.STEP_START | mState.mLayoutStep 的預設值,這種情況下,表示RecyclerView還未經歷 dispatchLayoutStep1 ,因為 dispatchLayoutStep1 呼叫之後 mState.mLayoutStep 會變為 State.STEP_LAYOUT 。 |
State.STEP_LAYOUT | 當 mState.mLayoutStep 為 State.STEP_LAYOUT 時,表示此時處於layout階段,這個階段會呼叫 dispatchLayoutStep2 方法 layout RecyclerView 的 children 。呼叫 dispatchLayoutStep2 方法之後,此時 mState.mLayoutStep 變為了 State.STEP_ANIMATIONS 。 |
State.STEP_ANIMATIONS | 當 mState.mLayoutStep 為 State.STEP_ANIMATIONS 時,表示 RecyclerView 處於第三個階段,也就是執行動畫的階段,也就是呼叫 dispatchLayoutStep3 方法。當 dispatchLayoutStep3 方法執行完畢之後, mState.mLayoutStep 又變為了 State.STEP_START 。 |
從上表中,我們瞭解到 mState.mLayoutStep
的三個狀態對應著不同的 dispatchLayoutStep
方法。這一點,我們必須清楚,否則接下來的程式碼將難以理解。
好了,前戲準備的差不多,現在應該進入高潮了:joy:。我們開始正式的分析原始碼了。
if (mLayout.isAutoMeasureEnabled()) { final int widthMode = MeasureSpec.getMode(widthSpec); final int heightMode = MeasureSpec.getMode(heightSpec); /** * This specific call should be considered deprecated and replaced with * {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could * break existing third party code but all documentation directs developers to not * override {@link LayoutManager#onMeasure(int, int)} when * {@link LayoutManager#isAutoMeasureEnabled()} returns true. */ mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); final boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; if (measureSpecModeIsExactly || mAdapter == null) { return; } if (mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); } // set dimensions in 2nd step. Pre-layout should happen with old dimensions for // consistency mLayout.setMeasureSpecs(widthSpec, heightSpec); mState.mIsMeasuring = true; dispatchLayoutStep2(); // now we can get the width and height from the children. mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); // if RecyclerView has non-exact width and height and if there is at least one child // which also has non-exact width & height, we have to re-measure. if (mLayout.shouldMeasureTwice()) { mLayout.setMeasureSpecs( MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); mState.mIsMeasuring = true; dispatchLayoutStep2(); // now we can get the width and height from the children. mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); } }
我將這段程式碼分為三步。我們來看看:
- 呼叫
LayoutManager
的onMeasure
方法進行測量。對於onMeasure
方法,我也感覺到非常的迷惑,發現傳統的LayoutManager
都沒有實現這個方法。後面,我們會將簡單的看一下這個方法。 - 如果
mState.mLayoutStep
為State.STEP_START
的話,那麼就會執行dispatchLayoutStep1
方法,然後會執行dispatchLayoutStep2
方法。 - 如果需要第二次測量的話,會再一次呼叫
dispatchLayoutStep2
方法。
以上三步,我們一步一步的來分析。首先,我們來看看第一步,也是看看 onMeasure
方法。
LayoutManager
的 onMeasure
方法究竟為我們做什麼,我們來看看:
public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) { mRecyclerView.defaultOnMeasure(widthSpec, heightSpec); }
預設是呼叫的 RecyclerView
的 defaultOnMeasure
方法,至於 defaultOnMeasure
方法裡面究竟做了什麼,這在前面已經介紹過了,這裡就不再介紹了。
View
的 onMeasure
方法的作用通產來說有兩個。一是測量自身的寬高,從 RecyclerView
來看,它將自己的測量工作託管給了 LayoutManager
的 onMeasure
方法。所以,我們在自定義 LayoutManager
時,需要注意 onMeasure
方法存在,不過從官方提供的幾個 LayoutManager
,都沒有重寫這個方法。所以不到萬得已,最好不要重寫 LayoutManager
的 onMeasure
方法;二是測量子 View
,不過到這裡我們還沒有看到具體的實現。
接下來,我們來分析第二步,看看 dispatchLayoutStep1
方法和 dispatchLayoutStep2
方法究竟做了什麼。
在正式分析第二步之前,我們先對這三個方法有一個大概的認識。
方法名 | 作用 |
---|---|
dispatchLayoutStep1 | 三大 dispatchLayoutStep 方法第一步。本方法的作用主要有三點:1.處理 Adapter 更新;2.決定是否執行 ItemAnimator ;3.儲存 ItemView 的動畫資訊。本方法也被稱為preLayout(預佈局),當 Adapter 更新了,這個方法會儲存每個 ItemView 的舊資訊(oldViewHolderInfo) |
dispatchLayoutStep2 | 三大 dispatchLayoutStep 方法第二步。在這個方法裡面,真正進行 children 的測量和佈局。 |
dispatchLayoutStep3 | 三大 dispatchLayoutStep 方法第三步。這個方法的作用執行在 dispatchLayoutStep1 方法裡面儲存的動畫資訊。本方法不是本文的介紹重點,後面在介紹 ItemAnimator 時,會重點分析這個方法。 |
我們回到onMeasure方法裡面,先看看整個執行過程。
if (mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); } // set dimensions in 2nd step. Pre-layout should happen with old dimensions for // consistency mLayout.setMeasureSpecs(widthSpec, heightSpec); mState.mIsMeasuring = true; dispatchLayoutStep2();
如果 mState.mLayoutStep == State.STEP_START
時,才會呼叫 dispatchLayoutStep1
方法,這裡與我們前面介紹 mLayoutStep
對應起來了。現在我們看看 dispatchLayoutStep1
方法
private void dispatchLayoutStep1() { mState.assertLayoutStep(State.STEP_START); fillRemainingScrollValues(mState); mState.mIsMeasuring = false; startInterceptRequestLayout(); mViewInfoStore.clear(); onEnterLayoutOrScroll(); processAdapterUpdatesAndSetAnimationFlags(); saveFocusInfo(); mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged; mItemsAddedOrRemoved = mItemsChanged = false; mState.mInPreLayout = mState.mRunPredictiveAnimations; mState.mItemCount = mAdapter.getItemCount(); findMinMaxChildLayoutPositions(mMinMaxLayoutPositions); if (mState.mRunSimpleAnimations) { // 找到沒有被remove的ItemView,儲存OldViewHolder資訊,準備預佈局 } if (mState.mRunPredictiveAnimations) { // 進行預佈局 } else { clearOldPositions(); } onExitLayoutOrScroll(); stopInterceptRequestLayout(false); mState.mLayoutStep = State.STEP_LAYOUT; }
本文只簡單分析一下這個方法,因為這個方法跟 ItemAnimator
有莫大的關係,後續在介紹 ItemAnimator
時會詳細的分析。在這裡,我們將重點放在 processAdapterUpdatesAndSetAnimationFlags
裡面,因為這個方法計算了 mRunSimpleAnimations
和 mRunPredictiveAnimations
。
private void processAdapterUpdatesAndSetAnimationFlags() { if (mDataSetHasChangedAfterLayout) { // Processing these items have no value since data set changed unexpectedly. // Instead, we just reset it. mAdapterHelper.reset(); if (mDispatchItemsChangedEvent) { mLayout.onItemsChanged(this); } } // simple animations are a subset of advanced animations (which will cause a // pre-layout step) // If layout supports predictive animations, pre-process to decide if we want to run them if (predictiveItemAnimationsEnabled()) { mAdapterHelper.preProcess(); } else { mAdapterHelper.consumeUpdatesInOnePass(); } boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged; mState.mRunSimpleAnimations = mFirstLayoutComplete && mItemAnimator != null && (mDataSetHasChangedAfterLayout || animationTypeSupported || mLayout.mRequestedSimpleAnimations) && (!mDataSetHasChangedAfterLayout || mAdapter.hasStableIds()); mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations && animationTypeSupported && !mDataSetHasChangedAfterLayout && predictiveItemAnimationsEnabled(); }
這裡我們的重心放在 mFirstLayoutComplete
變數裡面,我們發現 mRunSimpleAnimations
的值與 mFirstLayoutComplete
有關, mRunPredictiveAnimations
同時跟 mRunSimpleAnimations
有關。所以這裡我們可以得出一個結論,當 RecyclerView
第一次載入資料時,是不會執行的動畫。換句話說,每個 ItemView
還沒有 layout
完畢,怎麼會進行動畫。這一點,我們也可以通過Demo來證明,這裡也就不展示了。
接下來我們看看 dispatchLayoutStep2
方法,這個方法是真正佈局 children
。我們來看看:
private void dispatchLayoutStep2() { startInterceptRequestLayout(); onEnterLayoutOrScroll(); mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS); mAdapterHelper.consumeUpdatesInOnePass(); mState.mItemCount = mAdapter.getItemCount(); mState.mDeletedInvisibleItemCountSincePreviousLayout = 0; // Step 2: Run layout mState.mInPreLayout = false; mLayout.onLayoutChildren(mRecycler, mState); mState.mStructureChanged = false; mPendingSavedState = null; // onLayoutChildren may have caused client code to disable item animations; re-check mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null; mState.mLayoutStep = State.STEP_ANIMATIONS; onExitLayoutOrScroll(); stopInterceptRequestLayout(false); }
在這裡,我們重點的看兩行程式碼。一是在這裡,我們可以看到 Adapter
的 getItemCount
方法被呼叫;二是呼叫了 LayoutManager
的 onLayoutChildren
方法,這個方法裡面進行對 children
的測量和佈局,同時這個方法也是這裡的分析重點。
系統的 LayoutManager
的 onLayoutChildren
方法是一個空方法,所以需要 LayoutManager
的子類自己來實現。從這裡,我們可以得出兩個點。
- 子類
LayoutManager
需要自己實現onLayoutChildren
方法,從而來決定RecyclerView
在該LayoutManager
的策略下,應該怎麼佈局。從這裡,我們看出來RecyclerView
的靈活性。 -
LayoutManager
類似於ViewGroup
,將onLayoutChildren
方法(ViewGroup
是onLayout
方法)公開出來,這種模式在Android中很常見的。
這裡,我先不對 onLayoutChildren
方法進行展開,待會會詳細的分析。
接下來,我們來分析第三種情況-- 沒有開啟自動測量 。
(3).沒有開啟自動測量
我們先來看看這一塊的程式碼。
if (mHasFixedSize) { mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); return; } // custom onMeasure if (mAdapterUpdateDuringMeasure) { startInterceptRequestLayout(); onEnterLayoutOrScroll(); processAdapterUpdatesAndSetAnimationFlags(); onExitLayoutOrScroll(); if (mState.mRunPredictiveAnimations) { mState.mInPreLayout = true; } else { // consume remaining updates to provide a consistent state with the layout pass. mAdapterHelper.consumeUpdatesInOnePass(); mState.mInPreLayout = false; } mAdapterUpdateDuringMeasure = false; stopInterceptRequestLayout(false); } else if (mState.mRunPredictiveAnimations) { // If mAdapterUpdateDuringMeasure is false and mRunPredictiveAnimations is true: // this means there is already an onMeasure() call performed to handle the pending // adapter change, two onMeasure() calls can happen if RV is a child of LinearLayout // with layout_width=MATCH_PARENT. RV cannot call LM.onMeasure() second time // because getViewForPosition() will crash when LM uses a child to measure. setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight()); return; } if (mAdapter != null) { mState.mItemCount = mAdapter.getItemCount(); } else { mState.mItemCount = 0; } startInterceptRequestLayout(); mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); stopInterceptRequestLayout(false); mState.mInPreLayout = false; // clear
例如上面的程式碼,我將分為2步:
- 如果
mHasFixedSize
為true(也就是呼叫了setHasFixedSize
方法),將直接呼叫LayoutManager
的onMeasure
方法進行測量。 - 如果
mHasFixedSize
為false,同時此時如果有資料更新,先處理資料更新的事務,然後呼叫LayoutManager
的onMeasure
方法進行測量
通過上面的描述,我們知道,如果未開啟自動測量,那麼肯定會呼叫 LayoutManager
的 onMeasure
方法來進行測量,這就是 LayoutManager
的 onMeasure
方法的作用。
至於 onMeasure
方法怎麼進行測量,那就得看 LayoutManager
的實現類。在這裡,我們就不進行深入的追究了。
3. layout
measure
過程分析的差不多了,接下來我們就該分析第二個過程-- layout
。我們來看看 onLayout
方法:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG); dispatchLayout(); TraceCompat.endSection(); mFirstLayoutComplete = true; }
onLayout
方法本身沒有做多少的事情,重點還是在 dispatchLayout
方法裡面。
void dispatchLayout() { if (mAdapter == null) { Log.e(TAG, "No adapter attached; skipping layout"); // leave the state in START return; } if (mLayout == null) { Log.e(TAG, "No layout manager attached; skipping layout"); // leave the state in START return; } mState.mIsMeasuring = false; if (mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() || mLayout.getHeight() != getHeight()) { // First 2 steps are done in onMeasure but looks like we have to run again due to // changed size. mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); } else { // always make sure we sync them (to ensure mode is exact) mLayout.setExactMeasureSpecsFrom(this); } dispatchLayoutStep3(); }
dispatchLayout
方法也是非常的簡單,這個方法保證 RecyclerView
必須經歷三個過程-- dispatchLayoutStep1
、 dispatchLayoutStep2
、 dispatchLayoutStep3
。
同時,在後面的文章中,你會看到 dispatchLayout
方法其實還為 RecyclerView
節省了很多步驟,也就是說,在 RecyclerView
經歷一次完整的 dispatchLayout
之後,後續如果引數有所變化時,可能只會經歷最後的1步或者2步。當然這些都是後話了:joy:。
對於 dispatchLayoutStep1
和 dispatchLayoutStep2
方法,我們前面已經講解了,這裡就不做過多的解釋了。這裡,我們就簡單的看一下 dispatchLayoutStep3
方法吧。
private void dispatchLayoutStep3() { // ······ mState.mLayoutStep = State.STEP_START; // ······ }
為什麼這裡只是簡單看一下 dispatchLayoutStep3
方法呢?因為這個方法主要是做Item的動畫,也就是我們熟知的 ItemAnimator
的執行,而本文不對動畫進行展開,所以先省略動畫部分。
在這裡,我們需要關注 dispatchLayoutStep3
方法的是,它將 mLayoutStep
重置為了 State.STEP_START
。也就是說如果下一次重新開始 dispatchLayout
的話,那麼肯定會經歷 dispatchLayoutStep1
、 dispatchLayoutStep2
、 dispatchLayoutStep3
三個方法。
以上就是 RecyclerView
的layout過程,是不是感覺非常的簡單? RecyclerView
跟其他 ViewGroup
不同的地方在於,如果開啟了自動測量,在 measure
階段,已經將 Children
佈局完成了;如果沒有開啟自動測量,則在 layout
階段才佈局 Children
。
4. draw
接下來,我們來分析三大流程的最後一個階段-- draw
。在正式分析draw過程之前,我先來對 RecyclerView
的 draw
做一個概述。
RecyclerView
分為三步,我們來看看:
- 呼叫
super.draw
方法。這裡主要做了兩件事:1. 將Children
的繪製分發給ViewGroup
;2. 將分割線的繪製分發給ItemDecoration
。 - 如果需要的話,呼叫
ItemDecoration
的onDrawOver
方法。通過這個方法,我們在每個ItemView
上面畫上很多東西。 - 如果
RecyclerView
呼叫了setClipToPadding
,會實現一種特殊的滑動效果-- 每個ItemView可以滑動到padding區域 。
我們來看看這部分的程式碼:
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); } // 第三步 // TODO If padding is not 0 and clipChildrenToPadding is false, to draw glows properly, we // need find children closest to edges. Not sure if it is worth the effort. // ······ }
熟悉三大流程的同學,肯定知道第一步會回撥到 onDraw
方法裡面,也就是說關於 Children
的繪製和 ItemDecoration
的繪製,是在 onDraw
方法裡面。
@Override public void onDraw(Canvas c) { super.onDraw(c); final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDraw(c, this, mState); } }
onDraw
方法是不是非常的簡單?呼叫 super.onDraw
方法將 Children
的繪製分發給 ViewGroup
執行;然後將 ItemDecoration
的繪製分發到 ItemDecoration
的 onDraw
方法裡面去。從這裡,我們可以看出來, RecyclerView
的設計實在是太靈活了!
至於其餘兩步都比較簡單,這裡就不詳細分析了。不過,從這裡,我們終於明白了 ItemDecoration
的 onDraw
方法和 onDrawOver
方法的區別。
5. LayoutManager的onLayoutChildren方法
從整體來說, RecyclerView
的三大流程還是比較簡單,不過在整個過程中,我們似乎忽略了一個過程--那就是 RecyclerView
到底是怎麼 layout children
的?
前面在介紹 dispatchLayoutStep2
方法時,只是簡單的介紹了, RecyclerView
通過呼叫 LayoutManager
的 onLayoutChildren
方法。 LayoutManager
本身對這個方法沒有進行實現,所以必須得看看它的子類,這裡我們就來看看 LinearLayoutManager
。
由於 LinearLayoutManager
的 onLayoutChildren
方法比較長,這裡不可能貼出完整的程式碼,所以這裡我先對這個方法做一個簡單的概述,方便大家理解。
- 確定錨點的資訊,這裡面的資訊包括:1.
Children
的佈局方向,有start和end兩個方向;2.mPosition
和mCoordinate
,分別表示Children
開始填充的position和座標。 - 呼叫
detachAndScrapAttachedViews
方法,detach
掉或者remove
掉RecyclerView
的Children
。這一點本來不在本文的講解範圍內,但是為了後續對RecyclerView
的快取機制有更好的瞭解,這裡特別的提醒一下。 - 根據錨點資訊,呼叫
fill
方法進行Children
的填充。這個過程中根據錨點資訊的不同,可能會呼叫兩次fill
方法。
接下來,我們看看程式碼:
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. // create layout state // ······ // 第一步 final View focused = getFocusedChild(); if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION || mPendingSavedState != null) { mAnchorInfo.reset(); mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; // calculate anchor position and coordinate updateAnchorInfoForLayout(recycler, state, mAnchorInfo); mAnchorInfo.mValid = true; } // ······ // 第二步 detachAndScrapAttachedViews(recycler); mLayoutState.mIsPreLayout = state.isPreLayout(); // 第三步 if (mAnchorInfo.mLayoutFromEnd) { // fill towards start updateLayoutStateToFillStart(mAnchorInfo); mLayoutState.mExtra = extraForStart; fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; final int firstElement = mLayoutState.mCurrentPosition; if (mLayoutState.mAvailable > 0) { extraForEnd += mLayoutState.mAvailable; } // fill towards end updateLayoutStateToFillEnd(mAnchorInfo); mLayoutState.mExtra = extraForEnd; mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; if (mLayoutState.mAvailable > 0) { // end could not consume all. add more items towards start extraForStart = mLayoutState.mAvailable; updateLayoutStateToFillStart(firstElement, startOffset); mLayoutState.mExtra = extraForStart; fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; } } else { // fill towards end updateLayoutStateToFillEnd(mAnchorInfo); mLayoutState.mExtra = 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.mExtra = 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.mExtra = extraForEnd; fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; } } // ······ }
相信從上面的程式碼都可以找出每一步的執行。現在,我們來詳細分析每一步。首先來看第一步-- 確定錨點的資訊
。
要想看錨點資訊的計算過程,我們可以從 updateAnchorInfoForLayout
方法裡面來找出答案,我們來看看 updateAnchorInfoForLayout
方法:
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; }
我相信通過上面的程式碼註釋,大家都能明白 updateAnchorInfoForLayout
方法到底幹了嘛,這裡我簡單分析一下這三種確定所做的含義,具體是怎麼做的,這裡就不討論,因為這裡面的細節太多了,深入的討論容易將我們聰明無比的大腦搞暈:joy:。
- 第一種計算方式,表示含義有兩種:1.
RecyclerView
被重建,期間回調了onSaveInstanceState
方法,所以目的是為了恢復上次的佈局;2.RecyclerView
呼叫了scrollToPosition
之類的方法,所以目的是讓
RecyclerView
滾到準確的位置上去。所以,錨點的資訊根據上面的兩種情況來計算。 - 第二種計算方法,從
Children
上面來計算錨點資訊。這種計算方式也有兩種情況:1. 如果當前有擁有焦點的Child
,那麼有當前有焦點的Child的位置來計算錨點;2. 如果沒有child擁有焦點,那麼根據佈局方向(此時佈局方向由mLayoutFromEnd
來決定)獲取可見的第一個ItemView
或者最後一個ItemView
。 - 如果前面兩種方式都計算失敗了,那麼採用第三種計算方式,也就是預設的計算方式。
以上就是 updateAnchorInfoForLayout
方法所做的事情,這裡就不詳細糾結每種計算方式的細節,有興趣的同學可以看看。
至於第二步,呼叫 detachAndScrapAttachedViews
方法對所有的 ItemView
進行回收,這部分的內容屬於 RecyclerView
快取機制的部分,本文先在這裡埋下一個伏筆,後續專門講解 RecyclerView
會詳細的分析它,所以這裡就不講解了。
接下來我們來看看第三步,也就是呼叫 fill
方法來填充 Children
。在正式分析填充過程時,我們先來看一張圖片:

圖片的原圖出自 RecyclerView剖析 ,如有侵權,請聯絡我。
上圖形象的展現出三種 fill
的情況。其中,我們可以看到第三種情況, fill
方法被呼叫了兩次。
fill
方法:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { // ······ while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { // ······ layoutChunk(recycler, state, layoutState, layoutChunkResult); } // ······ }
fill
方法的程式碼比較長,其實都是來計算可填充的空間,真正填充 Child
的地方是 layoutChunk
方法。我們來看看 layoutChunk
方法。
由於 layoutChunk
方法比較長,這裡我就不完整的展示,為了方便理解,我對這個方法做一個簡單的概述,讓大家有一個大概的理解。
- 呼叫
LayoutState
的next
方法獲得一個ItemView
。千萬別小看這個next
方法,RecyclerView
快取機制的起點就是從這個方法開始,可想而知,這個方法到底為我們做了多少事情。 - 如果
RecyclerView
是第一次佈局Children的話(layoutState.mScrapList == null
為true),會先呼叫addView,將View
新增到RecyclerView
裡面去。 - 呼叫
measureChildWithMargins
方法,測量每個ItemView
的寬高。注意這個方法測量ItemView的寬高考慮到了兩個因素:1.margin屬性;2.ItemDecoration
的offset
。 - 呼叫
layoutDecoratedWithMargins
方法,佈局ItemView
。這裡也考慮上面的兩個因素的。
至於每一步具體幹了嘛,這裡就不詳細的解釋,都是一些基本操作,有興趣的同學可以看看。
綜上所述,便是 LayoutManager
的 onLayoutChildren
方法整個執行過程,思路還是比較簡單的。
6. 總結
本文到此就差不多了,在最後,我做一個簡單的總結。
-
RecyclerView
的measure
過程分為三種情況,每種情況都有執行過程。通常來說,我們都會走自動測量的過程。 - 自動測量裡面需要分清楚
mState.mLayoutStep
狀態值,因為根據不同的狀態值呼叫不同的dispatchLayoutStep
方法。 -
layout
過程也根據mState.mLayoutStep
狀態來呼叫不同的dispatchLayoutStep
方法 -
draw
過程主要做了四件事:1.繪製ItemDecoration
的onDraw
部分;2.繪製Children
;3.繪製ItemDecoration
的drawOver
部分;4. 根據mClipToPadding
的值來判斷是否進行特殊繪製。