【進階】RecyclerView原始碼解析(一)——繪製流程
引言
自從Google出了RecyclerView後,基本上列表的場景已經完全替代了原來的ListView和GridView,現在不僅僅是列表,多樣式(俗稱蓋樓),複雜頁面等,只要我們願意,RecyclerView幾乎可以代替實現80%的佈局,GitHub可以發現各種各樣給予RecyclerView的開源庫,無論是Adapter還是LayoutManager等。阿里對應出的vlayout其實也是RecyclerView的深度拓展,所以最近感覺僅僅瞭解用法是不夠的,使用RecyclerView越多,就越會發現這個元件的深度不能僅僅停留在使用層面,從RecyclerView可以延伸出一系列進階的使用、拓展、優化、封裝等,所以打算從原始碼角度來看一下RecyclerView的實現機制,更方便我們進行進階學習使用RecyclerView。
帶著問題看原始碼
最早的看原始碼可能帶著一點強迫和模仿性質,大家都在看原始碼都在分析原始碼,雖然不知道看原始碼能幹什麼,但是就是看就夠了,所以挑選的也是大家都在分析的Volley,所以前面看的幾個框架的原始碼從Volley到最近的OkHttp的原始碼,總感覺是為了看原始碼而看原始碼,雖然最後看完後收穫確實很多,但是感覺方式總是不是特別正確,這次的RecyclerView的分析想法卻不是這樣的,由於對RecyclerView使用的程度和頻率越來越多,越來越多的不解和疑問讓我想要去看一看RecyclerView的原始碼,總的疑惑有下面幾個:
* 1.首先沒有見過特別詳細的RecyclerView的原始碼分析系列,所有關於RecyclerView都是停留在使用或者少數進階使用的部落格
2.RecyclerView,LayoutManager,Adapter,ViewHolder,ItemDecoration這些和RecycleView使用息息相關的類到底是什麼關係
3.RecyclerView作為列表,繪製流程到底什麼樣的
4.RecyclerView有什麼不常用的進階使用方式,但是卻很適合RecyclerView作為很“重”的元件的優化,像setRecyclerPool用處到底是什麼
5.大家都只要要使用RecyclerView替代ListView和GridView,好用,都在用,但是都沒有追究到底這背後的原因到底是什麼,RecyclerView到底比ListView好在哪裡,到底該不該替換,效能
以上問題不一定看完原始碼都能解決(個人能力堪憂啊…),但是帶著問題來看原始碼總會有不錯的收穫,或者大家有什麼關於使用RecyclerView的學習部落格連結,使用經驗和體會都可以在下面評論供大家一起學習討論。
思路
當開啟RecyclerVew的原始碼會發現非常的複雜,感覺無從下手,不會像Volley和OkHttp從發起開始到發起結束,目標性那麼明確。那麼既然RecyclerView繼承的是ViewGroup,也就是說RecyclerView其實就是和LinearLayout等佈局一樣的一個自定義ViewGroup。
既然涉及到自定義元件,那麼當我們自己來實現一個自定義ViewGroup,最重要的步驟無非是下面幾點:
1. 重寫onMeasure用於確定自定義ViewGroup的大小
2. 重寫onLayout用於佈局子view的位置
所以原始碼的分析也對應從這裡開始進行分析。
onMeasure
和分析OkHttp的原始碼一樣,這裡就不放上RecyclerView中的onMeasure的所有原始碼了,那樣只會增加我們的理解難度,所以同樣選擇利用虛擬碼進行分析。
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
//layoutManager沒有設定的話,直接走default的方法,所以會為空白
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.mAutoMeasure) {
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
//如果測量是絕對值,則跳過measure過程直接走layout
if (skipMeasure || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
//mLayoutStep預設值是 State.STEP_START
dispatchLayoutStep1();
//執行完dispatchLayoutStep1()後是State.STEP_LAYOUT
}
..........
//真正執行LayoutManager繪製的地方
dispatchLayoutStep2();
//執行完後是State.STEP_ANIMATIONS
..........
//寬高都不確定的時候,會繪製兩次
// 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()) {
..........
dispatchLayoutStep2();
.......... }
} else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
..........
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
..........
mState.mInPreLayout = false; // clear
}
}
原始碼放上了,現在可以一步一步分析onMeasure的過程了,先從第一個小點開始吧。
if (mLayout == null) {
//layoutManager沒有設定的話,直接走default的方法,所以會為空白
defaultOnMeasure(widthSpec, heightSpec);
return;
}
這裡的mLayout其實就是我們給RecyclerView設定的LayoutManager物件,這一段程式碼其實就很好的解釋了為什麼我們有時候初次使用RecyclerView的時候忘記設定LayoutManager後,RecyclerView會沒有按照我們所想的那樣顯示出來。
這裡可以看到,如果mLayout為null的話,會走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);
}
可以看到這裡的chooseSize方法其實就是更加寬高的Mode得到相應的值後直接呼叫setMeasuredDimension(width, height)設定寬高了,可以發現這裡其實是沒有進行child的測量就直接return結束了onMeasure過程的,這也就解釋了為什麼我們沒有設定LayoutManager會導致顯示空白了。
接下來技術一個判斷
if (mLayout.mAutoMeasure) {
}else{
}
這裡其實mAutoMeasure這個值,LinearLayoutManager還是其他兩個Manager,預設值都是true。所以接著往下看。
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
//如果測量是絕對值,則跳過measure過程直接走layout
if (skipMeasure || mAdapter == null) {
return;
}
這裡解釋的很清楚,如果寬和高的測量值是絕對值時,直接跳過onMeasure方法。那這裡可能有疑問了,如果沒有執行onMeasure方法,那麼子View沒有繪製,會造成空白的情況,但是實際情況是當我們給RecyclerView設定絕對值大小的時候,子View仍可以正常繪製出來。這個問題後面會解答。(onLayout裡會執行子View的繪製)
if (mState.mLayoutStep == State.STEP_START) {
//mLayoutStep預設值是 State.STEP_START
dispatchLayoutStep1();
//執行完dispatchLayoutStep1()後是State.STEP_LAYOUT
}
接下來就要開始繪製的準備了,這裡可以看到首先判斷mLayoutStep,這裡mLayoutStep的預設值其實就是 State.STEP_START,並且每次繪製流程結束後,會重置為 State.STEP_START。接下來執行dispatchLayoutStep1();方法dispatchLayoutStep1();其實沒有必要過多分析,因為分析原始碼主要是對於繪製思想的理解,如果過多的糾結於每一行程式碼的含義,那麼會陷入很大的困擾中。這裡就放上官方對於dispatchLayoutStep1();的註釋吧。(順道翻譯一下)
/**
* The first step of a layout where we;
* - process adapter updates
* - decide which animation should run
* - save information about current views
* - If necessary, run predictive layout and save its information
*/
/**
* 1.處理Adapter的更新
* 2.決定那些動畫需要執行
* 3.儲存當前View的資訊
* 4.如果必要的話,執行上一個Layout的操作並且儲存他的資訊
*/
接下來就是我們的真正執行LayoutManager繪製的地方dispatchLayoutStep2()。
private void dispatchLayoutStep2() {
....
//重寫的getItemCount方法
mState.mItemCount = mAdapter.getItemCount();
....
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
....
}
同樣的,這裡放上虛擬碼,便於理解。
這裡注意兩個地方,第一個mAdapter.getItemCount(),可以看到我們每次重寫Adapter時重寫的方法getItemCount方法用到的地方了。
第二個,可以看到mLayout.onLayoutChildren(mRecycler, mState);這個方法,為什麼說RecyclerView將View的繪製交給了LayoutManager,這裡就是最有力的體現,可以看到,這裡將RecycleView內部持有的Recycler和state傳給了LayoutManager的onLayoutChildren方法,單從方法的名字其實就可以看出。這裡我們進入LayoutManager裡看一看。(本次分析給予LinearLayoutManager)
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
....
// resolve layout direction
//判斷繪製方向,給mShouldReverseLayout賦值,預設是正向繪製,則mShouldReverseLayout是false
resolveShouldLayoutReverse();
final View focused = getFocusedChild();
//mValid的預設值是false,一次測量之後設為true,onLayout完成後會回撥執行reset方法,又變為false
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
|| mPendingSavedState != null) {
....
//mStackFromEnd預設是false,除非手動呼叫setStackFromEnd()方法,兩個都會false,異或則為false
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
//計算錨點的位置和偏移量
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
....
} else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
>= mOrientationHelper.getEndAfterPadding()
|| mOrientationHelper.getDecoratedEnd(focused)
<= mOrientationHelper.getStartAfterPadding())) {
....
}
....
//mLayoutFromEnd為false
if (mAnchorInfo.mLayoutFromEnd) {
//倒著繪製的話,先往上繪製,再往下繪製
// fill towards start
// 從錨點到往上
updateLayoutStateToFillStart(mAnchorInfo);
....
fill(recycler, mLayoutState, state, false);
....
// 從錨點到往下
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
....
//調兩遍fill方法
fill(recycler, mLayoutState, state, false);
....
if (mLayoutState.mAvailable > 0) {
// end could not consume all. add more items towards start
....
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
....
}
} else {
//正常繪製流程的話,先往下繪製,再往上繪製
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
....
fill(recycler, mLayoutState, state, false);
....
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
....
fill(recycler, mLayoutState, state, false);
....
if (mLayoutState.mAvailable > 0) {
....
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
....
fill(recycler, mLayoutState, state, false);
....
}
}
....
layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
//完成後重置引數
if (!state.isPreLayout()) {
mOrientationHelper.onLayoutComplete();
} else {
mAnchorInfo.reset();
}
mLastStackFromEnd = mStackFromEnd;
}
這裡雖然已經儘量刪減了很多程式碼,但是還是很多…但是其實原理理解起來還是比較容易的。簡單的說其實可以總結縮略為:
先尋找頁面當前的錨點
以這個錨點未基準,向上和向下分別填充
填充完後,如果還有剩餘的可填充大小,再填充一次
這樣理解起來就比較容易了,接下來我們就一步一步來看一下原始碼。
尋找錨點
resolveShouldLayoutReverse();
final View focused = getFocusedChild();
//mValid的預設值是false,一次測量之後設為true,onLayout完成後會回撥執行reset方法,又變為false
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
|| mPendingSavedState != null) {
....
//mStackFromEnd預設是false,除非手動呼叫setStackFromEnd()方法,兩個都會false,異或則為false
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
//計算錨點的位置和偏移量
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
....
} else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
>= mOrientationHelper.getEndAfterPadding()
|| mOrientationHelper.getDecoratedEnd(focused)
<= mOrientationHelper.getStartAfterPadding())) {
....
}
首先執行的resolveShouldLayoutReverse();方法,從方法的命名上可以理解為是否需要倒著繪製
//判斷繪製方向,給mShouldReverseLayout賦值,預設是正向繪製,則mShouldReverseLayout是false
private void resolveShouldLayoutReverse() {
// A == B is the same result, but we rather keep it readable
if (mOrientation == VERTICAL || !isLayoutRTL()) {
//預設mReverseLayout是false,建構函式,可以通過setReverseLayout來設定
mShouldReverseLayout = mReverseLayout;
} else {
mShouldReverseLayout = !mReverseLayout;
}
}
可以看到這裡我註釋寫的很清楚,如果我們沒有手動呼叫setReverseLayout()方法,預設情況下,是不會倒著繪製的。
接下來對於幾個變數的註釋這裡解釋一下。
首先是mAnchorInfo.mValid,這裡mAnchorInfo就是我們要的錨點。mValid的預設值是false,一次測量之後設為true,onLayout完成後會回撥執行reset方法,又變為false
再就是mAnchorInfo.mLayoutFromEnd。
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
可以看到這裡用到了位運算子^異或,前面已經分析了,mShouldReverseLayout預設是fasle的,mStackFromEnd預設是false,除非手動呼叫setStackFromEnd()方法,兩個都會false,異或則為false。
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
接下來這個方法就是對於錨點位置的確定,裡面其實就是對於當前狀態的偏移量的計算,得出當前的錨點位置,具體比較複雜,這裡就不做分析了。後面就是繪製的地方,主要就是兩種方向,正向(先向上再向下),逆向(先向下再向上),所以這裡我們就看平常的情況。
{
//正常繪製流程的話,先往下繪製,再往上繪製
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
....
fill(recycler, mLayoutState, state, false);
....
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
....
fill(recycler, mLayoutState, state, false);
....
if (mLayoutState.mAvailable > 0) {
....
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
....
fill(recycler, mLayoutState, state, false);
....
}
可以看到這裡有兩種方法。
1.updateLayoutStateToFill…()
2.fill()
第一個方法其實就是確定當前方向上錨點的相關的狀態資訊。
這裡最主要的就是第二個方法fill(),可以看到這裡至少呼叫了兩次fill()方法,當還有剩餘可以繪製的時候會再調一次fill()方法。這也證明了我們的想法,是通過錨點分別向上和向下兩次繪製。這裡放上一張圖便於理解
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
.....
layoutChunk(recycler, state, layoutState, layoutChunkResult);
.....
return start - layoutState.mAvailable;
}
這裡fill其實最重要的就是看這裡的layoutChunk(recycler, state, layoutState, layoutChunkResult)方法。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//next方法很重要
View view = layoutState.next(recycler);
if (view == null) {
if (DEBUG && layoutState.mScrapList == null) {
throw new RuntimeException("received null view when unexpected");
}
...
}
LayoutParams params = (LayoutParams) view.getLayoutParams();
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);
}
}
//測量ChildView
measureChildWithMargins(view, 0, 0);
......
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
//layout Child
layoutDecoratedWithMargins(view, left, top, right, bottom);
......
}
這個方法其實我是不忍註釋的,滿滿的乾貨啊,首先是我們的next方法,千萬不要因為這個next短小不起眼,就認為不重要。我只能說這個方法很重要。
View next(RecyclerView.Recycler recycler) {
//預設mScrapList=null,但是執行layoutForPredictiveAnimations方法的時候不會為空
if (mScrapList != null) {
return nextViewFromScrapList();
}
//重要,從recycler獲得View,mScrapList是被LayoutManager持有,recycler是被RecyclerView持有
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
第一個mSrapList其實預設是空的,只有執行layoutForPredictiveAnimations前不為空,執行完後又變為空,所以這裡暫時不需要考慮。
可以看到View view = recycler.getViewForPosition(mCurrentPosition)終於看到RecyclerView中快取策略的身影,RecyclerView的快取不會在這篇部落格講解,但是這裡不得不讓我們注意
下面從流程上簡單的看一下View view = recycler.getViewForPosition(mCurrentPosition);這個方法
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
/**
* Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
* cache, the RecycledViewPool, or creating it directly.
*/
/**
* 註釋寫的很清楚,從Recycler的scrap,cache,RecyclerViewPool,或者直接create建立
*/
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//一堆判斷之後,如果不成立
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
可以看到這裡,getViewForPosition會呼叫tryGetViewHolderForPositionByDeadline方法,tryGetViewHolderForPositionByDeadline方法的註釋寫的很清楚從Recycler的scrap,cache,RecyclerViewPool,或者直接create建立,這裡我們也看到了我們最熟悉的mAdapter.createViewHolder(RecyclerView.this, type); 方法!
關於RecyclerView的快取策略不出意外應該會在下篇部落格進行分析
next()方法分析結束後,其實就比較容易了。
LayoutParams params = (LayoutParams) view.getLayoutParams();
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);
}
}
剩下的就是RecyclerView的addView方法。新增完View後會呼叫
//測量ChildView
measureChildWithMargins(view, 0, 0);
//----------------------------------------------------------
public void measureChildWithMargins(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)) {
//子View的測量
child.measure(widthSpec, heightSpec);
}
}
從這個方法裡我們看到了子View的測量,當然還有一個需要我們注意的地方那就是mRecyclerView.getItemDecorInsetsForChild(child)
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
}
if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
// changed/invalid items should not be updated until they are rebound.
return lp.mDecorInsets;
}
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
//getItemOffsets()實現分割線的回撥方法!
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
其實可以看到這裡在測量子View的時候是將我們實現自定義分割線重寫的getItemOffsets方法。這裡其實也就可以理解了自定義分割線的原理就是在子View的測量過程前給上下左右加上自定義分割線所對應設定給這個child的邊距。
測量完成後,緊接著就呼叫了layoutDecoratedWithMargins(view, left, top, right, bottom)對子View完成了layout。
public void layoutDecoratedWithMargins(View child, int left, int top, int right,
int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = lp.mDecorInsets;
//layout
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}
終於到此,我們對於onMeasure方法分析結束了,這裡分析完成對於後面的onLayout的分析就比較簡單了。
onLayout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
` ...
dispatchLayout();
...
}
void dispatchLayout() {
....
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
...
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.
...
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
這裡的程式碼就比較好理解了,並且上面提到的問題也就迎刃而解了,當我們給RecyclerView設定固定的寬高的時候,onMeasure是直接跳過了執行,那麼為什麼子View仍然能繪製出來。
這裡可以看到,如果onMeasure沒有執行,mState.mLayoutStep == State.STEP_START就成立,所以仍然會執行 dispatchLayoutStep1(), dispatchLayoutStep2();也就對應的會繪製子View。
而後面的註釋也比較清楚,由於我們在Layout的時候改變了寬高,也會導致dispatchLayoutStep2();,也就是子View的重新繪製。
如果上面情況都沒有,那麼onLayout的作用就僅僅是dispatchLayoutStep3(),而 dispatchLayoutStep3()方法的作用除了重置一些引數,外還和執行動畫有關。
private void dispatchLayoutStep3() {
//重置引數
if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
// traverse list in reverse because we may call animateChange in the loop which may
// remove the target view holder.
//需要動畫的情況。找出ViewHolder現在的位置,並且處理改變動畫。最後觸發動畫。
}
// Step 4: Process view info lists and trigger animations
mViewInfoStore.process(mViewInfoProcessCallback);
}
//成功回撥
}
到此!!!對於RecyclerView的繪製流程其實我們有了一個大體的瞭解,總結一下關鍵點:
1.RecyclerView是將繪製流程交給LayoutManager處理,如果沒有設定不會測量子View。
2.繪製流程是區分正向繪製和倒置繪製。
3.繪製是先確定錨點,然後向上繪製,向下繪製,fill()至少會執行兩次,如果繪製完還有剩餘空間,則會再執行一次fill()方法。
4.LayoutManager獲得View是從RecyclerView中的Recycler.next()方法獲得,涉及到RecyclerView的快取策略,如果快取沒有拿到,則走我們自己重寫的onCreateView方法。
5.如果RecyclerView寬高沒有寫死,onMeasure就會執行完子View的measure和Layout方法,onLayout僅僅是重置一些引數,如果寫死,子View的measure和layout會延後到onLayout中執行。