RecyclerView 原始碼分析(一)
轉載自瓊珶和予
RecyclerView 原始碼分析(一)
RecyclerView的三大流程
本系列文章樓主打算從幾個地方說起。先是將RecyclerView當成一個普通的View,分別分析它的三大流程、事件傳遞(包括巢狀滑動)
;然後是分析RecyclerView的快取原理
,這也是RecyclerView的精華所在;然後分析的是RecyclerView的Adapter、LayoutManager、ItemAnimator和ItemDecoration
。最後就是RecyclerView的擴充套件
,包括LayoutManager的自定義和使用RecyclerView常見的坑
等。
1. 概述
在分析RecyclerView原始碼之前,我們還是對RecyclerView有一個初步的瞭解,簡單的瞭解它是什麼,它的基本結構有哪些。
RecyclerView是Google爸爸在2014年的IO大會提出來。但是在實際開發中,自從有了RecyclerView,ListView和GridView就很少用了,所以我們暫且認為RecyclerView的目的是替代ListView和GridView。
RecyclerView本身是一個展示大量資料的控制元件,相比較ListView,RecyclerView的4級快取(也有人說是3級快取,這些都不重要)就表現的非常出色,在效能方面相比於ListView提升了不少。同時由於LayoutManager的存在,讓RecyclerView不僅有ListView的特點,同時兼有GridView的特點。這可能是RecyclerView受歡迎的原因之一吧。
RecyclerView在設計方面上也是非常的靈活,不同的部分承擔著不同的職責。其中Adapter負責提供資料,包括建立ViewHolder和繫結資料,LayoutManager負責ItemView的測量和佈局,ItemAnimator負責每個ItemView的動畫,ItemDecoration負責每個ItemView的間隙。這種插拔式的架構使得RecyclerView變得非常的靈活,每一個人都可以根據自身的需求來定義不同的部分。
正因為這種插拔式的設計,使得RecyclerView在使用上相比較於其他的控制元件稍微難那麼一點點,不過這都不算事,誰叫RecyclerView這麼惹人愛呢。
好了,好像廢話有點多,現在我們正式來分析原始碼吧,本文的重點是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方法。這一點,我們必須清楚,否則接下來的程式碼將難以理解。
好了,前戲準備的差不多,現在應該進入高潮了。我們開始正式的分析原始碼了。
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方法裡面究竟做了什麼,這在前面已經介紹過了,這裡就不再介紹了。
我還是再貼一下defaultOnMeasure裡面做了什麼吧,先是通過LayoutManager的chooseSize方法來計算值,然後就是setMeasuredDimension方法來設定寬高。
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();
dispatchLayoutStep1
如果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
接下來我們看看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步。當然這些都是後話了。
對於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方法到底幹了嘛,這裡我簡單分析一下這三種確定所做的含義,具體是怎麼做的,這裡就不討論,因為這裡面的細節太多了,深入的討論容易將我們聰明無比的大腦搞暈。
- 第一種計算方式,表示含義有兩種:1.
RecyclerView被重建,期間回撥了onSaveInstanceState方法,所以目的是為了恢復上次的佈局
;2.RecyclerView呼叫了scrollToPosition之類的方法,所以目的是讓RecyclerView滾到準確的位置上去
。所以,錨點的資訊根據上面的兩種情況來計算。 - 第二種計算方法,
從Children上面來計算錨點資訊
。這種計算方式也有兩種情況:1.如果當前有擁有焦點的Child,那麼有當前有焦點的Child的位置來計算錨點
;2.如果沒有child擁有焦點,那麼根據佈局方向(此時佈局方向由mLayoutFromEnd來決定)獲取可見的第一個ItemView或者最後一個ItemView
。 - 如果前面兩種方式都計算失敗了,那麼採用第三種計算方式,也
就是預設的計算方式
。
以上就是updateAnchorInfoForLayout方法所做的事情,這裡就不詳細糾結每種計算方式的細節,有興趣的同學可以看看。
detachAndScrapAttachedViews
至於第二步,呼叫detachAndScrapAttachedViews方法對所有的ItemView進行回收,這部分的內容屬於RecyclerView快取機制的部分,本文先在這裡埋下一個伏筆,後續專門講解RecyclerView會詳細的分析它,所以這裡就不講解了。
呼叫fill方法來填充Children
接下來我們來看看第三步,也就是呼叫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過程主要做了四件事:
- 繪製ItemDecoration的onDraw部分;
- 繪製Children;
- 繪製ItemDecoration的drawOver部分;
- 根據mClipToPadding的值來判斷是否進行特殊繪製。
相關文章
- RecyclerView 原始碼分析(一) —— 繪製流程解析View原始碼
- RecyclerView之SnapHelper原始碼分析View原始碼
- RecyclerView 原始碼分析(二) —— 快取機制View原始碼快取
- RecyclerView原始碼解析View原始碼
- RecyclerView動畫原始碼淺析View動畫原始碼
- RecyclerView用法和原始碼深度解析View原始碼
- 深度 Mybatis 3 原始碼分析(一)SqlSessionFactoryBuilder原始碼分析MyBatis原始碼SQLSessionUI
- 原始碼|jdk原始碼之HashMap分析(一)原始碼JDKHashMap
- MyBatis原始碼分析(一)MyBatis原始碼
- preact原始碼分析(一)React原始碼
- Redux原始碼分析(一)Redux原始碼
- AFL原始碼分析(一)原始碼
- 原始碼分析一:EventBus原始碼
- Backbone原始碼分析(一)原始碼
- Cobar 原始碼分析(一)原始碼
- YYCache 原始碼分析(一)原始碼
- LinkedList原始碼分析(一)原始碼
- DelayQueue系列(一):原始碼分析原始碼
- Retrofit原始碼分析三 原始碼分析原始碼
- 《YYModel原始碼分析(一)YYClassInfo》原始碼
- Netty原始碼分析--NIO(一)Netty原始碼
- DataTable.js原始碼分析(一)JS原始碼
- Volcano 原理、原始碼分析(一)原始碼
- Retrofit原始碼分析一 概覽原始碼
- AFNetworking 原始碼分析(一)原始碼
- Tinker接入及原始碼分析(一)原始碼
- Zookeeper原始碼分析(一) ----- 原始碼執行環境搭建原始碼
- Spring事務原始碼分析專題(一)JdbcTemplate使用及原始碼分析Spring原始碼JDBC
- 集合原始碼分析[2]-AbstractList 原始碼分析原始碼
- 集合原始碼分析[1]-Collection 原始碼分析原始碼
- 集合原始碼分析[3]-ArrayList 原始碼分析原始碼
- Guava 原始碼分析之 EventBus 原始碼分析Guava原始碼
- RxJava2原始碼分析(一):基本流程分析RxJava原始碼
- jQuery 原始碼分析第一篇之入口原始碼jQuery原始碼
- 深入OKHttp原始碼分析(一)----同步和非同步請求流程和原始碼分析HTTP原始碼非同步
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- 以太坊原始碼分析(36)ethdb原始碼分析原始碼