Android RecyclerView的繪制流程和緩存機(jī)制源碼剖析

前言

對于一個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é)如下:

mea-lay.png
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ī)制你雌。文章中可能有寫的不正確的地方,還望批評指正二汛!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末婿崭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子肴颊,更是在濱河造成了極大的恐慌氓栈,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件苫昌,死亡現(xiàn)場離奇詭異颤绕,居然都是意外死亡悄雅,警方通過查閱死者的電腦和手機(jī)挥下,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門司澎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人袜硫,你說我怎么就攤上這事〉猜ǎ” “怎么了婉陷?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵帚称,是天一觀的道長。 經(jīng)常有香客問我秽澳,道長闯睹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任担神,我火速辦了婚禮楼吃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘妄讯。我一直安慰自己孩锡,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布亥贸。 她就那樣靜靜地躺著躬窜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪炕置。 梳的紋絲不亂的頭發(fā)上荣挨,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機(jī)與錄音朴摊,去河邊找鬼垦沉。 笑死,一個胖子當(dāng)著我的面吹牛仍劈,可吹牛的內(nèi)容都是我干的厕倍。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼贩疙,長吁一口氣:“原來是場噩夢啊……” “哼讹弯!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起这溅,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤组民,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后悲靴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體臭胜,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年癞尚,在試婚紗的時候發(fā)現(xiàn)自己被綠了耸三。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡浇揩,死狀恐怖仪壮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情胳徽,我是刑警寧澤积锅,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布爽彤,位于F島的核電站,受9級特大地震影響缚陷,放射性物質(zhì)發(fā)生泄漏适篙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一箫爷、第九天 我趴在偏房一處隱蔽的房頂上張望匙瘪。 院中可真熱鬧,春花似錦蝶缀、人聲如沸丹喻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碍论。三九已至,卻和暖如春柄慰,著一層夾襖步出監(jiān)牢的瞬間鳍悠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工坐搔, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留藏研,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓概行,卻偏偏與公主長得像蠢挡,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子凳忙,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容