RecyclerView刷新機(jī)制

前面分析了RecyclerView的基本結(jié)構(gòu)
本文繼續(xù)來看一下RecyclerView是如何完成UI的刷新以及在滑動時子View的添加邏輯翰守。

本文會從源碼分析兩件事 :

  1. adapter.notifyXXX()時RecyclerView的UI刷新的邏輯,即子View是如何添加到RecyclerView中的吨灭。
  2. 在數(shù)據(jù)存在的情況下怀酷,滑動RecyclerView子View是如何添加到RecyclerView并滑動的杂伟。

本文不會涉及到RecyclerView的動畫方椎,動畫的實(shí)現(xiàn)會專門在一篇文章中分析钦睡。

adapter.notifyDataSetChanged()引起的刷新

我們假設(shè)RecyclerView在初始狀態(tài)是沒有數(shù)據(jù)的墓律,然后往數(shù)據(jù)源中加入數(shù)據(jù)后膀估,調(diào)用adapter.notifyDataSetChanged()來引起RecyclerView的刷新:

data.addAll(datas)
adapter.notifyDataSetChanged()

用圖描述就是下面兩個狀態(tài)的轉(zhuǎn)換:


adapter.notifyDataSetChanged.png

接下來就來分析這個變化的源碼,在上一篇文章中已經(jīng)解釋過只锻,adapter.notifyDataSetChanged()時玖像,會引起RecyclerView重新布局(requestLayout),RecyclerViewonMeasure就不看了齐饮,核心邏輯不在這里捐寥。因此從onLayout()方法開始看:

RecyclerView.onLayout

這個方法直接調(diào)用了dispatchLayout:

void dispatchLayout() {
    ...
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        dispatchLayoutStep2();
    } else if (數(shù)據(jù)變化 || 布局變化) {
        dispatchLayoutStep2();
    }
    dispatchLayoutStep3();
}

上面我裁剪掉了一些代碼,可以看到整個布局過程總共分為3步, 下面是這3步對應(yīng)的方法:

STEP_START ->  dispatchLayoutStep1()
STEP_LAYOUT -> dispatchLayoutStep2()
STEP_ANIMATIONS -> dispatchLayoutStep2(), dispatchLayoutStep3()

第一步STEP_START主要是來存儲當(dāng)前子View的狀態(tài)并確定是否要執(zhí)行動畫祖驱。這一步就不細(xì)看了握恳。 而第3步STEP_ANIMATIONS是來執(zhí)行動畫的,本文也不分析了捺僻,本文主要來看一下第二步STEP_LAYOUT,即dispatchLayoutStep2():

dispatchLayoutStep2()

先來看一下這個方法的大致執(zhí)行邏輯:

private void dispatchLayoutStep2() {  
    startInterceptRequestLayout(); //方法執(zhí)行期間不能重入
    ...
    //設(shè)置好初始狀態(tài)
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
    mState.mInPreLayout = false;

    mLayout.onLayoutChildren(mRecycler, mState); //調(diào)用布局管理器去布局

    mState.mStructureChanged = false;
    mPendingSavedState = null;
    ...
    mState.mLayoutStep = State.STEP_ANIMATIONS; //接下來執(zhí)行布局的第三步

    stopInterceptRequestLayout(false);
}

這里有一個mState乡洼,它是一個RecyclerView.State對象。顧名思義它是用來保存RecyclerView狀態(tài)的一個對象匕坯,主要是用在LayoutManager束昵、Adapter等組件之間共享RecyclerView狀態(tài)的「鹁可以看到這個方法將布局的工作交給了mLayout锹雏。這里它的實(shí)例是LinearLayoutManager,因此接下來看一下LinearLayoutManager.onLayoutChildren():

LinearLayoutManager.onLayoutChildren()

這個方法也挺長的术奖,就不展示具體源碼了礁遵。不過布局邏輯還是很簡單的:

  1. 確定錨點(diǎn)(Anchor)View, 設(shè)置好AnchorInfo
  2. 根據(jù)錨點(diǎn)View確定有多少布局空間mLayoutState.mAvailable可用
  3. 根據(jù)當(dāng)前設(shè)置的LinearLayoutManager的方向開始擺放子View

