前言
對于一個Android開發(fā)者來說摧冀,RecyclerView應(yīng)該是日常開發(fā)中使用最頻繁的控件之一了吧配椭。自從谷歌在開發(fā)者大會上推出它以后,從前用來展示列表的控件ListView和悦、GridView等就不再那么的受寵了赋焕,因為RecyclerView相比它們來說,實在是強(qiáng)大和好用多了再菊。而究竟是什么原因讓RecyclerView如此的受歡迎爪喘,這就需要我們走進(jìn)它的源碼,來了解一下它的實現(xiàn)原理(本文屬于RecyclerView進(jìn)階學(xué)習(xí)纠拔,不會介紹其基本的使用方式秉剑,因此需要對RecyclerView的使用方式有著基本的了解)。文章將按照以下幾個章節(jié)進(jìn)行分析:
1.RecyclerView的定義稠诲。
2.從源碼分析RecyclerView的繪制流程侦鹏。
3.從源碼理解RecyclerView的緩存機(jī)制。
4.LinearLayoutManager源碼分析臀叙。
走進(jìn)源碼
1.定義:
既然RecyclerView是一個控件略水,那么它要么繼承自View,要么繼承自ViewGroup劝萤。源碼中關(guān)于它的定義如下:
一種靈活的視圖渊涝,提供一個有限的窗口展示大型的數(shù)據(jù)集合。
說到集合床嫌,說明它肯定不單單只可以展示一條數(shù)據(jù)跨释,因此我們可以推測RecyclerView是繼承自ViewGroup的。現(xiàn)實也是如此厌处,RecyclerView的確是繼承自ViewGroup的鳖谈,那么首先我們就要對它的繪制流程有一個基本的了解,看看它究竟是如何將每一個條目itemView繪制到屏幕上的(這里要求對View的繪制流程有一定的了解阔涉,可以參考我前面的文章 Android 自定義View--從源碼理解View的繪制流程)缆娃。
2.繪制流程:
2.1.onMeasure:
首先看一下RecyclerView的measure流程,這里貼出它的onMeasure方法的源碼(請留意源碼中的注釋):
protected void onMeasure(int widthSpec, int heightSpec) {
1 if (mLayout == null) {
2 defaultOnMeasure(widthSpec, heightSpec);
3 return;
4 }
5 if (mLayout.isAutoMeasureEnabled()) {
6 final int widthMode = MeasureSpec.getMode(widthSpec);
7 final int heightMode = MeasureSpec.getMode(heightSpec);
// LayoutManager中的onMeasure方法內(nèi)部最終也是調(diào)用剛剛的defaultOnMeasure方法瑰排;
// 之所以沒有直接調(diào)用defaultOnMeasure方法是因為可能會破壞現(xiàn)有的一些三方代碼贯要;
8 mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
9 final boolean measureSpecModeIsExactly =
10 widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
11 if (measureSpecModeIsExactly || mAdapter == null) {
12 return;
13 }
// mState是一個State類型的成員變量,它的mLayoutStep變量默認(rèn)值為State.STEP_START
// State類是RecyclerView的一個內(nèi)部類椭住,它保存一些關(guān)于RecyclerView的有用信息
14 if (mState.mLayoutStep == State.STEP_START) {
15 dispatchLayoutStep1(); // 布局流程第一步
16 }
17 mLayout.setMeasureSpecs(widthSpec, heightSpec);
18 mState.mIsMeasuring = true;
19 dispatchLayoutStep2(); // 布局流程第二步
// 通過子View來獲取RecyclerView的寬度和高度
20 mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// 如果RecyclerView有不確切的寬度和高度并且至少有一個子View也有不確切的寬度和高度郭毕,我們必須重新測量。
21 if (mLayout.shouldMeasureTwice()) {
22 mLayout.setMeasureSpecs(
23 MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
24 MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
25 mState.mIsMeasuring = true;
26 dispatchLayoutStep2();
// now we can get the width and height from the children.
27 mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
28 }
29 } else {
30 ........
}
}
在這個方法中函荣,首先我們要知道的是mLayout這個變量的含義显押,它就是我們設(shè)置給RecyclerView的LayoutManager扳肛。而通常情況下我們使用的都是LinearLayoutManager或GridLayoutManager(繼承自LinearLayoutManager),在LinearLayoutManager中乘碑,isAutoMeasureEnabled方法的返回值為true挖息,因此這里略去了else(代碼第30行)中的邏輯,只分析一般場景下的measure流程兽肤。
首先套腹,在第一行判斷mLayout是否為空,如果為空资铡,就執(zhí)行defaultOnMeasure方法电禀,然后調(diào)用return結(jié)束onMeasure方法。而在defaultOnMeasure方法中笤休,其實就是調(diào)用LayoutManager中的chooseSize方法尖飞,根據(jù)當(dāng)前RecyclerView寬度和高度的測量模式來分別獲取寬和高的尺寸值,然后調(diào)用View中的setMeasuredDimension方法將測量出的寬和高的尺寸值保存店雅。其方法的源碼如下:
void defaultOnMeasure(int widthSpec, int heightSpec) {
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);
}
當(dāng)mLayout不為空時進(jìn)入if條件語句政基,在第8行調(diào)用LayoutManager的onMeasure方法保存測量出的寬和高的尺寸值;然后在第11行進(jìn)行判斷闹啦,如果寬和高的測量模式都為EXACTLY模式或者Adapter為空沮明,調(diào)用return結(jié)束onMeasure方法;如果不滿足繼續(xù)向下執(zhí)行到第14行窍奋,如果mState.mLayoutStep的值為State.STEP_START荐健,就執(zhí)行dispatchLayoutStep1方法,關(guān)于dispatchLayoutStep1方法琳袄,它的方法源碼有點長并且邏輯很復(fù)雜江场,這里只貼出源碼中針對此方法的注釋:
該方法是布局流程的第一步,首先進(jìn)行適配器的更新挚歧,決定應(yīng)該執(zhí)行哪個動畫,然后保存當(dāng)前視圖的信息吁峻,如果有必要的話執(zhí)行先前的布局操作并且保存它的信息滑负。
根據(jù)源碼注釋我們可以得知,其實在dispatchLayoutStep1方法中的主要操作就是更新適配器中的內(nèi)容確保即將繪制到屏幕上的視圖信息的準(zhǔn)確性用含,并且保存當(dāng)前視圖的信息矮慕,在方法的最后一步mState.mLayoutStep的值將被置為State.STEP_LAYOUT;接下來在第17行會調(diào)用LayoutManager的setMeasureSpecs方法將寬和高的測量模式和測量尺寸在LayoutManager中保存一份啄骇;然后再向下執(zhí)行至19行痴鳄,調(diào)用dispatchLayoutStep2方法,這個方法是布局流程的第二步缸夹,這里我們依然只貼出源碼中關(guān)于該方法的注釋:
在第二個布局步驟中痪寻,我們對最終狀態(tài)的視圖進(jìn)行實際布局螺句;如果需要,這個步驟可以運(yùn)行多次橡类。
在這個方法中蛇尚,會對RecyclerView進(jìn)行實際的布局操作,而在前面分析View的繪制流程的文章中我們知道顾画,布局流程的實質(zhì)就是ViewGroup類型的父布局來確定它的每一個子View在布局中的位置取劫。而在dispatchLayoutStep2方法的內(nèi)部,會調(diào)用LayoutManager的onLayoutChildren方法來進(jìn)行RecyclerView的實際布局操作研侣,也就是說RecyclerView的布局流程是由LayoutManager完成的谱邪。首先我們看下LayoutManager中的onLayoutChildren方法的源碼,如下:
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
方法內(nèi)僅打印一條Log日志庶诡,日志內(nèi)容為你必須重寫onLayoutChildren方法惦银。因此,在使用自定義LayoutManager時灌砖,記得要重寫它的onLayoutChildren方法璧函,并在方法內(nèi)部編寫真正的布局邏輯。此時不知道你是否會有個疑問基显,貌似還沒有對RecyclerView中的每一個條目itemView進(jìn)行measure流程蘸吓,怎么直接就進(jìn)行l(wèi)ayout流程了呢!這里我們拿LinearLayoutManager的onLayoutChildren方法為例撩幽,其實在方法的內(nèi)部库继,會依次調(diào)用到LayoutManager中的measureChildWithMargins和layoutDecoratedWithMargins方法,兩個方法的內(nèi)部又分別會調(diào)用到每一個子View(條目itemView)的measure和layout方法窜醉。因此宪萄,其實在LinearLayoutManager的onLayoutChildren方法中不僅完成了對RecyclerView的layout流程,還完成了對RecyclerView的每一個條目的measure流程(后面會詳細(xì)分析LinearLayoutManager中的onLayoutChildren方法)榨惰。最后拜英,在dispatchLayoutStep2方法的結(jié)尾處會將mState.mLayoutStep的值置為State.STEP_ANIMATIONS;
現(xiàn)在,繼續(xù)回到RecyclerView的onMeasure方法琅催,在第20行調(diào)用LayoutManager的setMeasuredDimensionFromChildren方法來根據(jù)子View(條目itemView)來獲取RecyclerView的寬度和高度居凶,方法的源碼如下:
void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
final int count = getChildCount();
if (count == 0) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
return;
}
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
final Rect bounds = mRecyclerView.mTempRect;
getDecoratedBoundsWithMargins(child, bounds);
if (bounds.left < minX) {
minX = bounds.left;
}
if (bounds.right > maxX) {
maxX = bounds.right;
}
if (bounds.top < minY) {
minY = bounds.top;
}
if (bounds.bottom > maxY) {
maxY = bounds.bottom;
}
}
mRecyclerView.mTempRect.set(minX, minY, maxX, maxY);
setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec); // LayoutManager中的方法
}
雖然方法將近30行并不是很短,但是邏輯卻是非常的簡單易懂藤抡,就是遍歷RecyclerView中的每一個條目侠碧,根據(jù)每一個條目的矩陣邊界值(top、left缠黍、right弄兜、bottom)來不斷的改變RecyclerView的矩陣邊界值。然后在方法的最后一行調(diào)用LayoutManager的setMeasuredDimension方法,源碼如下:
public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight();
int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom();
int width = chooseSize(wSpec, usedWidth, getMinimumWidth());
int height = chooseSize(hSpec, usedHeight, getMinimumHeight());
setMeasuredDimension(width, height);
}
方法內(nèi)部根據(jù)傳過來的矩陣信息和設(shè)置的Padding來計算RecyclerView的寬度和高度的尺寸值替饿,再調(diào)用chooseSize方法(前面已經(jīng)說明)根據(jù)測量模式獲取最終的寬度和高度的尺寸值语泽,最后調(diào)用
setMeasuredDimension方法(內(nèi)部最終調(diào)用到View的setMeasuredDimension方法)保存測量出的寬度和高度的尺寸值。
最后回到onMeasure方法的第21行盛垦,根據(jù)LayoutManager的shouldMeasureTwice方法的返回值決定是否需要進(jìn)行二次測量湿弦。我們還是看一下LinearLayoutManager中的shouldMeasureTwice方法的源碼:
@Override
boolean shouldMeasureTwice() {
return getHeightMode() != View.MeasureSpec.EXACTLY
&& getWidthMode() != View.MeasureSpec.EXACTLY
&& hasFlexibleChildInBothOrientations();
}
boolean hasFlexibleChildInBothOrientations() {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final ViewGroup.LayoutParams lp = child.getLayoutParams();
if (lp.width < 0 && lp.height < 0) {
return true;
}
}
return false;
}
方法的邏輯都能看懂,根據(jù)方法內(nèi)部的判斷邏輯腾夯,我們可以總結(jié)出一個結(jié)論颊埃,如果不想進(jìn)行二次測量操作,最好將RecyclerView的寬度和高度中的至少一個的測量模式指定為EXACTLY模式蝶俱。
到這里班利,RecyclerView的onMeasure方法就分析完了。關(guān)于這個onMeasure方法榨呆,我猜很多人會有疑問罗标,為什么在它的內(nèi)部會包含layout的流程,既然這個方法中包含了RecyclerView的layout流程积蜻,那么RecyclerView的onLayout方法是不是為一個空方法呢闯割,帶著這個疑問我們走進(jìn)RecyclerView的onLayout方法。
2.2.onLayout:
首先竿拆,我們來看一下onLayout方法的源碼:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
并不是一個空方法挪圾,內(nèi)部調(diào)用了dispatchLayout方法來進(jìn)行l(wèi)ayout操作裕菠,下面我們來看一下dispatchLayout方法的源碼:
void dispatchLayout() {
1 if (mAdapter == null) {
2 Log.e(TAG, "No adapter attached; skipping layout");
3 return;
4 }
5 if (mLayout == null) {
6 Log.e(TAG, "No layout manager attached; skipping layout");
7 return;
8 }
9 mState.mIsMeasuring = false;
10 if (mState.mLayoutStep == State.STEP_START) {
11 dispatchLayoutStep1();
12 mLayout.setExactMeasureSpecsFrom(this);
13 dispatchLayoutStep2();
14 } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
15 || mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to changed size.
16 mLayout.setExactMeasureSpecsFrom(this);
17 dispatchLayoutStep2();
18 } else {
// always make sure we sync them (to ensure mode is exact)
19 mLayout.setExactMeasureSpecsFrom(this);
20 }
21 dispatchLayoutStep3();
}
方法的開始判斷Adapter和LayoutManager是否為空只锭,如果為空芦劣,就調(diào)用return結(jié)束當(dāng)前方法;否則繼續(xù)向下執(zhí)行御板。在第10行锥忿,判斷mState.mLayoutStep的值是否等于State.STEP_START,如果等于就進(jìn)入if條件體中怠肋,里面的dispatchLayoutStep1和dispatchLayoutStep2方法前面已經(jīng)說過敬鬓,這里我們看下LayoutManager的setExactMeasureSpecsFrom方法:
void setExactMeasureSpecsFrom(RecyclerView recyclerView) {
setMeasureSpecs(MeasureSpec.makeMeasureSpec(recyclerView.getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(recyclerView.getHeight(), MeasureSpec.EXACTLY));
}
方法內(nèi)部調(diào)用LayoutManager的setMeasureSpecs方法將RecyclerView的寬度和高度的測量模式和測量尺寸在LayoutManager中保存,而在保存之前先將寬度和高度的測量模式全部指定成EXACTLY模式笙各,再調(diào)用MeasureSpec的makeMeasureSpec將尺寸值和模式合成一個32位的MeasureSpec值钉答。
接著看dispatchLayout方法第14行的判斷條件,根據(jù)源碼中的注釋我們可以理解為酪惭,布局的前兩步流程已經(jīng)在onMeasure方法中執(zhí)行過希痴,當(dāng)發(fā)現(xiàn)RecyclerView的大小發(fā)生改變的時候我們要再次調(diào)用dispatchLayoutStep2方法布局子View(條目itemView)者甲,這種情況貌似不太常見春感。
再接著看第19行,在else語句中也調(diào)用了LayoutManager的setExactMeasureSpecsFrom方法,也就是說只要是正常的執(zhí)行完了onLayout方法鲫懒,RecyclerView的寬度和高度的測量模式都會變成EXACTLY模式嫩实,即使你最初在布局中設(shè)置RecyclerView的寬和高為wrap_content。其實這不難理解窥岩,因為在onLayout方法之前甲献,我們已經(jīng)通過onMeasure方法獲取到了RecyclerView確切的寬和高的尺寸值了,因此這里將寬和高的測量模式都指定成EXACTLY模式也沒什么不妥的了(感興趣的可以自行驗證下)颂翼。
最后來看下dispatchLayout方法的最后一行調(diào)用了dispatchLayoutStep3方法晃洒,這是layout流程的第三步也是最后一步,方法的源碼有些長且邏輯復(fù)雜朦乏,因此這里也還是只貼出該方法源碼中的注釋內(nèi)容:
布局的最后一步球及,保存關(guān)于視圖的動畫信息,觸發(fā)動畫并進(jìn)行必要的清理呻疹。
由此可知在dispatchLayoutStep3方法中主要是做和動畫相關(guān)的操作吃引。至此,RecyclerView的layout流程也就分析完了刽锤。
2.3.measure镊尺、layout流程回顧:
在分析完measure和layout流程的邏輯之后,我們現(xiàn)在回過頭來分析下二者之間邏輯執(zhí)行的聯(lián)系并思。前面在分析onMeasure方法的時候庐氮,我們看到在onMeasure方法中居然存在layout流程的前兩步操作,而在什么情況下會在onMeasure中執(zhí)行這兩步布局操作呢纺荧?通過上面的分析我們知道在Adapter不為空的前提下旭愧,如果RecyclerView的寬度或者高度二者中只要有一個的測量模式不是EXACTLY模式(即被指定為wrap_content),那么就會在onMeasure中執(zhí)行l(wèi)ayout流程的前兩步操作宙暇,而且一般情況下输枯,如果兩者的測量模式都不是EXACTLY模式,還有可能在onMeasure方法中進(jìn)行二次測量和布局的操作占贫;相反桃熄,如果二者的測量模式均為EXACTLY模式,那么onMeasure方法就會在執(zhí)行完RecyclerView自身的measure流程后便結(jié)束掉型奥。
再來看看onLayout方法中的dispatchLayout方法瞳收,如果mState.mLayoutStep的值為State.STEP_START,那么就會在dispatchLayout方法中執(zhí)行l(wèi)ayout流程的前兩步操作厢汹,而在分析measure流程時我們提到過螟深,在dispatchLayoutStep2方法的結(jié)尾會將mState.mLayoutStep的值置為State.STEP_ANIMATIONS。因此烫葬,如果在onMeasure方法中執(zhí)行了layout流程的前兩步操作(dispatchLayoutStep1和dispatchLayoutStep2)界弧,那么在dispatchLayout方法中就不會再次執(zhí)行凡蜻;反之,layout流程的前兩步操作就會在dispatchLayout方法中進(jìn)行的垢箕。這里用一張圖總結(jié)如下:
2.4.onDraw:
到了RecyclerView繪制的最后一個流程-draw流程划栓,在RecyclerView中它將draw和onDraw方法都重寫了,源碼如下:
@Override
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);
}
........
}
@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);
}
}
在draw方法中略去的是繪制邊界發(fā)光效果(EdgeEffect)的邏輯条获,這里不詳細(xì)分析忠荞,我們重點看兩個方法中關(guān)于mItemDecorations的操作。首先帅掘,這個mItemDecorations是一個存儲ItemDecoration類型數(shù)據(jù)的集合委煤,ItemDecoration就是我們一般情況下可能調(diào)用RecyclerView的addItemDecoration方法添加給RecyclerView每一個條目的裝飾。現(xiàn)在修档,在RecyclerView的draw和onDraw方法中分別調(diào)用了它的onDrawOver和onDraw方法素标,難道一個ItemDecoration還要分兩步繪制?我們還是先看下這兩個方法的源碼:
/**
* 給RecyclerView繪制合適的裝飾萍悴。使用此方法繪制的任何內(nèi)容都將在繪制項目視圖之后繪制头遭,從而顯示在視圖上。
*/
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
onDrawOver(c, parent);
}
@Deprecated
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {
}
/**
* 給RecyclerView繪制合適的裝飾癣诱。使用此方法繪制的任何內(nèi)容都將在繪制項目視圖之前繪制计维,因此將顯示在視圖之下。
*/
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
onDraw(c, parent);
}
@Deprecated
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {
}
兩個方法的內(nèi)部都各自調(diào)用自己的重載方法撕予,并且兩個重載的方法也都是空方法鲫惶。這就奇怪了,明明是兩個空方法实抡,而且也不是抽象方法欠母,這就意味著我們在使用ItemDecoration時并不是一定要重寫這兩個方法,那為什么還要弄出兩個不一樣的空方法呢吆寨?細(xì)心的人可能已經(jīng)通過上面代碼中的注釋看出了兩個方法的不同了赏淌,其實就是調(diào)用的時機(jī)不同,一個會在繪制條目視圖之前被調(diào)用啄清,一個會在繪制條目視圖之后被調(diào)用六水;那么在這兩個方法中繪制的裝飾內(nèi)容將分別會呈現(xiàn)在條目視圖的下面和上面。到這里可能還是有人會有疑問辣卒,雖然源碼中的注釋是這么解釋的掷贾,但是拿什么證明它們兩個的調(diào)用時機(jī)就是這樣的呢!答案當(dāng)然是在RecyclerView的draw和onDraw方法中啊荣茫。這里還是要考驗?zāi)銓iew的繪制流程的掌握度了想帅,在View繪制的draw流程中我們知道,View的draw方法將被父布局調(diào)用啡莉,然后在View的draw方法中港准,會依次調(diào)用到View的onDraw和dispatchDraw方法憎乙,其中dispatchDraw方法完成每個子View的繪制(RecyclerView沒有重寫dispatchDraw方法,直接復(fù)用ViewGroup中的dispatchDraw方法)叉趣。
現(xiàn)在再來看看剛剛的RecyclerView的draw和onDraw方法,在draw方法中先是調(diào)用了super.draw方法该押,這就說明RecyclerView的onDraw和dispatchDraw方法會先被調(diào)用到疗杉,而在onDraw方法中調(diào)用的是ItemDecoration的onDraw方法,因此現(xiàn)在可以證明ItemDecoration的onDraw方法是在繪制每個條目視圖之前調(diào)用的了蚕礼;而在執(zhí)行完super.draw方法后烟具,才會繼續(xù)向下執(zhí)行draw方法中的內(nèi)容,因此也就驗證了ItemDecoration的onDrawOver方法是在繪制每個條目視圖之后調(diào)用的了奠蹬。搞懂了兩個方法的調(diào)用時機(jī)朝聋,我們在之后使用自定義ItemDecoration時就可以根據(jù)自身需求來選擇實現(xiàn)對應(yīng)的方法和邏輯了。
2.5.繪制流程總結(jié):
到這里囤躁,RecyclerView的繪制流程大致就講完了冀痕,通過對它的繪制流程的學(xué)習(xí),我們可以從中總結(jié)出兩點關(guān)于RecyclerView使用上的注意點:
1.必須給RecyclerView設(shè)置LayoutManager狸演,因為RecyclerView的每一個條目itemView的測量和布局操作是在LayoutManager中完成的言蛇;如果不設(shè)置,RecyclerView將無法正常顯示宵距。
2.在設(shè)置RecyclerView的寬度和高度時腊尚,最好指定為match_parent或確切的數(shù)值,這樣可以避免進(jìn)行多次測量操作满哪。
3.緩存機(jī)制:
在分析過了RecyclerView的繪制流程后婿斥,我們也算對其有了一個基本的了解。接下來我們就要再深入的了解一下它的緩存機(jī)制了哨鸭,因為我們一直都說RecyclerView非常強(qiáng)大民宿,但到底是什么原因讓它這么強(qiáng)大呢?其實就是它的視圖復(fù)用邏輯非常的完美像鸡,本質(zhì)就是它的緩存機(jī)制做的非常的強(qiáng)大勘高。
3.1.ViewHolder:
在分析RecyclerView的緩存機(jī)制之前,我們還要明確一些關(guān)于RecyclerView的知識點坟桅。那就是在RecyclerView中华望,每一個條目itemView都會與一個ViewHolder關(guān)聯(lián)。對于一個ViewHolder來說仅乓,我們可以直接通過holder.itemView獲取到對應(yīng)的條目itemView赖舟;而對于itemView來說,我們又可以通過獲取它的LayoutParams來獲取到對應(yīng)的mViewHolder夸楣,二者可以說是你中有我我中有你的關(guān)系宾抓。在RecyclerView的視圖復(fù)用機(jī)制中子漩,也正是從holder中獲取到復(fù)用的視圖itemView,關(guān)于ViewHolder源碼中的解釋為:
ViewHolder用來描述一個條目itemView以及它在RecyclerView中位置信息
3.2.onCreateViewHolder:
在了解了ViewHolder和itemView的關(guān)系之后石洗,我們來一點一點揭開RecyclerView緩存機(jī)制的面紗幢泼,首先,要想緩存一個東西那么必須要先創(chuàng)建出這個東西讲衫,我們就從ViewHolder的創(chuàng)建說起缕棵。在我們實現(xiàn)一個Adapter的時候,必須要重寫基類中的三個抽象方法涉兽,其中有一個方法就是onCreateViewHolder招驴,ViewHolder就是在這個方法中創(chuàng)建的。關(guān)于onCreateViewHolder方法枷畏,源碼中的解釋說到别厘,當(dāng)RecyclerView需要一個新的給定類型的條目視圖的時候這個方法會被調(diào)用,那么我們就先看一下onCreateViewHolder是在哪兒被調(diào)用的:
public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
try {
TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
final VH holder = onCreateViewHolder(parent, viewType);
if (holder.itemView.getParent() != null) {
throw new IllegalStateException("ViewHolder views must not be attached when"
+ " created. Ensure that you are not passing 'true' to the attachToRoot"
+ " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");
}
holder.mItemViewType = viewType;
return holder;
} finally {
TraceCompat.endSection();
}
}
在Adapter的createViewHolder方法中拥诡,我們找到了onCreateViewHolder方法的調(diào)用触趴,在createViewHolder方法中通過調(diào)用onCreateViewHolder方法創(chuàng)建一個ViewHolder對象并返回。而之所以貼出它的源碼是因為方法中可能會拋出異常渴肉,而拋出異常的原因應(yīng)該都能看懂雕蔽,當(dāng)我們填充條目視圖的時候不能直接將它附加到RecyclerView中,也就是在調(diào)用LayoutInflater的inflate方法時宾娜,attachToRoot參數(shù)記得傳false批狐,這個原因后面會講到。
3.3.tryGetViewHolderForPositionByDeadline:
現(xiàn)在繼續(xù)尋找createViewHolder方法的調(diào)用處前塔,在Recycler類的tryGetViewHolderForPositionByDeadline方法中我們找到了createViewHolder方法的調(diào)用嚣艇,并且createViewHolder方法僅僅只有這一處被調(diào)用的地方。該方法的源碼如下:
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
........
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position); // step1
fromScrapOrHiddenOrCache = holder != null;
}
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); // step2
........
}
if (holder == null) {
........
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); // step3
........
}
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view); // step4
........
}
}
if (holder == null) { // fallback to pool
holder = getRecycledViewPool().getRecycledView(type); // step5
........
}
if (holder == null) {
........
holder = mAdapter.createViewHolder(RecyclerView.this, type); // step6
........
}
}
........
return holder;
}
這里只貼出了tryGetViewHolderForPositionByDeadline方法中的關(guān)鍵代碼华弓,都是和獲取ViewHolder實例相關(guān)的代碼食零。首先說一下剛剛提到的Recycler這個類,它是RecyclerView的一個內(nèi)部類寂屏,這個類是負(fù)責(zé)管理RecyclerView的視圖以供重復(fù)利用的贰谣,也就是說RecyclerView的緩存機(jī)制其實就在這個Recycler類中。再來看下源碼中關(guān)于這個tryGetViewHolderForPositionByDeadline方法的解釋:
嘗試獲取給定位置的ViewHolder迁霎,可以從回收器碎片吱抚、緩存以及RecycledViewPool中獲取或直接創(chuàng)建它。
由此可知tryGetViewHolderForPositionByDeadline這個方法就是用來獲取ViewHolder的考廉,在方法的內(nèi)部會根據(jù)對應(yīng)的條件從不同的地方獲取到給定位置上的ViewHolder實例秘豹,方法中一共有6處可以獲取到ViewHolder實例的地方(step1-step6),接下來我們一個一個分析昌粤。
3.3.1.getChangedScrapViewForPosition:
在step1處既绕,當(dāng)mState.isPreLayout()為true的時候首先通過getChangedScrapViewForPosition方法獲取一個ViewHolder實例啄刹。而mState.isPreLayout()方法的返回值就是State類中的mInPreLayout變量的值(State類也是RecyclerView的一個內(nèi)部類,它包含一些關(guān)于RecyclerView狀態(tài)的有用信息)凄贩,mInPreLayout變量的默認(rèn)值為false誓军,在預(yù)布局時(前面講RecyclerView繪制流程中的dispatchLayoutStep1方法中),當(dāng)RecyclerView的條目發(fā)生了增加或者移除并且有動畫的時候疲扎,才有被置為true的可能昵时,這種情況并不常見。在getChangedScrapViewForPosition方法的內(nèi)部對Recycler類中的mChangedScrap集合進(jìn)行遍歷评肆,先是對比ViewHolder的位置信息,如果未找到對應(yīng)的ViewHolder非区,再對比ViewHolder的itemId(即我們通過實現(xiàn)Adapter的getItemId方法為每一個item指定的id)瓜挽,如果兩次遍歷均未找到對應(yīng)的ViewHolder,那么就返回null(此方法源碼簡單易懂征绸,請自行查看)久橙。
3.3.2.getScrapOrHiddenOrCachedHolderForPosition:
在step2處,如果在step1處未獲取到ViewHolder管怠,那么調(diào)用getScrapOrHiddenOrCachedHolderForPosition方法來獲取ViewHolder淆衷。首先看一下這個方法的源碼:
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
if (!dryRun) { // dryRun參數(shù)在方法的調(diào)用處均為false
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
// This View is good to be used. We just need to unhide, detach and move to the scrap list.
final ViewHolder vh = getChildViewHolderInt(view);
........
return vh;
}
}
// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
if (!holder.isInvalid() && holder.getLayoutPosition() == position
&& !holder.isAttachedToTransitionOverlay()) {
........
return holder;
}
}
return null;
}
在該方法中,會嘗試從三個地方獲取ViewHolder實例渤弛。首先對Recycler類中的mAttachedScrap集合進(jìn)行遍歷祝拯,當(dāng)發(fā)現(xiàn)mAttachedScrap集合中存在某個ViewHolder的位置信息和方法中傳入的位置信息一致并且這個ViewHolder是有效的,就返回這個ViewHolder實例她肯;如果在mAttachedScrap中沒有找到對應(yīng)的ViewHolder佳头,那么就繼續(xù)向下執(zhí)行調(diào)用mChildHelper的findHiddenNonRemovedView方法,這個mChildHelper是RecyclerView的一個成員變量晴氨,而這個ChildHelper類是一個負(fù)責(zé)管理RecyclerView條目的助手類康嘉,在它的findHiddenNonRemovedView方法中遍歷它內(nèi)部的mHiddenViews集合來返回一個對應(yīng)位置上的隱藏視圖,然后通過getChildViewHolderInt方法獲取這個視圖對應(yīng)的ViewHolder實例然后返回籽前;如果在mHiddenViews中還未找到就繼續(xù)向下執(zhí)行遍歷Recycler類中的mCachedViews集合來尋找對應(yīng)位置的ViewHolder實例⊥ふ洌現(xiàn)在簡單總結(jié)下在step2中獲取ViewHolder實例的先后順序:
mAttachedScrap(Recycler)-->mHiddenViews(ChildHelper)-->mCachedViews(Recycler)
3.3.3. getScrapOrCachedViewForId:
在step3處,當(dāng)mAdapter.hasStableIds()為true的時候枝哄,會調(diào)用getScrapOrCachedViewForId來獲取ViewHolder實例肄梨。在Adapter的hasStableIds方法內(nèi),返回的是Adapter內(nèi)的成員變量mHasStableIds的值挠锥,這個值默認(rèn)為false峭范,只有當(dāng)我們手動調(diào)用Adapter的setHasStableIds方法時才有可能將其置為true。而關(guān)于這個setHasStableIds方法源碼中的解釋為:
指示是否可以用唯一類型的標(biāo)識符來表示數(shù)據(jù)集中的每一項
也就是說如果我們想要調(diào)用setHasStableIds方法將mHasStableIds變量置為true的話瘪贱,我們必須要確保每一個條目都有一個同一類型的并且唯一的標(biāo)識符可以用來識別它們纱控,那到底要怎么設(shè)置這個唯一標(biāo)識符呢辆毡?我們先來看一下剛剛getScrapOrCachedViewForId這個方法的源碼:
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
// Look in our attached views first
final int count = mAttachedScrap.size();
for (int i = count - 1; i >= 0; i--) {
final ViewHolder holder = mAttachedScrap.get(i);
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
if (type == holder.getItemViewType()) {
........
return holder;
}
........
}
}
// Search the first-level cache
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
if (holder.getItemId() == id && !holder.isAttachedToTransitionOverlay()) {
if (type == holder.getItemViewType()) {
........
return holder;
}
........
}
}
return null;
}
看到該方法的源碼應(yīng)該能猜到剛剛說到的唯一標(biāo)識符是什么了吧,其實就是每一個ViewHolder的mItemId甜害。當(dāng)我們實現(xiàn)一個Adapter的時候舶掖,我們可以根據(jù)自身的需要來決定是否重寫Adaper的getItemId方法,當(dāng)我們重寫了這個方法的時候尔店,我們在方法中返回的long類型的值就會最終被賦值到ViewHolder的mItemId變量上的眨攘。因此,這里要提前說明兩個問題嚣州,第一鲫售,當(dāng)我們手動調(diào)用了Adapter的setHasStableIds方法將mHasStableIds置為true時,我們一定要確保我們重寫了Adapter的getItemId方法為每一個條目都設(shè)置了唯一標(biāo)識该肴,因為這時會根據(jù)ViewHolder的mItemId變量來判斷緩存中是否有對應(yīng)的ViewHolder實例情竹;第二,在重寫的Adapter的getItemId方法內(nèi)匀哄,我們一定要確保為每一個條目設(shè)置的標(biāo)識都是唯一的秦效,不能重復(fù)。以上兩點一定要注意涎嚼,否則在視圖復(fù)用的時候會出現(xiàn)視圖紊亂的情況(感興趣可自行驗證下)阱州。
現(xiàn)在再回到getScrapOrCachedViewForId方法,方法內(nèi)又一次對mAttachedScrap和mCachedViews進(jìn)行遍歷法梯,只不過在這里是根據(jù)ViewHolder的mItemId變量進(jìn)行匹配苔货,同時還要確保ViewHolder的mItemViewType變量值一致。這個mItemViewType變量其實我們都熟悉立哑,當(dāng)我們實現(xiàn)Adapter時蒲赂,通過重寫Adapter的getItemViewType方法為每一個條目設(shè)置的類型值最終就會賦值給對應(yīng)的ViewHolder的mItemViewType變量。
3.3.4. ViewCacheExtension:
在step4處刁憋,當(dāng)mViewCacheExtension不為空時滥嘴,調(diào)用ViewCacheExtension的getViewForPositionAndType方法獲取一個視圖。這個ViewCacheExtension是RecyclerView的一個抽象的內(nèi)部類至耻,內(nèi)部只有g(shù)etViewForPositionAndType這么一個抽象方法若皱,這個類的定義如下:
ViewCacheExtension是一個幫助類,它提供了一個可以由開發(fā)人員控制的額外的視圖緩存層尘颓。
也就是說這個類是供我們開發(fā)人員自行實現(xiàn)緩存邏輯的一個幫助類走触,我們可以通過重寫它的抽象方法來實現(xiàn)具體的獲取指定位置的視圖緩存的邏輯。關(guān)于這一級緩存疤苹,可以根據(jù)自身的情況來選擇性的使用互广,不過在不確保自己的緩存邏輯沒問題的情況下還是慎用的。
3.3.5. RecycledViewPool:
在step5處,到了RecyclerView的最后一級緩存了惫皱,通過調(diào)用RecycledViewPool的getRecycledView方法獲取一個ViewHolder實例像樊。首先說一下RecycledViewPool,它是RecyclerView的一個內(nèi)部類旅敷,在它的內(nèi)部還有一個叫ScrapData的內(nèi)部類生棍,這個ScrapData類的源碼如下:
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
在ScrapData類的內(nèi)部擁有一個mScrapHeap集合用來存儲ViewHolder,現(xiàn)在再來看一下RecycledViewPool的getRecycledView方法:
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
方法中的mScrap變量是RecycledViewPool中的一個SparseArray類型的數(shù)組媳谁,這個數(shù)組內(nèi)部的value類型為ScrapData類型涂滴,它的key就是ViewHolder的mItemViewType值。在getRecycledView方法中晴音,我們根據(jù)mItemViewType獲取mScrap數(shù)組中對應(yīng)的ScrapData數(shù)據(jù)柔纵,然后再遍歷ScrapData中的mScrapHeap集合返回一個對應(yīng)mItemViewType的ViewHolder實例。
3.3.6. createViewHolder:
在step6處锤躁,我們通過調(diào)用createViewHolder方法來創(chuàng)建一個新的ViewHolder實例搁料,createViewHolder方法的內(nèi)部實現(xiàn)原理在前面已經(jīng)講過。在方法執(zhí)行到這里的時候进苍,說明在前面的幾處緩存中并不存在指定位置上的ViewHolder實例加缘,這時就要新建ViewHolder鸭叙,也就在此時我們在Adapter中實現(xiàn)的onCreateViewHolder方法就會被調(diào)用了觉啊。
3.4. 緩存小結(jié):
現(xiàn)在,我們完成了對RecyclerView獲取緩存視圖的邏輯的分析沈贝,通過分析我們可以知道杠人,RecyclerView一共存在五層緩存,它們分別為mChangedScrap宋下、mAttachedScrap嗡善、mCachedViews、mViewCacheExtension以及mRecyclerPool学歧,其中mViewCacheExtension一般情況下是不會用到這一級緩存的罩引。而對于這幾級緩存,通過分析Recyler類的源碼注釋以及這幾處緩存的調(diào)用時機(jī)可以總結(jié)出它們之間的區(qū)別如下:
3.4.1. mChangedScrap:
這個集合存放ViewHolder對象枝笨,數(shù)量上不做限制袁铐,它存放的是發(fā)生了變化的ViewHolder,如果使用這里面緩存的ViewHolder是要重新走Adapter的綁定方法的横浑。
3.4.2. mAttachedScrap:
這個集合也是存放ViewHolder對象剔桨,同樣沒有數(shù)量上的限制,存放在這里的ViewHolder數(shù)據(jù)是不做修改的徙融,不會重新走Adapter的綁定方法洒缀。在上面的mChangedScrap以及當(dāng)前的mAttachedScrap中存放的ViewHolder對應(yīng)的視圖僅僅是被detach掉了,當(dāng)再次被使用時只需重新attach即可,并未與RecyclerView完全解除關(guān)系树绩。
3.4.3. mCachedViews:
這個集合依然存放的是ViewHolder對象萨脑,但是不同于上面兩層緩存的是,它里面存放的ViewHolder對應(yīng)的視圖已經(jīng)被remove掉葱峡,和RecyclerView已經(jīng)沒有任何關(guān)系了砚哗,但是它里面的ViewHolder依然保存著之前的數(shù)據(jù)信息,比如position和綁定的數(shù)據(jù)等砰奕。這一級緩存是有容量限制的蛛芥,默認(rèn)是2。
3.4.4. mRecyclerPool:
這個RecycledViewPool在前面已經(jīng)講過它內(nèi)部的存儲結(jié)構(gòu)军援,它的內(nèi)部實際存儲的也是ViewHolder對象仅淑。只不過這里面保存的ViewHolder對應(yīng)的視圖不僅僅是已經(jīng)被remove掉的視圖,而且是沒有綁定任何數(shù)據(jù)信息的視圖了胸哥,如果使用這里緩存的ViewHolder是需要重新走Adapter的綁定方法了涯竟。
關(guān)于RecyclerView的緩存機(jī)制這里就分析到這,接下來我們再針對LinearLayoutManager的部分源碼進(jìn)行分析空厌,從而對RecyclerView整體的工作機(jī)制有著更加深入的理解庐船。
4.LinearLayoutManager:
通過最開始分析RecyclerView的繪制流程我們知道,LayoutManager在RecyclerView的使用中扮演著非常重要的角色嘲更。首先筐钟,在它的onLayoutChildren方法中將完成對每一個條目的測量和布局流程;其次在它的scrollBy方法中還會對RecyclerView的滑動事件進(jìn)行響應(yīng)處理赋朦。接下來我們就來分析下兩個方法的源碼:
4.1.onLayoutChildren:
關(guān)于LinearLayoutManager的onLayoutChildren方法篓冲,由于方法的源碼比較長,這里不打算貼出源碼了宠哄。在這個方法中進(jìn)行的主要操作按照順序依次如下:
1.在LinearLayoutManager中存在一個LayoutState類壹将,這個類是RecyclerView在填充空白區(qū)域時存儲臨時狀態(tài)的幫助類,在onLayoutChildren方法中毛嫉,先對其進(jìn)行初始化操作(如果mLayoutState為空)诽俯。
2.確定RecyclerView的布局方向,通過LinearLayoutManager的resolveShouldLayoutReverse方法承粤。
3.確定錨點的位置和坐標(biāo)暴区,它決定了條目布局的起始位置。其中AnchorInfo是LinearLayoutManager的一個內(nèi)部類密任,用來保存錨點信息的颜启。
4.通過調(diào)用LayoutManager的detachAndScrapAttachedViews方法對當(dāng)前存在的條目進(jìn)行暫時的回收緩存,每一個條目會根據(jù)對應(yīng)的條件緩存到不同的地方浪讳。
5.根據(jù)錨點信息向start和end方向填充條目視圖缰盏,通過調(diào)用LinearLayoutManager的fill方法。
6.調(diào)用layoutForPredictiveAnimations方法進(jìn)行和PredictiveAnimation相關(guān)的預(yù)布局操作。
通過以上的步驟可以看出口猜,在onLayoutChildren方法內(nèi)就是完成每一個條目向RecyclerView上的填充操作负溪。而真正的將每一個條目添加到RecyclerView上的操作是在步驟5,步驟5中調(diào)用了LinearLayoutManager中的fill方法济炎,在fill方法中存在一個while循環(huán)川抡,根據(jù)mLayoutState來判斷是否存在可填充的條目視圖,如果存在就會在while循環(huán)內(nèi)部調(diào)用layoutChunk方法將條目視圖添加到RecyclerView上须尚,我們一起看下這個layoutChunk方法的源碼:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
1 View view = layoutState.next(recycler);
2 ........
3 RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
4 if (layoutState.mScrapList == null) {
5 if (mShouldReverseLayout == (layoutState.mLayoutDirection
6 == LayoutState.LAYOUT_START)) {
7 addView(view);
8 } else {
9 addView(view, 0);
10 }
11 } else {
12 ........
13 }
14 measureChildWithMargins(view, 0, 0);
15 result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
16 int left, top, right, bottom;
17 if (mOrientation == VERTICAL) {
18 if (isLayoutRTL()) {
19 right = getWidth() - getPaddingRight();
20 left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
21 } else {
22 left = getPaddingLeft();
23 right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
24 }
25 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
26 bottom = layoutState.mOffset;
27 top = layoutState.mOffset - result.mConsumed;
28 } else {
29 top = layoutState.mOffset;
30 bottom = layoutState.mOffset + result.mConsumed;
31 }
32 } else {
33 ........
34 }
35 layoutDecoratedWithMargins(view, left, top, right, bottom);
36 ........
}
只保留了方法中一些常規(guī)情況下的代碼片段崖堤,在第一行通過調(diào)用LayoutState的next方法獲取一個條目視圖,關(guān)于這個next方法我們接下來會單獨分析耐床;在接下來的第4行判斷LayoutState的mScrapList是否為空密幔,這個mScrapList僅僅在layoutForPredictiveAnimations方法被調(diào)用過程中才可能不為空,因此這次我們暫時不考慮它的存在撩轰;在第5行根據(jù)布局的方向來調(diào)用addView方法將條目視圖添加到RecyclerView上胯甩,關(guān)于這個addView方法接下來也會單獨分析;接著就是在第14行調(diào)用measureChildWithMargins對條目視圖進(jìn)行測量操作堪嫂;最后在第35行調(diào)用layoutDecoratedWithMargins方法對條目視圖進(jìn)行布局操作偎箫。其實這個方法非常的簡單易懂,因此有些地方就不做詳細(xì)解讀皆串,我們重點看一下剛剛說到的兩個方法淹办,首先看一下LayoutState的next方法。
4.2.LayoutState的next方法:
我們直接看一下這個方法的源碼:
View next(RecyclerView.Recycler recycler) {
1 if (mScrapList != null) {
2 return nextViewFromScrapList();
3 }
4 final View view = recycler.getViewForPosition(mCurrentPosition);
5 mCurrentPosition += mItemDirection;
6 return view;
}
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
這里我們依然不考慮mScrapList不為空的情況愚战。在第4行會調(diào)用Recycler的getViewForPosition方法娇唯,而這個方法內(nèi)部經(jīng)過逐層的調(diào)用齐遵,最終就會調(diào)用到前面我們說到的tryGetViewHolderForPositionByDeadline方法寂玲。這也就說明在RecyclerView的繪制流程中,是通過Recycler的tryGetViewHolderForPositionByDeadline方法來獲取每一個條目視圖的梗摇。
4.3.LayoutManager的addViewInt方法:
接著我們看一下剛剛說到的addView方法拓哟,這個方法其實是在LayoutManager中,在它的方法內(nèi)部最終會調(diào)用到addViewInt方法伶授,方法的源碼如下:
private void addViewInt(View child, int index, boolean disappearing) {
final ViewHolder holder = getChildViewHolderInt(child);
........
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (holder.wasReturnedFromScrap() || holder.isScrap()) {
........
mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
} else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child
// ensure in correct position
int currentIndex = mChildHelper.indexOfChild(child);
if (index == -1) {
index = mChildHelper.getChildCount();
}
if (currentIndex == -1) {
throw new IllegalStateException("Added View has RecyclerView as parent but"
+ " view is not a real child. Unfiltered index:"
+ mRecyclerView.indexOfChild(child) + mRecyclerView.exceptionLabel());
}
if (currentIndex != index) {
mRecyclerView.mLayout.moveView(currentIndex, index);
}
} else {
mChildHelper.addView(child, index, false);
........
}
........
}
在方法的內(nèi)部根據(jù)條目視圖的來源不同而進(jìn)行不同的操作處理断序,第一,如果視圖來自scrap緩存糜烹,那么調(diào)用mChildHelper的attachViewToParent方法將視圖重新依附到RecyclerView上违诗;如果視圖不是來自scrap緩存并且它的父布局就是當(dāng)前的RecyclerView,那么就驗證它的位置的合法性疮蹦,如果位置不合法就把它從當(dāng)前位置移動到另一個位置诸迟;如果前兩種情況都不滿足說明當(dāng)前的視圖并未添加到RecyclerView上,那么就調(diào)用mChildHelper的addView方法將視圖添加到RecyclerView上。
方法中的邏輯很容易理解阵苇,這里重點看一下剛剛說到的這個mChildHelper壁公,前面已經(jīng)對這個ChildHelper類做過簡單的介紹,它是管理RecyclerView條目的一個幫助類绅项,在addViewInt方法中調(diào)用到的它的幾個方法最終都會經(jīng)過它內(nèi)部的Callback接口回調(diào)到RecyclerView中紊册,最終通過調(diào)用RecyclerView的addView、attachViewToParent快耿、removeViewAt等方法完成每一個條目視圖的添加囊陡,依附,移除等操作掀亥」匦保可以說ChildHelper起到一個橋梁的作用,幫助RecyclerView更好的完成對每一個條目的管理工作铺浇。
到這里L(fēng)inearLayoutManager的onLayoutChildren以及它內(nèi)部的幾個重要方法痢畜,我們就分析完了,通過分析我們可以證實當(dāng)我們繪制一個RecyclerView的時候鳍侣,每一個條目視圖的獲取丁稀,展示,測量以及布局操作是在LayoutManager的onLayoutChildren方法中完成的倚聚。
4.4.scrollBy方法:
在LinearLayoutManager中有一個scrollBy方法线衫,在這個方法的內(nèi)部完成了對RecyclerView滑動事件的處理。說到滑動惑折,首先我們會想到的是RecyclerView的onTouchEvent方法授账,再準(zhǔn)確些可以說是onTouchEvent方法中針對ACTION_MOVE事件的處理邏輯,我們一起看下源碼:
public boolean onTouchEvent(MotionEvent e) {
........
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
........
switch (action) {
........
case MotionEvent.ACTION_MOVE: {
........
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
........
} break;
........
}
........
return true;
}
這里略去了大部分代碼惨驶,首先在方法的開始會確定當(dāng)前RecyclerView在哪個方向上可以滑動白热,在LinearLayoutManager中canScrollHorizontally和canScrollVertically方法的返回值取決于LinearLayoutManager中的mOrientation變量的值。這個mOrientation的默認(rèn)值為LinearLayout.VERTICAL粗卜,此時canScrollHorizontally方法返回false屋确,canScrollVertically方法返回true,當(dāng)mOrientation的值為LinearLayout.HORIZONTAL時续扔,canScrollHorizontally方法就返回true攻臀,而canScrollVertically方法返回false,這個邏輯應(yīng)該很好理解纱昧,這里不多說了刨啸。接著看下在ACTION_MOVE事件處理中的scrollByInternal方法,在這個方法的內(nèi)部經(jīng)過逐層調(diào)用最終會根據(jù)滑動方向以及滑動偏移量來決定調(diào)用LayoutManager的scrollVerticallyBy或scrollHorizontallyBy方法中的其一识脆,在LinearLayoutManager中设联,這兩個方法的內(nèi)部最終都會調(diào)用到scrollBy方法加匈。scrollBy方法的源碼如下:
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
........
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
........
return scrolled;
}
這里僅貼出scrollBy方法的少許源碼,目的其實就是想讓大家看到fill方法的調(diào)用仑荐,從而讓大家明白RecyclerView的視圖復(fù)用邏輯也是通過fill方法完成的雕拼,而剛剛fill方法的邏輯也已經(jīng)分析過了。說到視圖復(fù)用粘招,就不得不想到數(shù)據(jù)更新啥寇,Adapter的onBindViewHolder方法就是負(fù)責(zé)實現(xiàn)RecyclerView每一個條目數(shù)據(jù)更新邏輯的,在onBindViewHolder方法中我們要根據(jù)方法中的位置參數(shù)來獲取數(shù)據(jù)源中對應(yīng)的數(shù)據(jù)來設(shè)置到視圖上洒扎,從而確保RecyclerView在滑動過程中不會出現(xiàn)數(shù)據(jù)顯示不正確的問題辑甜。而onBindViewHolder方法的最終調(diào)用處也是在Recycler的tryGetViewHolderForPositionByDeadline方法中,這里就不貼出具體的源碼了袍冷,可以自行查看源碼中的調(diào)用邏輯磷醋。
4.5.LinearLayoutManager小結(jié):
到這里,關(guān)于LinearLayoutManager的主要邏輯也就分析的差不多了胡诗,通過分析我們可以得到以下這些結(jié)論:
1.LinearLayoutManager的onLayoutChildren方法中會完成RecyclerView的條目的測量和布局操作邓线。
2.LinearLayoutManager的scrollBy方法中會完成對RecyclerView滑動事件的處理。
3.Recycler這個類在RecyclerView的整個工作機(jī)制中扮演著非常重要的作用煌恢,它不僅僅完成視圖的回收和復(fù)用邏輯骇陈,同時創(chuàng)建一個條目視圖的邏輯也在其內(nèi)部完成。
結(jié)語
關(guān)于RecyclerView的知識點遠(yuǎn)不止文章中提到的這些瑰抵,也只有自己走進(jìn)源碼認(rèn)真的閱讀它的內(nèi)部實現(xiàn)邏輯才能更好的掌握它的工作機(jī)制你雌。文章中可能有寫的不正確的地方,還望批評指正二汛!