前面分析了RecyclerView的基本結構 本文繼續來看一下
RecyclerView
是如何完成UI的重新整理
以及在滑動時子View的新增邏輯
。
本文會從原始碼分析兩件事 :
adapter.notifyXXX()
時RecyclerView的UI重新整理的邏輯,即子View
是如何新增到RecyclerView
中的。- 在資料存在的情況下,滑動
RecyclerView
時子View
是如何新增到RecyclerView
並滑動的。
本文不會涉及到RecyclerView
的動畫,動畫的實現會專門在一篇文章中分析。
adapter.notifyDataSetChanged()
引起的重新整理
我們假設RecyclerView
在初始狀態是沒有資料的,然後往資料來源中加入資料後,呼叫adapter.notifyDataSetChanged()
來引起RecyclerView
的重新整理:
data.addAll(datas)
adapter.notifyDataSetChanged()
複製程式碼
用圖描述就是下面兩個狀態的轉換:
接下來就來分析這個變化的原始碼,在上一篇文章中已經解釋過,adapter.notifyDataSetChanged()
時,會引起RecyclerView
重新佈局(requestLayout
),RecyclerView
的onMeasure
就不看了,核心邏輯不在這裡。因此從onLayout()
方法開始看:
RecyclerView.onLayout
這個方法直接呼叫了dispatchLayout
:
void dispatchLayout() {
...
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
dispatchLayoutStep2();
} else if (資料變化 || 佈局變化) {
dispatchLayoutStep2();
}
dispatchLayoutStep3();
}
複製程式碼
上面我裁剪掉了一些程式碼,可以看到整個佈局過程總共分為3步, 下面是這3步對應的方法:
STEP_START -> dispatchLayoutStep1()
STEP_LAYOUT -> dispatchLayoutStep2()
STEP_ANIMATIONS -> dispatchLayoutStep2(), dispatchLayoutStep3()
複製程式碼
第一步STEP_START
主要是來儲存當前子View
的狀態並確定是否要執行動畫。這一步就不細看了。 而第3步STEP_ANIMATIONS
是來執行動畫的,本文也不分析了,本文主要來看一下第二步STEP_LAYOUT
,即dispatchLayoutStep2()
:
dispatchLayoutStep2()
先來看一下這個方法的大致執行邏輯:
private void dispatchLayoutStep2() {
startInterceptRequestLayout(); //方法執行期間不能重入
...
//設定好初始狀態
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState); //呼叫佈局管理器去佈局
mState.mStructureChanged = false;
mPendingSavedState = null;
...
mState.mLayoutStep = State.STEP_ANIMATIONS; //接下來執行佈局的第三步
stopInterceptRequestLayout(false);
}
複製程式碼
這裡有一個mState
,它是一個RecyclerView.State
物件。顧名思義它是用來儲存RecyclerView
狀態的一個物件,主要是用在LayoutManager、Adapter等
元件之間共享RecyclerView狀態
的。可以看到這個方法將佈局的工作交給了mLayout
。這裡它的例項是LinearLayoutManager
,因此接下來看一下LinearLayoutManager.onLayoutChildren()
:
LinearLayoutManager.onLayoutChildren()
這個方法也挺長的,就不展示具體原始碼了。不過佈局邏輯還是很簡單的:
- 確定錨點
(Anchor)View
, 設定好AnchorInfo
- 根據
錨點View
確定有多少佈局空間mLayoutState.mAvailable
可用 - 根據當前設定的
LinearLayoutManager
的方向開始擺放子View
接下來就從原始碼來看這三步。
確定錨點View
錨點View
大部分是通過updateAnchorFromChildren
方法確定的,這個方法主要是獲取一個View,把它的資訊設定到AnchorInfo
中 :
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout // 即和你是否在 manifest中設定了佈局 rtl 有關
private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
...
View referenceChild = anchorInfo.mLayoutFromEnd
? findReferenceChildClosestToEnd(recycler, state) //如果是從end(尾部)位置開始佈局,那就找最接近end的那個位置的View作為錨點View
: findReferenceChildClosestToStart(recycler, state); //如果是從start(頭部)位置開始佈局,那就找最接近start的那個位置的View作為錨點View
if (referenceChild != null) {
anchorInfo.assignFromView(referenceChild, getPosition(referenceChild));
...
return true;
}
return false;
}
複製程式碼
即, 如果是start to end
, 那麼就找最接近start(RecyclerView頭部)的View作為佈局的錨點View。如果是end to start (rtl)
, 就找最接近end的View作為佈局的錨點。
AnchorInfo
最重要的兩個屬性時mCoordinate
和mPosition
,找到錨點View後就會通過anchorInfo.assignFromView()
方法來設定這兩個屬性:
public void assignFromView(View child, int position) {
if (mLayoutFromEnd) {
mCoordinate = mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper.getTotalSpaceChange();
} else {
mCoordinate = mOrientationHelper.getDecoratedStart(child);
}
mPosition = position;
}
複製程式碼
mCoordinate
其實就是錨點View
的Y(X)
座標去掉RecyclerView
的padding。mPosition
其實就是錨點View
的位置。
確定有多少佈局空間可用並擺放子View
當確定好AnchorInfo
後,需要根據AnchorInfo
來確定RecyclerView
當前可用於佈局的空間,然後來擺放子View。以佈局方向為start to end (正常方向)
為例, 這裡的錨點View
其實是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的頂部
複製程式碼
上面我標註了(1)和(2)
, 1次佈局是由這兩部分組成的, 具體如下圖所示 :
然後我們來看一下fill towards end
的實現:
fill towards end
確定可用佈局空間
在fill
之前,需要先確定從錨點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
用來儲存佈局狀態的一個物件。mLayoutState.mAvailable
就是用來表示有多少空間可用來佈局
。mOrientationHelper.getEndAfterPadding() - offset
其實大致可以理解為RecyclerView
的高度。所以這裡可用佈局空間mLayoutState.mAvailable
就是RecyclerView的高度
擺放子view
接下來繼續看LinearLayoutManager.fill()
方法,這個方法是佈局的核心方法,是用來向RecyclerView
中新增子View的方法:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable; //前面分析,其實就是RecyclerView的高度
...
int remainingSpace = layoutState.mAvailable + layoutState.mExtra; //extra 是你設定的額外佈局的範圍, 這個一般不推薦設定
LayoutChunkResult layoutChunkResult = mLayoutChunkResult; //儲存佈局一個child view後的結果
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回收邏輯
,會在單獨的一篇文章中講。 即這個方法的核心是呼叫layoutChunk()
來不斷消耗layoutState.mAvailable
,直到消耗完畢。繼續看一下layoutChunk()方法
, 這個方法的主要邏輯是:
- 從
Recycler
中獲取一個View
- 新增到
RecyclerView
中 - 調整
View
的佈局引數,呼叫其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)) { //根據佈局方向,新增到不同的位置
addView(view);
} else {
addView(view, 0);
}
measureChildWithMargins(view, 0, 0); //呼叫view的measure
...measure後確定佈局引數 left/top/right/bottom
layoutDecoratedWithMargins(view, left, top, right, bottom); //呼叫view的layout
...
}
複製程式碼
到這裡其實就完成了上面的fill towards end
:
updateLayoutStateToFillEnd(mAnchorInfo); //確定佈局可用空間
...
fill(recycler, mLayoutState, state, false); //填充view
複製程式碼
fill towards start
就是從錨點View
向RecyclerView頂部
來擺放子View,具體邏輯類似fill towards end
,就不細看了。
RecyclerView滑動時的重新整理邏輯
接下來我們再來分析一下在不載入新的資料情況下,RecyclerView
在滑動時是如何展示子View
的,即下面這種狀態 :
下面就來分析一下3、4
號和12、13
號是如何展示的。
RecyclerView
在OnTouchEvent
對滑動事件做了監聽,然後派發到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
, 這裡繼續看LinearLayoutManager.scrollVerticallyBy
, 它直接呼叫了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
...
}
複製程式碼
這個方法的主要執行邏輯是:
- 根據佈局方向和滑動的距離來確定可用佈局空間
mLayoutState.mAvailable
- 呼叫
fill()
來擺放子View - 滾動RecyclerView
fill()
的邏輯這裡我們就不再看了,因此我們主要看一下1 和 3
。
根據佈局方向和滑動的距離來確定可用佈局空間
以向下滾動為為例,看一下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();
,其實就是(childView的bottom + childView的margin) - RecyclerView的Padding
。 什麼意思呢? 看下圖:
RecyclerView的padding
我沒標註,不過相信上圖可以讓你理解: 滑動佈局可用空間mLayoutState.mAvailable
。同時mLayoutState.mScrollingOffset
就是滾動的距離 - mLayoutState.mAvailable
所以 consumed
也可以理解:
int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
複製程式碼
fill()
就不看了。子View擺放完畢後就要滾動佈局展示剛剛擺放好的子View。這是依靠的mOrientationHelper.offsetChildren(-scrolled)
, 繼續看一下是如何執行RecyclerView
的滾動的
滾動RecyclerView
對於RecyclerView
的滾動,最終呼叫到了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);
}
}
複製程式碼
可以看到邏輯很簡單,就是改變當前子View佈局的top和bottom來達到滾動的效果。
本文就分析到這裡。接下來會繼續分析RecyclerView
的複用邏輯。 原始碼看的可能不是十分的細緻,如果有錯誤歡迎指出。
歡迎關注我的Android進階計劃。看更多幹貨