*本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布
??從今天開始活尊,樓主正式開始分析RecyclerView
的源碼牵辣。為了閱讀RecyclerView
的源碼陨闹,樓主專門去看了View
的三大流程扑浸,也就是所謂的刷裝備引矩。當(dāng)然在閱讀RecyclerView
的源碼時(shí)梁丘,也參考了其他大佬的文章,本文盡可能的貼出比較優(yōu)秀的文章旺韭,正所謂他山之石氛谜,可以攻玉。
??作為系列的第一篇文章区端,說說樓主為什么需要來專門的閱讀RecyclerView
的源碼值漫,主要從三大方面說起。一是RecyclerView
在實(shí)際開發(fā)非常的重要织盼,現(xiàn)在幾乎每個(gè)app都會展示很多的數(shù)據(jù)杨何,列表展示自然是非常好的方式,而在RecyclerView
在列表中占據(jù)著舉足輕重的作用沥邻,所以RecyclerView
在實(shí)際開發(fā)中危虱,是經(jīng)常見的,我們得之魚唐全,還必須得之漁埃跷;二是現(xiàn)在網(wǎng)上分析RecyclerView的文章滿篇飛,但是文章大多都比較零碎邮利,沒有系統(tǒng)的分析RecyclerView
捌蚊,本文打算系統(tǒng)的分析RecyclerView
,也可以說是集百家之長近弟;三是樓主本身對RecycleView
的使用也是非常的頻繁缅糟,但是沒有深入的了解它的原理,所以這也算是對自身的一個(gè)提升祷愉。
??閱讀源碼本身是一件非炒盎拢枯燥和耗時(shí)間的事情,對樓主自身來說二鳄,也是亞歷山大赴涵,害怕自己自身的經(jīng)驗(yàn)不足,誤導(dǎo)前來學(xué)習(xí)的同學(xué)订讼,所以如果文章中有錯(cuò)誤的地方髓窜,請各位大佬指點(diǎn)。
??本系列文章樓主打算從幾個(gè)地方說起。先是將RecyclerView
當(dāng)成一個(gè)普通的View
寄纵,分別分析它的三大流程鳖敷、事件傳遞(包括嵌套滑動(dòng));然后是分析RecyclerView
的緩存原理程拭,這也是RecyclerView
的精華所在;然后分析的是RecyclerView
的Adapter
定踱、LayoutManager
、ItemAnimator
和ItemDecoration
恃鞋。最后就是RecyclerView
的擴(kuò)展崖媚,包括LayoutManager
的自定義和使用RecyclerView
常見的坑等。
??看到上面所寫的列表恤浪,自己也不禁留下冷汗畅哑,原來RecyclerView
有這么多的內(nèi)容,真擔(dān)心自己不能完成任務(wù)??水由。
1. 概述
??在分析RecyclerView
源碼之前荠呐,我們還是對RecyclerView
有一個(gè)初步的了解,簡單的了解它是什么绷杜,它的基本結(jié)構(gòu)有哪些。
??RecyclerView
是Google爸爸在2014年的IO大會提出來(看來RecyclerView
的年齡還是比較大了??)濒募,具體目的是不是用來替代ListView
的鞭盟,樓主也不知道,因?yàn)槟菚r(shí)候樓主還在讀高二瑰剃。但是在實(shí)際開發(fā)中齿诉,自從有了RecyclerView
,ListView
和GridView
就很少用了晌姚,所以我們暫且認(rèn)為RecyclerView
的目的是替代ListView
和GridView
粤剧。
??RecyclerView
本身是一個(gè)展示大量數(shù)據(jù)的控件,相比較ListView
,RecyclerView
的4級緩存(也有人說是3級緩存挥唠,這些都不重要??)就表現(xiàn)的非常出色抵恋,在性能方面相比于ListView
提升了不少。同時(shí)由于LayoutManager
的存在,讓RecyclerView
不僅有ListView
的特點(diǎn)宝磨,同時(shí)兼有GridView
的特點(diǎn)弧关。這可能是RecyclerView
受歡迎的原因之一吧。
??RecyclerView
在設(shè)計(jì)方面上也是非常的靈活唤锉,不同的部分承擔(dān)著不同的職責(zé)世囊。其中Adapter
負(fù)責(zé)提供數(shù)據(jù),包括創(chuàng)建ViewHolder
和綁定數(shù)據(jù)窿祥,LayoutManager
負(fù)責(zé)ItemView
的測量和布局,ItemAnimator
負(fù)責(zé)每個(gè)ItemView
的動(dòng)畫株憾,ItemDecoration
負(fù)責(zé)每個(gè)ItemView
的間隙。這種插拔式的架構(gòu)使得RecyclerView
變得非常的靈活晒衩,每一個(gè)人都可以根據(jù)自身的需求來定義不同的部分嗤瞎。
??正因?yàn)檫@種插拔式的設(shè)計(jì)墙歪,使得RecyclerView
在使用上相比較于其他的控件稍微難那么一點(diǎn)點(diǎn),不過這都不算事猫胁,誰叫RecyclerView
這么惹人愛呢??箱亿。
??好了,好像廢話有點(diǎn)多弃秆,現(xiàn)在我們正式來分析源碼吧届惋,本文的重點(diǎn)是RecyclerView
的三大流程。
??本文參考文章:
??注意菠赚,本文RecyclerView
源碼均來自于27.1.1
2. measure
??不管RecyclerView
是多么神奇脑豹,它也是一個(gè)View
,所以分析它的三大流程是非常有必要的衡查。同時(shí)瘩欺,如果了解過RecyclerView
的同學(xué)應(yīng)該都知道,RecyclerView
的三大流程跟普通的View
比較拌牲,有很大的不同俱饿。
??首先,我們來看看measure過程塌忽,來看看RecyclerView
的onMeasure
方法拍埠。
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
// 第一種情況
}
if (mLayout.isAutoMeasureEnabled()) {
// 第二種情況
} else {
// 第三種情況
}
}
??onMeasure
方法還是有點(diǎn)長,這里我將它分為3種情況土居,我將簡單解釋這三種情況枣购。
mLayout
即LayoutManager
的對象。我們知道擦耀,當(dāng)RecyclerView
的LayoutManager
為空時(shí),RecyclerView
不能顯示任何的數(shù)據(jù)棉圈,在這里我們找到答案。LayoutManager
開啟了自動(dòng)測量時(shí)眷蜓,這是一種情況分瘾。在這種情況下,有可能會測量兩次吁系。- 第三種情況就是沒有開啟自動(dòng)測量的情況芹敌,這種情況比較少,因?yàn)闉榱?code>RecyclerView支持
warp_content
屬性垮抗,系統(tǒng)提供的LayoutManager
都開啟自動(dòng)測量的氏捞,不過我們還是要分析的。
??首先我們來第一種情況冒版。
(1).當(dāng)LayoutManager為空時(shí)
??這種情況下比較簡單液茎,我們來看看源碼:
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
??直接調(diào)了defaultOnMeasure
方法,我們繼續(xù)來看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
方法來計(jì)算值滞造,然后就是setMeasuredDimension
方法來設(shè)置寬高。我們來看看:
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
方法表達(dá)的意思比較簡單栋烤,就是通過RecyclerView
的測量mode來獲取不同的值谒养,這里就不詳細(xì)的解釋了。
??到此明郭,第一種情況就分析完畢了买窟。因?yàn)楫?dāng)LayoutManager
為空時(shí),那么當(dāng)RecyclerView
處于onLayout
階段時(shí)薯定,會調(diào)用dispatchLayout
方法始绍。而在dispatchLayout
方法里面有這么一行代碼:
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
??所以,當(dāng)LayoutManager
為空時(shí)话侄,不顯示任何數(shù)據(jù)是理所當(dāng)然的亏推。
??現(xiàn)在我們來看看第二種情況,也就是正常的情況年堆。
(2). 當(dāng)LayoutManager開啟了自動(dòng)測量
??在分析這種情況之前吞杭,我們先對了解幾個(gè)東西。
??RecyclerView
的測量分為兩步变丧,分別調(diào)用dispatchLayoutStep1
和dispatchLayoutStep2
芽狗。同時(shí),了解過RecyclerView
源碼的同學(xué)應(yīng)該知道在RecyclerView
的源碼里面還一個(gè)dispatchLayoutStep3
方法锄贷。這三個(gè)方法的方法名比較接近译蒂,所以容易讓人搞混淆曼月。本文會詳細(xì)的講解這三個(gè)方法的作用谊却。
??由于在這種情況下,只會調(diào)用dispatchLayoutStep1
和dispatchLayoutStep2
這兩個(gè)方法哑芹,所以這里會重點(diǎn)的講解這兩個(gè)方法炎辨。而dispatchLayoutStep3
方法的調(diào)用在RecyclerView
的onLayout
方法里面,所以在后面分析onLayout
方法時(shí)再來看dispatchLayoutStep3
方法聪姿。
??我們在分析之前碴萧,先來看一個(gè)東西--mState.mLayoutStep
。這個(gè)變量有幾個(gè)取值情況末购。我們分別來看看:
取值 | 含義 |
---|---|
State.STEP_START |
mState.mLayoutStep 的默認(rèn)值破喻,這種情況下,表示RecyclerView還未經(jīng)歷dispatchLayoutStep1 盟榴,因?yàn)?code>dispatchLayoutStep1調(diào)用之后mState.mLayoutStep 會變?yōu)?code>State.STEP_LAYOUT曹质。 |
State.STEP_LAYOUT | 當(dāng)mState.mLayoutStep 為State.STEP_LAYOUT 時(shí),表示此時(shí)處于layout階段,這個(gè)階段會調(diào)用dispatchLayoutStep2 方法layout RecyclerView 的children 羽德。調(diào)用dispatchLayoutStep2 方法之后几莽,此時(shí)mState.mLayoutStep 變?yōu)榱?code>State.STEP_ANIMATIONS。 |
State.STEP_ANIMATIONS | 當(dāng)mState.mLayoutStep 為State.STEP_ANIMATIONS 時(shí)宅静,表示RecyclerView 處于第三個(gè)階段章蚣,也就是執(zhí)行動(dòng)畫的階段,也就是調(diào)用dispatchLayoutStep3 方法姨夹。當(dāng)dispatchLayoutStep3 方法執(zhí)行完畢之后纤垂,mState.mLayoutStep 又變?yōu)榱?code>State.STEP_START。 |
??從上表中匀伏,我們了解到mState.mLayoutStep
的三個(gè)狀態(tài)對應(yīng)著不同的dispatchLayoutStep
方法洒忧。這一點(diǎn),我們必須清楚够颠,否則接下來的代碼將難以理解熙侍。
??好了,前戲準(zhǔn)備的差不多履磨,現(xiàn)在應(yīng)該進(jìn)入高潮了??蛉抓。我們開始正式的分析源碼了。
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);
}
}
??我將這段代碼分為三步剃诅。我們來看看:
- 調(diào)用
LayoutManager
的onMeasure
方法進(jìn)行測量巷送。對于onMeasure
方法,我也感覺到非常的迷惑矛辕,發(fā)現(xiàn)傳統(tǒng)的LayoutManager
都沒有實(shí)現(xiàn)這個(gè)方法笑跛。后面,我們會將簡單的看一下這個(gè)方法聊品。- 如果
mState.mLayoutStep
為State.STEP_START
的話飞蹂,那么就會執(zhí)行dispatchLayoutStep1
方法,然后會執(zhí)行dispatchLayoutStep2
方法翻屈。- 如果需要第二次測量的話陈哑,會再一次調(diào)用
dispatchLayoutStep2
方法。
??以上三步伸眶,我們一步一步的來分析惊窖。首先,我們來看看第一步厘贼,也是看看onMeasure
方法界酒。
??LayoutManager
的onMeasure
方法究竟為我們做什么,我們來看看:
public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}
??默認(rèn)是調(diào)用的RecyclerView
的defaultOnMeasure
方法,至于defaultOnMeasure
方法里面究竟做了什么嘴秸,這在前面已經(jīng)介紹過了毁欣,這里就不再介紹了售担。
??View
的onMeasure
方法的作用通產(chǎn)來說有兩個(gè)。一是測量自身的寬高署辉,從RecyclerView
來看族铆,它將自己的測量工作托管給了LayoutManager
的onMeasure
方法。所以哭尝,我們在自定義LayoutManager
時(shí)哥攘,需要注意onMeasure
方法存在,不過從官方提供的幾個(gè)LayoutManager
材鹦,都沒有重寫這個(gè)方法逝淹。所以不到萬得已,最好不要重寫LayoutManager
的onMeasure
方法桶唐;二是測量子View
,不過到這里我們還沒有看到具體的實(shí)現(xiàn)栅葡。
??接下來,我們來分析第二步尤泽,看看dispatchLayoutStep1
方法和dispatchLayoutStep2
方法究竟做了什么欣簇。
??在正式分析第二步之前,我們先對這三個(gè)方法有一個(gè)大概的認(rèn)識坯约。
方法名 | 作用 |
---|---|
dispatchLayoutStep1 | 三大dispatchLayoutStep 方法第一步熊咽。本方法的作用主要有三點(diǎn):1.處理Adapter 更新;2.決定是否執(zhí)行ItemAnimator ;3.保存ItemView 的動(dòng)畫信息。本方法也被稱為preLayout(預(yù)布局)闹丐,當(dāng)Adapter 更新了横殴,這個(gè)方法會保存每個(gè)ItemView 的舊信息(oldViewHolderInfo) |
dispatchLayoutStep2 | 三大dispatchLayoutStep 方法第二步。在這個(gè)方法里面卿拴,真正進(jìn)行children 的測量和布局衫仑。 |
dispatchLayoutStep3 | 三大dispatchLayoutStep 方法第三步。這個(gè)方法的作用執(zhí)行在dispatchLayoutStep1 方法里面保存的動(dòng)畫信息堕花。本方法不是本文的介紹重點(diǎn)文狱,后面在介紹ItemAnimator 時(shí),會重點(diǎn)分析這個(gè)方法航徙。 |
??我們回到onMeasure方法里面如贷,先看看整個(gè)執(zhí)行過程陷虎。
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
時(shí)到踏,才會調(diào)用 dispatchLayoutStep1
方法,這里與我們前面介紹mLayoutStep
對應(yīng)起來了∩性常現(xiàn)在我們看看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) {
// 找到?jīng)]有被remove的ItemView,保存OldViewHolder信息窝稿,準(zhǔn)備預(yù)布局
}
if (mState.mRunPredictiveAnimations) {
// 進(jìn)行預(yù)布局
} else {
clearOldPositions();
}
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;
}
??本文只簡單分析一下這個(gè)方法,因?yàn)檫@個(gè)方法跟ItemAnimator
有莫大的關(guān)系凿掂,后續(xù)在介紹ItemAnimator
時(shí)會詳細(xì)的分析伴榔。在這里纹蝴,我們將重點(diǎn)放在processAdapterUpdatesAndSetAnimationFlags
里面,因?yàn)檫@個(gè)方法計(jì)算了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
變量里面塘安,我們發(fā)現(xiàn)mRunSimpleAnimations
的值與mFirstLayoutComplete
有關(guān),mRunPredictiveAnimations
同時(shí)跟mRunSimpleAnimations
有關(guān)援奢。所以這里我們可以得出一個(gè)結(jié)論,當(dāng)RecyclerView
第一次加載數(shù)據(jù)時(shí),是不會執(zhí)行的動(dòng)畫。換句話說擦秽,每個(gè)ItemView
還沒有layout
完畢板乙,怎么會進(jìn)行動(dòng)畫。這一點(diǎn)具篇,我們也可以通過Demo來證明纬霞,這里也就不展示了。
??接下來我們看看dispatchLayoutStep2
方法驱显,這個(gè)方法是真正布局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);
}
??在這里,我們重點(diǎn)的看兩行代碼埃疫。一是在這里绢陌,我們可以看到Adapter
的getItemCount
方法被調(diào)用;二是調(diào)用了LayoutManager
的onLayoutChildren
方法,這個(gè)方法里面進(jìn)行對children
的測量和布局熔恢,同時(shí)這個(gè)方法也是這里的分析重點(diǎn)脐湾。
??系統(tǒng)的LayoutManager
的onLayoutChildren
方法是一個(gè)空方法,所以需要LayoutManager
的子類自己來實(shí)現(xiàn)叙淌。從這里秤掌,我們可以得出兩個(gè)點(diǎn)。
- 子類
LayoutManager
需要自己實(shí)現(xiàn)onLayoutChildren
方法鹰霍,從而來決定RecyclerView
在該LayoutManager
的策略下闻鉴,應(yīng)該怎么布局。從這里茂洒,我們看出來RecyclerView
的靈活性孟岛。LayoutManager
類似于ViewGroup
,將onLayoutChildren
方法(ViewGroup
是onLayout
方法)公開出來,這種模式在Android中很常見的督勺。
??這里渠羞,我先不對onLayoutChildren
方法進(jìn)行展開,待會會詳細(xì)的分析智哀。
??接下來次询,我們來分析第三種情況--沒有開啟自動(dòng)測量。
(3).沒有開啟自動(dòng)測量
??我們先來看看這一塊的代碼瓷叫。
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(也就是調(diào)用了setHasFixedSize
方法)送巡,將直接調(diào)用LayoutManager
的onMeasure
方法進(jìn)行測量。- 如果
mHasFixedSize
為false盒卸,同時(shí)此時(shí)如果有數(shù)據(jù)更新骗爆,先處理數(shù)據(jù)更新的事務(wù),然后調(diào)用LayoutManager
的onMeasure
方法進(jìn)行測量
??通過上面的描述蔽介,我們知道淮腾,如果未開啟自動(dòng)測量,那么肯定會調(diào)用LayoutManager
的onMeasure
方法來進(jìn)行測量屉佳,這就是LayoutManager
的onMeasure
方法的作用谷朝。
??至于onMeasure
方法怎么進(jìn)行測量,那就得看LayoutManager
的實(shí)現(xiàn)類武花。在這里圆凰,我們就不進(jìn)行深入的追究了。
3. layout
??measure
過程分析的差不多了体箕,接下來我們就該分析第二個(gè)過程--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
方法本身沒有做多少的事情,重點(diǎn)還是在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
方法也是非常的簡單跃须,這個(gè)方法保證RecyclerView
必須經(jīng)歷三個(gè)過程--dispatchLayoutStep1
、dispatchLayoutStep2
娃兽、dispatchLayoutStep3
菇民。
??同時(shí),在后面的文章中投储,你會看到dispatchLayout
方法其實(shí)還為RecyclerView
節(jié)省了很多步驟第练,也就是說,在RecyclerView
經(jīng)歷一次完整的dispatchLayout
之后玛荞,后續(xù)如果參數(shù)有所變化時(shí)娇掏,可能只會經(jīng)歷最后的1步或者2步。當(dāng)然這些都是后話了??勋眯。
??對于dispatchLayoutStep1
和dispatchLayoutStep2
方法婴梧,我們前面已經(jīng)講解了,這里就不做過多的解釋了客蹋。這里塞蹭,我們就簡單的看一下dispatchLayoutStep3
方法吧。
private void dispatchLayoutStep3() {
// ······
mState.mLayoutStep = State.STEP_START;
// ······
}
??為什么這里只是簡單看一下dispatchLayoutStep3
方法呢嚼酝?因?yàn)檫@個(gè)方法主要是做Item的動(dòng)畫浮还,也就是我們熟知的ItemAnimator
的執(zhí)行竟坛,而本文不對動(dòng)畫進(jìn)行展開闽巩,所以先省略動(dòng)畫部分钧舌。
??在這里,我們需要關(guān)注dispatchLayoutStep3
方法的是涎跨,它將mLayoutStep
重置為了State.STEP_START
洼冻。也就是說如果下一次重新開始dispatchLayout
的話,那么肯定會經(jīng)歷dispatchLayoutStep1
隅很、dispatchLayoutStep2
撞牢、dispatchLayoutStep3
三個(gè)方法。
??以上就是RecyclerView
的layout過程叔营,是不是感覺非常的簡單屋彪?RecyclerView
跟其他ViewGroup
不同的地方在于,如果開啟了自動(dòng)測量绒尊,在measure
階段畜挥,已經(jīng)將Children
布局完成了;如果沒有開啟自動(dòng)測量婴谱,則在layout
階段才布局Children
蟹但。
4. draw
??接下來,我們來分析三大流程的最后一個(gè)階段--draw
谭羔。在正式分析draw過程之前华糖,我先來對RecyclerView
的draw
做一個(gè)概述。
??RecyclerView
分為三步瘟裸,我們來看看:
- 調(diào)用
super.draw
方法客叉。這里主要做了兩件事:1. 將Children
的繪制分發(fā)給ViewGroup
;2. 將分割線的繪制分發(fā)給ItemDecoration
。- 如果需要的話话告,調(diào)用
ItemDecoration
的onDrawOver
方法十办。通過這個(gè)方法,我們在每個(gè)ItemView
上面畫上很多東西超棺。- 如果
RecyclerView
調(diào)用了setClipToPadding
,會實(shí)現(xiàn)一種特殊的滑動(dòng)效果--每個(gè)ItemView可以滑動(dòng)到padding區(qū)域向族。
??我們來看看這部分的代碼:
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.
// ······
}
??熟悉三大流程的同學(xué),肯定知道第一步會回調(diào)到onDraw
方法里面棠绘,而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
方法是不是非常的簡單?onDraw
方法主要是將ItemDecoration
的繪制分發(fā)到ItemDecoration
的onDraw
方法里面去氧苍。從這里夜矗,我們可以看出來,RecyclerView
的設(shè)計(jì)實(shí)在是太靈活了让虐!
??至于其余兩步都比較簡單紊撕,這里就不詳細(xì)分析了。不過赡突,從這里对扶,我們終于明白了ItemDecoration
的onDraw
方法和onDrawOver
方法的區(qū)別区赵。
5. LayoutManager的onLayoutChildren方法
??從整體來說,RecyclerView
的三大流程還是比較簡單浪南,不過在整個(gè)過程中笼才,我們似乎忽略了一個(gè)過程--那就是RecyclerView
到底是怎么layout children
的?
??前面在介紹dispatchLayoutStep2
方法時(shí)络凿,只是簡單的介紹了骡送,RecyclerView
通過調(diào)用LayoutManager
的onLayoutChildren
方法。LayoutManager
本身對這個(gè)方法沒有進(jìn)行實(shí)現(xiàn)絮记,所以必須得看看它的子類摔踱,這里我們就來看看LinearLayoutManager
。
??由于LinearLayoutManager
的onLayoutChildren
方法比較長怨愤,這里不可能貼出完整的代碼昌渤,所以這里我先對這個(gè)方法做一個(gè)簡單的概述,方便大家理解憔四。
- 確定錨點(diǎn)的信息膀息,這里面的信息包括:1.
Children
的布局方向,有start和end兩個(gè)方向了赵;2.mPosition
和mCoordinate
潜支,分別表示Children
開始填充的position和坐標(biāo)。- 調(diào)用
detachAndScrapAttachedViews
方法柿汛,detach
掉或者remove
掉RecyclerView
的Children
冗酿。這一點(diǎn)本來不在本文的講解范圍內(nèi),但是為了后續(xù)對RecyclerView
的緩存機(jī)制有更好的了解络断,這里特別的提醒一下裁替。- 根據(jù)錨點(diǎn)信息,調(diào)用
fill
方法進(jìn)行Children
的填充貌笨。這個(gè)過程中根據(jù)錨點(diǎn)信息的不同弱判,可能會調(diào)用兩次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;
}
}
// ······
}
??相信從上面的代碼都可以找出每一步的執(zhí)行〔現(xiàn)在,我們來詳細(xì)分析每一步膀跌。首先來看第一步--確定錨點(diǎn)的信息
遭商。
??要想看錨點(diǎn)信息的計(jì)算過程,我們可以從updateAnchorInfoForLayout
方法里面來找出答案捅伤,我們來看看updateAnchorInfoForLayout
方法:
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
AnchorInfo anchorInfo) {
// 第一種計(jì)算方式
if (updateAnchorFromPendingData(state, anchorInfo)) {
return;
}
// 第二種計(jì)算方式
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
return;
}
// 第三種計(jì)算方式
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
??我相信通過上面的代碼注釋劫流,大家都能明白updateAnchorInfoForLayout
方法到底干了嘛,這里我簡單分析一下這三種確定所做的含義,具體是怎么做的祠汇,這里就不討論仍秤,因?yàn)檫@里面的細(xì)節(jié)太多了,深入的討論容易將我們聰明無比的大腦搞暈??座哩。
- 第一種計(jì)算方式徒扶,表示含義有兩種:1.
RecyclerView
被重建粮彤,期間回調(diào)了onSaveInstanceState
方法根穷,所以目的是為了恢復(fù)上次的布局;2.RecyclerView
調(diào)用了scrollToPosition
之類的方法导坟,所以目的是讓
RecyclerView
滾到準(zhǔn)確的位置上去屿良。所以,錨點(diǎn)的信息根據(jù)上面的兩種情況來計(jì)算惫周。- 第二種計(jì)算方法尘惧,從
Children
上面來計(jì)算錨點(diǎn)信息。這種計(jì)算方式也有兩種情況:1. 如果當(dāng)前有擁有焦點(diǎn)的Child
递递,那么有當(dāng)前有焦點(diǎn)的Child的位置來計(jì)算錨點(diǎn)喷橙;2. 如果沒有child擁有焦點(diǎn),那么根據(jù)布局方向(此時(shí)布局方向由mLayoutFromEnd
來決定)獲取可見的第一個(gè)ItemView
或者最后一個(gè)ItemView
登舞。- 如果前面兩種方式都計(jì)算失敗了贰逾,那么采用第三種計(jì)算方式,也就是默認(rèn)的計(jì)算方式菠秒。
??以上就是updateAnchorInfoForLayout
方法所做的事情疙剑,這里就不詳細(xì)糾結(jié)每種計(jì)算方式的細(xì)節(jié),有興趣的同學(xué)可以看看践叠。
??至于第二步言缤,調(diào)用detachAndScrapAttachedViews
方法對所有的ItemView
進(jìn)行回收,這部分的內(nèi)容屬于RecyclerView
緩存機(jī)制的部分,本文先在這里埋下一個(gè)伏筆禁灼,后續(xù)專門講解RecyclerView
會詳細(xì)的分析它管挟,所以這里就不講解了。
??接下來我們來看看第三步弄捕,也就是調(diào)用fill
方法來填充Children
哮独。在正式分析填充過程時(shí),我們先來看一張圖片:
??圖片的原圖出自RecyclerView剖析,如有侵權(quán)察藐,請聯(lián)系我皮璧。
??上圖形象的展現(xiàn)出三種
fill
的情況。其中分飞,我們可以看到第三種情況悴务,fill
方法被調(diào)用了兩次。??我們看看
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
方法的代碼比較長,其實(shí)都是來計(jì)算可填充的空間讯檐,真正填充Child
的地方是layoutChunk
方法羡疗。我們來看看layoutChunk
方法。
??由于layoutChunk
方法比較長别洪,這里我就不完整的展示叨恨,為了方便理解,我對這個(gè)方法做一個(gè)簡單的概述挖垛,讓大家有一個(gè)大概的理解痒钝。
- 調(diào)用
LayoutState
的next
方法獲得一個(gè)ItemView
。千萬別小看這個(gè)next
方法痢毒,RecyclerView
緩存機(jī)制的起點(diǎn)就是從這個(gè)方法開始送矩,可想而知,這個(gè)方法到底為我們做了多少事情哪替。- 如果
RecyclerView
是第一次布局Children的話(layoutState.mScrapList == null
為true)栋荸,會先調(diào)用addView,將View
添加到RecyclerView
里面去凭舶。- 調(diào)用
measureChildWithMargins
方法晌块,測量每個(gè)ItemView
的寬高。注意這個(gè)方法測量ItemView的寬高考慮到了兩個(gè)因素:1.margin屬性帅霜;2.ItemDecoration
的offset
匆背。- 調(diào)用
layoutDecoratedWithMargins
方法,布局ItemView
义屏。這里也考慮上面的兩個(gè)因素的靠汁。
??至于每一步具體干了嘛,這里就不詳細(xì)的解釋闽铐,都是一些基本操作蝶怔,有興趣的同學(xué)可以看看。
??綜上所述兄墅,便是LayoutManager
的onLayoutChildren
方法整個(gè)執(zhí)行過程踢星,思路還是比較簡單的。
6. 總結(jié)
??本文到此就差不多了隙咸,在最后沐悦,我做一個(gè)簡單的總結(jié)。
RecyclerView
的measure
過程分為三種情況五督,每種情況都有執(zhí)行過程藏否。通常來說,我們都會走自動(dòng)測量的過程充包。- 自動(dòng)測量里面需要分清楚
mState.mLayoutStep
狀態(tài)值副签,因?yàn)楦鶕?jù)不同的狀態(tài)值調(diào)用不同的dispatchLayoutStep
方法遥椿。layout
過程也根據(jù)mState.mLayoutStep
狀態(tài)來調(diào)用不同的dispatchLayoutStep
方法draw
過程主要做了四件事:1.繪制ItemDecoration
的onDraw
部分;2.繪制Children
;3.繪制ItemDecoration
的drawOver
部分;4. 根據(jù)mClipToPadding
的值來判斷是否進(jìn)行特殊繪制。