接下來就從源碼來看這三步。

確定錨點(diǎn)View

錨點(diǎn)View大部分是通過updateAnchorFromChildren方法確定的,這個方法主要是獲取一個View采记,把它的信息設(shè)置到AnchorInfo中 :

mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout   // 即和你是否在 manifest中設(shè)置了布局 rtl 有關(guān)

private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
    ...
    View referenceChild = anchorInfo.mLayoutFromEnd
            ? findReferenceChildClosestToEnd(recycler, state) //如果是從end(尾部)位置開始布局佣耐,那就找最接近end的那個位置的View作為錨點(diǎn)View
            : findReferenceChildClosestToStart(recycler, state); //如果是從start(頭部)位置開始布局,那就找最接近start的那個位置的View作為錨點(diǎn)View

    if (referenceChild != null) {
        anchorInfo.assignFromView(referenceChild, getPosition(referenceChild)); 
        ...
        return true;
    }
    return false;
}

即唧龄, 如果是start to end, 那么就找最接近start(RecyclerView頭部)的View作為布局的錨點(diǎn)View兼砖。如果是end to start (rtl), 就找最接近end的View作為布局的錨點(diǎn)。

AnchorInfo最重要的兩個屬性時mCoordinatemPosition,找到錨點(diǎn)View后就會通過anchorInfo.assignFromView()方法來設(shè)置這兩個屬性:

public void assignFromView(View child, int position) {
    if (mLayoutFromEnd) {
        mCoordinate = mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper.getTotalSpaceChange();
    } else {
        mCoordinate = mOrientationHelper.getDecoratedStart(child);  
    }
    mPosition = position;
}
  • mCoordinate其實(shí)就是錨點(diǎn)ViewY(X)坐標(biāo)去掉RecyclerView的padding掖鱼。
  • mPosition其實(shí)就是錨點(diǎn)View的位置然走。

確定有多少布局空間可用并擺放子View

當(dāng)確定好AnchorInfo后,需要根據(jù)AnchorInfo來確定RecyclerView當(dāng)前可用于布局的空間,然后來擺放子View戏挡。以布局方向?yàn)?code>start to end (正常方向)為例, 這里的錨點(diǎn)View其實(shí)是RecyclerView最頂部的View:

    // fill towards end  (1)
    updateLayoutStateToFillEnd(mAnchorInfo); //確定AnchorView到RecyclerView的底部的布局可用空間
    ...
    fill(recycler, mLayoutState, state, false); //填充view, 從 AnchorView 到RecyclerView的底部
    endOffset = mLayoutState.mOffset; 

    // fill towards start (2)
    updateLayoutStateToFillStart(mAnchorInfo); //確定AnchorView到RecyclerView的頂部的布局可用空間
    ...
    fill(recycler, mLayoutState, state, false); //填充view,從 AnchorView 到RecyclerView的頂部

上面我標(biāo)注了(1)和(2), 1次布局是由這兩部分組成的, 具體如下圖所示 :

RecyclerView的布局步驟.png

然后我們來看一下fill towards end的實(shí)現(xiàn):

fill towards end

確定可用布局空間

fill之前,需要先確定從錨點(diǎn)ViewRecyclerView底部有多少可用空間晨仑。是通過updateLayoutStateToFillEnd方法:

updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);

void updateLayoutStateToFillEnd(int itemPosition, int offset) {
    mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
    ...
    mLayoutState.mCurrentPosition = itemPosition;
    mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
    mLayoutState.mOffset = offset;
    mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
}

mLayoutStateLinearLayoutManager用來保存布局狀態(tài)的一個對象褐墅。mLayoutState.mAvailable就是用來表示有多少空間可用來布局mOrientationHelper.getEndAfterPadding() - offset其實(shí)大致可以理解為RecyclerView的高度洪己。所以這里可用布局空間mLayoutState.mAvailable就是RecyclerView的高度

擺放子view

