前面分析了RecyclerView的基本結(jié)構(gòu)
本文繼續(xù)來看一下RecyclerView
是如何完成UI的刷新
以及在滑動時子View的添加邏輯
翰守。
本文會從源碼分析兩件事 :
-
adapter.notifyXXX()
時RecyclerView的UI刷新的邏輯,即子View
是如何添加到RecyclerView
中的吨灭。 - 在數(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)換:
接下來就來分析這個變化的源碼,在上一篇文章中已經(jīng)解釋過只锻,adapter.notifyDataSetChanged()
時玖像,會引起RecyclerView
重新布局(requestLayout
),RecyclerView
的onMeasure
就不看了齐饮,核心邏輯不在這里捐寥。因此從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()
這個方法也挺長的术奖,就不展示具體源碼了礁遵。不過布局邏輯還是很簡單的:
- 確定錨點(diǎn)
(Anchor)View
, 設(shè)置好AnchorInfo
- 根據(jù)
錨點(diǎn)View
確定有多少布局空間mLayoutState.mAvailable
可用 - 根據(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
最重要的兩個屬性時mCoordinate
和mPosition
,找到錨點(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)View
的Y(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次布局是由這兩部分組成的, 具體如下圖所示 :
然后我們來看一下fill towards end
的實(shí)現(xiàn):
fill towards end
確定可用布局空間
在fill
之前,需要先確定從錨點(diǎn)View
到RecyclerView底部
有多少可用空間晨仑。是通過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;
}
mLayoutState
是LinearLayoutManager
用來保存布局狀態(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()方法
, 這個方法的主要邏輯是:
- 從
Recycler
中獲取一個View
- 添加到
RecyclerView
中 - 調(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)View
向RecyclerView頂部
來擺放子View益眉,具體邏輯類似fill towards end
晌柬,就不細(xì)看了。
RecyclerView滑動時的刷新邏輯
接下來我們再來分析一下在不加載新的數(shù)據(jù)情況下郭脂,RecyclerView
在滑動時是如何展示子View
的年碘,即下面這種狀態(tài) :
下面就來分析一下3、4
號和12展鸡、13
號是如何展示的屿衅。
RecyclerView
在OnTouchEvent
對滑動事件做了監(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í)行邏輯是:
- 根據(jù)布局方向和滑動的距離來確定可用布局空間
mLayoutState.mAvailable
- 調(diào)用
fill()
來擺放子View - 滾動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的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)階計劃诫给∠憷看更多干貨