前面分析了RecyclerView的基本結構 本文繼續來看一下
RecyclerView
是如何完成UI的刷新
以及在滑動時子View的添加邏輯
。git
本文會從源碼分析兩件事 :github
adapter.notifyXXX()
時RecyclerView的UI刷新的邏輯,即子View
是如何添加到RecyclerView
中的。RecyclerView
時子View
是如何添加到RecyclerView
並滑動的。本文不會涉及到RecyclerView
的動畫,動畫的實現會專門在一篇文章中分析。bash
adapter.notifyDataSetChanged()
引發的刷新咱們假設RecyclerView
在初始狀態是沒有數據的,而後往數據源中加入數據後,調用adapter.notifyDataSetChanged()
來引發RecyclerView
的刷新:源碼分析
data.addAll(datas)
adapter.notifyDataSetChanged()
複製代碼
用圖描述就是下面兩個狀態的轉換:佈局
接下來就來分析這個變化的源碼,在上一篇文章中已經解釋過,adapter.notifyDataSetChanged()
時,會引發RecyclerView
從新佈局(requestLayout
),RecyclerView
的onMeasure
就不看了,核心邏輯不在這裏。所以從onLayout()
方法開始看:post
這個方法直接調用了dispatchLayout
:動畫
void dispatchLayout() {
...
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
dispatchLayoutStep2();
} else if (數據變化 || 佈局變化) {
dispatchLayoutStep2();
}
dispatchLayoutStep3();
}
複製代碼
上面我裁剪掉了一些代碼,能夠看到整個佈局過程總共分爲3步, 下面是這3步對應的方法:ui
STEP_START -> dispatchLayoutStep1()
STEP_LAYOUT -> dispatchLayoutStep2()
STEP_ANIMATIONS -> dispatchLayoutStep2(), dispatchLayoutStep3()
複製代碼
第一步STEP_START
主要是來存儲當前子View
的狀態並肯定是否要執行動畫。這一步就不細看了。 而第3步STEP_ANIMATIONS
是來執行動畫的,本文也不分析了,本文主要來看一下第二步STEP_LAYOUT
,即dispatchLayoutStep2()
:spa
先來看一下這個方法的大體執行邏輯:code
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()
:
這個方法也挺長的,就不展現具體源碼了。不過佈局邏輯仍是很簡單的:
(Anchor)View
, 設置好AnchorInfo
錨點View
肯定有多少佈局空間mLayoutState.mAvailable
可用LinearLayoutManager
的方向開始擺放子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
的位置。
當肯定好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
以前,須要先肯定從錨點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的高度
接下來繼續看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
在滑動時是如何展現子View
的,即下面這種狀態 :
下面就來分析一下三、4
號和十二、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
處理滾動的核心方法。
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()
來擺放子Viewfill()
的邏輯這裏咱們就再也不看了,所以咱們主要看一下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.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進階計劃。看更多幹貨