接下來繼續(xù)看LinearLayoutManager.fill()方法妥凳,這個方法是布局的核心方法,是用來向RecyclerView中添加子View的方法:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
    final int start = layoutState.mAvailable;  //前面分析答捕,其實(shí)就是RecyclerView的高度
    ...
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;  //extra 是你設(shè)置的額外布局的范圍, 這個一般不推薦設(shè)置
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult; //保存布局一個child view后的結(jié)果
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { //有剩余空間的話逝钥,就一直添加 childView
        layoutChunkResult.resetInternal();
        ...
        layoutChunk(recycler, state, layoutState, layoutChunkResult);   //布局子View的核心方法
        ...
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; // 一次 layoutChunk 消耗了多少空間
        ...
        子View的回收工作
    }
    ...
}

這里我們不看子View回收邏輯,會在單獨(dú)的一篇文章中講拱镐。 即這個方法的核心是調(diào)用layoutChunk()來不斷消耗layoutState.mAvailable,直到消耗完畢艘款。繼續(xù)看一下layoutChunk()方法, 這個方法的主要邏輯是:

  1. Recycler中獲取一個View
  2. 添加到RecyclerView
  3. 調(diào)整View的布局參數(shù),調(diào)用其measure沃琅、layout方法哗咆。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);  //這個方法會向 recycler view 要一個holder 
        ...
        if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { //根據(jù)布局方向,添加到不同的位置
            addView(view);   
        } else {
            addView(view, 0);
        }
        measureChildWithMargins(view, 0, 0);    //調(diào)用view的measure
        
        ...measure后確定布局參數(shù) left/top/right/bottom

        layoutDecoratedWithMargins(view, left, top, right, bottom); //調(diào)用view的layout
        ...
    }

到這里其實(shí)就完成了上面的fill towards end:

    updateLayoutStateToFillEnd(mAnchorInfo); //確定布局可用空間
    ...
    fill(recycler, mLayoutState, state, false); //填充view

fill towards start就是從錨點(diǎn)ViewRecyclerView頂部來擺放子View益眉,具體邏輯類似fill towards end晌柬,就不細(xì)看了。

RecyclerView滑動時的刷新邏輯

接下來我們再來分析一下在不加載新的數(shù)據(jù)情況下郭脂,RecyclerView在滑動時是如何展示子View的年碘,即下面這種狀態(tài) :

RecyclerView滑動時的狀態(tài).png

下面就來分析一下3、4號和12展鸡、13號是如何展示的屿衅。

RecyclerViewOnTouchEvent對滑動事件做了監(jiān)聽,然后派發(fā)到scrollStep()方法:

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
    startInterceptRequestLayout(); //處理滑動時不能重入
    ...
    if (dx != 0) {
        consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
    }
    if (dy != 0) {
        consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }
    ...
    stopInterceptRequestLayout(false);

    if (consumed != null) { //記錄消耗
        consumed[0] = consumedX;
        consumed[1] = consumedY;
    }
}

即把滑動的處理交給了mLayout, 這里繼續(xù)看LinearLayoutManager.scrollVerticallyBy, 它直接調(diào)用了scrollBy(), 這個方法就是LinearLayoutManager處理滾動的核心方法娱颊。

LinearLayoutManager.scrollBy

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    final int absDy = Math.abs(dy);
    updateLayoutState(layoutDirection, absDy, true, state); //確定可用布局空間
    final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); //擺放子View
    ....
    final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
    mOrientationHelper.offsetChildren(-scrolled); // 滾動 RecyclerView
    ...
}

這個方法的主要執(zhí)行邏輯是:

  1. 根據(jù)布局方向和滑動的距離來確定可用布局空間mLayoutState.mAvailable
  2. 調(diào)用fill()來擺放子View
  3. 滾動RecyclerView

fill()的邏輯這里我們就不再看了傲诵,因此我們主要看一下1 和 3

根據(jù)布局方向和滑動的距離來確定可用布局空間

以向下滾動為為例箱硕,看一下updateLayoutState方法:

// requiredSpace是滑動的距離;  canUseExistingSpace是true
void updateLayoutState(int layoutDirection, int requiredSpace,boolean canUseExistingSpace, RecyclerView.State state) {

    if (layoutDirection == LayoutState.LAYOUT_END) { //滾動方法為向下
        final View child = getChildClosestToEnd(); //獲得RecyclerView底部的View
        ...
        mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; //view的位置
        mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child); //view的偏移 offset
        scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding();
    } else {
       ...
    }
    
    mLayoutState.mAvailable = requiredSpace;  
    if (canUseExistingSpace)  mLayoutState.mAvailable -= scrollingOffset;
    mLayoutState.mScrollingOffset = scrollingOffset;
}

所以可用的布局空間就是滑動的距離拴竹。那mLayoutState.mScrollingOffset是什么呢?

上面方法它的值是mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding();剧罩,其實(shí)就是(childView的bottom + childView的margin) - RecyclerView的Padding栓拜。 什么意思呢? 看下圖:

RecyclerView滾動時可使用的布局空間.png

RecyclerView的padding我沒標(biāo)注,不過相信上圖可以讓你理解: 滑動布局可用空間mLayoutState.mAvailable。同時mLayoutState.mScrollingOffset就是滾動的距離 - mLayoutState.mAvailable

所以 consumed也可以理解:

int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);   

fill()就不看了幕与。子View擺放完畢后就要滾動布局展示剛剛擺放好的子View挑势。這是依靠的mOrientationHelper.offsetChildren(-scrolled), 繼續(xù)看一下是如何執(zhí)行RecyclerView的滾動的

滾動RecyclerView

對于RecyclerView的滾動,最終調(diào)用到了RecyclerView.offsetChildrenVertical():

//dy這里就是滾動的距離
public void offsetChildrenVertical(@Px int dy) {
    final int childCount = mChildHelper.getChildCount();
    for (int i = 0; i < childCount; i++) {
        mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
    }
}

可以看到邏輯很簡單,就是改變當(dāng)前子View布局的top和bottom來達(dá)到滾動的效果啦鸣。

本文就分析到這里潮饱。接下來會繼續(xù)分析RecyclerView的復(fù)用邏輯。

歡迎關(guān)注我的Android進(jìn)階計劃诫给∠憷看更多干貨

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市中狂,隨后出現(xiàn)的幾起案子凫碌,更是在濱河造成了極大的恐慌,老刑警劉巖胃榕,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盛险,死亡現(xiàn)場離奇詭異,居然都是意外死亡勋又,警方通過查閱死者的電腦和手機(jī)苦掘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赐写,“玉大人鸟蜡,你說我怎么就攤上這事⊥ρ” “怎么了揉忘?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長端铛。 經(jīng)常有香客問我泣矛,道長,這世上最難降的妖魔是什么禾蚕? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任您朽,我火速辦了婚禮,結(jié)果婚禮上换淆,老公的妹妹穿的比我還像新娘哗总。我一直安慰自己,他們只是感情好倍试,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布讯屈。 她就那樣靜靜地躺著,像睡著了一般县习。 火紅的嫁衣襯著肌膚如雪涮母。 梳的紋絲不亂的頭發(fā)上谆趾,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天,我揣著相機(jī)與錄音叛本,去河邊找鬼沪蓬。 笑死,一個胖子當(dāng)著我的面吹牛来候,可吹牛的內(nèi)容都是我干的跷叉。 我是一名探鬼主播,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼吠勘,長吁一口氣:“原來是場噩夢啊……” “哼性芬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起剧防,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辫樱,沒想到半個月后峭拘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡狮暑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年鸡挠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片搬男。...
    茶點(diǎn)故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡拣展,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缔逛,到底是詐尸還是另有隱情备埃,我是刑警寧澤,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布褐奴,位于F島的核電站按脚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏敦冬。R本人自食惡果不足惜辅搬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望脖旱。 院中可真熱鬧堪遂,春花似錦、人聲如沸萌庆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽踊兜。三九已至竿滨,卻和暖如春佳恬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背于游。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工毁葱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贰剥。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓倾剿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蚌成。 傳聞我的和親對象是個殘疾皇子前痘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評論 2 350

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