RecyclerView 源碼淺析測量 布局 繪制 預布局
前言
上一篇博客內容對 RecyclerView 回收復用機制相關源碼進行了分析,本博客從自定義 View 三大流程 measure、layout、draw 的角度繼續(xù)對 RecyclerView 相關部分源碼進行分析。
onMeasure
onMeasure 中的邏輯大體上分為三種情況,先來看下源碼:
RecyclerView.java
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
// 第一種情況:沒有設置 LayoutManager
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
// 第二種情況:設置的 LayoutManager 開啟自動測量
if (mLayout.isAutoMeasureEnabled()) {
// ...
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// ...
mLastAutoMeasureSkippedDueToExact =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (mLastAutoMeasureSkippedDueToExact || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// 需要二次測量
if (mLayout.shouldMeasureTwice()) {
// ...
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
// ...
} else { // 第三種情況:設置的 LayoutManager 沒有開啟自動測量
// ...
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// ...
mState.mInPreLayout = false; // clear
}
}
源碼只貼出了重要部分,稍微總結下:
- 沒有設置 LayoutManager 時,調用 defaultOnMeasure 方法;
- 設置 LayoutManager 并且開啟自動測量時,調用 LayoutManager 的 onMeasure 方法,并且會執(zhí)行 dispatchLayoutStep1()、dispatchLayoutStep2();
- 設置 LayoutManager 且沒有開始自動測量時,僅調用了 LayoutManager 的 onMeasure 方法;
先來看一下 LayoutManager 的 onMeasure 方法:
RecyclerView.java
public abstract static class LayoutManager{
// ...
public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,int heightSpec) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}
}
默認實現和第一種情況一樣,調用了 defaultOnMeasure 方法,而且 sdk 中給我們提供的三種 LayoutManager(LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager)均沒有重寫 onMeasure 方法。
接著就來看看 defaultOnMeasure 的源碼:
RecyclerView.java
void defaultOnMeasure(int widthSpec, int heightSpec) {
// 通過 LayoutManager.chooseSize 獲取的寬和高的值
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);
}
接著看一下 LayoutManager.chooseSize 是如何獲取寬高的:
public static int chooseSize(int spec, int desired, int min) {
final int mode = View.MeasureSpec.getMode(spec);
final int size = View.MeasureSpec.getSize(spec);
switch (mode) {
case View.MeasureSpec.EXACTLY:
return size;
case View.MeasureSpec.AT_MOST:
return Math.min(size, Math.max(desired, min));
case View.MeasureSpec.UNSPECIFIED:
default:
return Math.max(desired, min);
}
}
這段代碼就不用解釋了吧?自定義 View 時經常會根據 mode 不同來處理寬高的最終值。
測量這部分到目前為止的代碼都比較簡單,測量對于寬高這部分并沒有特殊處理,剩余重要邏輯都在 dispatchLayoutStep1()、dispatchLayoutStep2() 方法中,這里先不對其進行詳細解釋,因為下面的 onLayout 中還有一個 dispatchLayoutStep3() 方法。
稍微總結下,測量部分除非有特殊的自定義 LayoutManager 對寬高有自定義需求,一般情況都會走默認的 defaultOnMeasure 方法,和大部分自定義 View 相同根據 mode 確定寬高。
onLayout
自定義 View 的第二大流程 onLayout,直接看源碼:
RecyclerView.java
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout(); // 只有一處方法調用 分發(fā)布局
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
void dispatchLayout() {
// ...
// 在 onMeasure 中這個值被設置為 true
mState.mIsMeasuring = false;
// ...
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates()
|| needsRemeasureDueToExactSkip
|| mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3(); // mState.mLayoutStep 的值在里面被設置為 State.STEP_START
}
onLayout 中的邏輯并不復雜,邏輯都放在了 dispatchLayout 中,而 dispatchLayout 中又根據各種判斷確保了 dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3 都會執(zhí)行。至于這三個方法在最后一小節(jié)分析。
onDraw
RecyclerView 重寫了 draw 方法,那么就先看一下 draw 方法:
RecyclerView.java
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);
}
// ...
}
draw 方法中獲取了所有的 ItemDecoration 也就是“分割線”,調用了其 onDrawOver 方法。關于 ItemDecoration 將和 LayoutManager 一起在下一篇博客中分析。
draw 的源碼中會繼續(xù)調用 onDraw 方法,繼續(xù)看一下 onDraw 方法:
RecyclerView.java
public void onDraw(Canvas c) {
super.onDraw(c);
// draw 方法中調用了 ItemDecoration 的 onDrawOver
// onDraw 方法又調用了 ItemDecoration 的 onDraw
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
可以看出 draw 和 onDraw 主要對分割線進行了繪制,由于 draw 方法先執(zhí)行,那么也就意味著 ItemDecoration 的 onDrawOver 方法會先繪制,之后再執(zhí)行其 onDraw 方法。
dispatchLayoutStep1、2、3
三大流程的整體代碼流程并不復雜,核心邏輯都在 dispatchLayoutStep1、2、3 這三個方法中,字面意思翻譯過來是“分發(fā)布局步驟1、2、3”,下面挨著來分析下。
dispatchLayoutStep1
概述:處理 Adapter 更新,決定哪個動畫應該被執(zhí)行,保存當前的視圖信息,如果需要的話進行預布局并保存相關信息。
RecyclerView.java
private void dispatchLayoutStep1() {
// 確認布局步驟 在 dispatchLayoutStep3 會設置為 STEP_START
// 并且 onLayout 調用 dispatchLayoutStep1 之前也進行了判斷
mState.assertLayoutStep(State.STEP_START);
// 獲取剩余的滾動距離(橫豎向)
fillRemainingScrollValues(mState);
// onMeasure 中標記為 true 這里再置為 false
mState.mIsMeasuring = false;
startInterceptRequestLayout();
// ViewInfoStore 用于保存動畫相關信息
// 清除保存的信息
mViewInfoStore.clear();
// 標記進入布局或者滾動狀態(tài) 內部是int類型進行++操作
onEnterLayoutOrScroll();
// 適配器更新和動畫預處理 設置 mState.mRunSimpleAnimations 和 mState.mRunPredictiveAnimations 的值
processAdapterUpdatesAndSetAnimationFlags();
// 保存焦點信息
saveFocusInfo();
// 一些信息保存
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
mItemsAddedOrRemoved = mItemsChanged = false;
// 預布局標志 和 mRunPredictiveAnimation 有關 這里先記住 后面會解釋什么是預布局
mState.mInPreLayout = mState.mRunPredictiveAnimations;
mState.mItemCount = mAdapter.getItemCount();
findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
// 下面兩個 if 都是動畫預處理 保存信息等等
// mRunSimpleAnimations 可以理解為 需要執(zhí)行動畫
if (mState.mRunSimpleAnimations) {
// ...
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
// ...
// 保存執(zhí)行動畫所需的信息 (預布局時的信息)
mViewInfoStore.addToPreLayout(holder, animationInfo);
// ...
}
}
// mRunPredictiveAnimations 可以理解為 需要執(zhí)行動畫的情況下需要進行預布局
// 換而言之需要拿到動畫執(zhí)行前后的各種信息(坐標等等)
if (mState.mRunPredictiveAnimations) {
// ...
// 這里如果需要預布局就調用 LayoutManager 的 onLayoutChildren 開始布局
// 注意 mState.mInPreLayout = mRunPredictiveAnimations
// 當 mRunPredictiveAnimations 為 ture 時 mInPreLayout 同樣為 true
mLayout.onLayoutChildren(mRecycler, mState);
// ...
} else {
clearOldPositions();
}
onExitLayoutOrScroll();
// 和 startInterceptRequestLayout 成對使用 貌似是防止多次 requestLayout
stopInterceptRequestLayout(false);
// 標記 STEP_START 完成 可以執(zhí)行 dispatchLayoutStep2
mState.mLayoutStep = State.STEP_LAYOUT;
}
dispatchLayoutStep1 整體上都是預布局處理,對動畫信息的保存等等,ViewInfoStore 是用于存儲 item 動畫相關信息,后面的博客中會分析。注意重點,如果需要執(zhí)行動畫將會執(zhí)行預布局,也就是調用 mLayout.onLayoutChildren 之前 mInPreLayout 為 true。 我并沒有每一行代碼都研究透徹,看源碼也大可不必讀懂每一行代碼,那樣會越陷越深。
dispatchLayoutStep2
概述:預布局狀態(tài)結束,開始真正的布局。
RecyclerView.java
private void dispatchLayoutStep2() {
startInterceptRequestLayout(); // 和 stopInterceptRequestLayout 成對出現
onEnterLayoutOrScroll(); // 上面已經說過了
// 判斷 State ,在 dispatchLayoutStep1 已經標記為 STEP_LAYOUT
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
if (mPendingSavedState != null && mAdapter.canRestoreState()) {
if (mPendingSavedState.mLayoutState != null) {
mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState);
}
mPendingSavedState = null;
}
// 預布局標記為 fasle
mState.mInPreLayout = false;
// 開始布局 這里是真正的測量和布局items
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
// 標記 STEP_ANIMATIONS
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
stopInterceptRequestLayout(false); // 和 startInterceptRequestLayout 成對出現
}
dispatchLayoutStep2 方法代碼不多,注意重點,在調用 mLayout.onLayoutChildren(mRecycler, mState) 之前將 mState.mInPreLayout 預布局標記為 false。
到這里可以看出預布局過程就發(fā)生在 dispatchLayoutStep1、2 之間。
dispatchLayoutStep3
概述:執(zhí)行 item 動畫以及布局完成后的收尾工作。
RecyclerView.java
private void dispatchLayoutStep3() {
// 判斷狀態(tài)是否為 STEP_ANIMATIONS
mState.assertLayoutStep(State.STEP_ANIMATIONS);
// 和 stopInterceptRequestLayout 成對出現
startInterceptRequestLayout();
// 和 onExitLayoutOrScroll 成對出現
onEnterLayoutOrScroll();
// 標記為 STEP_START, 在步驟 1 中會判斷是否為 STEP_START
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
// ...
// 保存動畫信息
// 布局完成后的坐標等等
mViewInfoStore.addToPostLayout(holder, animationInfo);
// ...
}
}
// 執(zhí)行 item 動畫
mViewInfoStore.process(mViewInfoProcessCallback);
}
// 清理 mAttachedScrap 和 mChangedScraop 緩存
mLayout.removeAndRecycleScrapInt(mRecycler);
// item 數量
mState.mPreviousLayoutItemCount = mState.mItemCount;
// 相關標記設為初始值
mDataSetHasChangedAfterLayout = false;
mDispatchItemsChangedEvent = false;
mState.mRunSimpleAnimations = false;
mState.mRunPredictiveAnimations = false;
mLayout.mRequestedSimpleAnimations = false;
// ...
// 布局完成回調
mLayout.onLayoutCompleted(mState);
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
// 清理
mViewInfoStore.clear();
// ...
}
mAttachedScrap 和 mChangedScrap
在本系列博客第一篇分析回收復用源碼時對 mAttachedScrap 和 mChangedScrap 并沒有詳細說明,到這里可以對他們倆分析一下了。
在第一篇的回收復用中,回收部分源碼執(zhí)行時,并沒有用到 mAttachedScrap 和 mChangedScrap,復用時卻優(yōu)先在他們倆容器中尋找緩存?,F在在布局步驟3 dispatchLayoutStep3 中也對其進行了清空,那么說明在 dispatchLayoutStep3 之前對其肯定有過回收的操作。
布局步驟1、2、3中,大部分邏輯都在 mLayout.onLayoutChildren 中,但其是一個空實現,所以,就以其開發(fā)中最常用到的實現類 LinearLayoutManager 源碼來分析看看:
LinearLayoutManager.java
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state){
// ...
//
detachAndScrapAttachedViews(recycler);
// ...
// fill 在第一篇博客中提到了 填充布局 算是回收復用的入口
fill(recycler, mLayoutState, state, false);
}
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
// 從這里可以看出將所有的可見的 item 都回收到了 mAttachedScrap 或者 mChangedScrap 中
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
在 onLayoutChildren 中對可見的 item 都進行了回收操作,并且緊接著執(zhí)行了 fill 進行了填充布局操作。由上述對 dispatchLayout1、2、3 的源碼分析可以得知,dispatchLayout3 對 mAttachedScrap 和 mChangedScrap 進行了清空操作,dispatchLayout1 調用 onLayoutChildren 進行預布局操作,而 dispatchLayout2 調用 onLayoutChildren 進行真正的布局操作。
那么顯而易見,mAttachedScrap 和 mChangedScrap 是對可見 item 的緩存,目的在于預布局、真正的布局階段復用,不用重新綁定數據。
預布局
上述內容中多次提到過預布局,到底什么是預布局?先大概說一下預布局的使用場景,如下圖所示:

假如屏幕中有一個 RecyclerView 且其有三個 item,當刪除 item3 時,item4 會遞補出現在屏幕內。這是開發(fā)中非常常見的情況吧,一般執(zhí)行刪除或者新增操作,我們都會添加動畫讓其顯得不生硬,那么思考下 item4 是什么時候添加到屏幕上的呢?
回到 LinearLayoutManager 的 fill 方法查看源碼:
LinearLayoutManager.java
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
//...
// 可用空間
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
// 當 remainingSpace > 0 會繼續(xù)循環(huán)
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
// ...
// 布局
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// 計算可用空間
// 注意這里的判斷條件
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
remainingSpace -= layoutChunkResult.mConsumed;
}
}
}
在計算可用空間時,有三個判斷條件:
!layoutChunkResult.mIgnoreConsumed
layoutState.mScrapList != null
!state.isPreLayout()
重點看 1 和 3,先說 3 吧,如果是預布局狀態(tài),也就是 dispatchLayoutStep1 調用進來時第三個條件是 false。至于條件 1 還需要看一下 layoutChunk 方法源碼:
LinearLayoutManager.java
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// ...
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
// ...
// 源碼最后部分有這么一處判斷
// 如果 viewholder 被標記為了移除或者改變 mIgnoreConsumed 設為 true
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
看完這段代碼再回到上面圖示中的場景:

當 item3 被刪除時,在預布局階段它所占用的空間會忽略不計,那么 fill 方法中在計算可用空間時就會多走一次 while 循環(huán),從而多添加一個 item。
那么 dispatchStep1 即可稱之為預布局階段,此時將要移除的 item3 以及即將添加到屏幕上的 item4 的預布局階段的位置信息等等保存,在 dispatchStep2 真正布局階段保存完成刪除操作后的位置信息等等,即可在 dispatchStep3 中根據兩個信息之間的差異做出對應的 item 動畫。關于動畫部分后面博客還會分析,由于篇幅原因暫時理解到這里。
最后
本篇博客內容從自定義 View 的三大流程角度開始分析 RecyclerView 相關源碼,接著牽連出分發(fā)布局的三個階段以及對預布局的理解。關于動畫部分沒有多提,后面動畫部分會單獨一篇博客分析。
以上就是RecyclerView 源碼淺析測量 布局 繪制 預布局的詳細內容,更多關于RecyclerView測量 布局 繪制 預布局的資料請關注腳本之家其它相關文章!
相關文章
Flutter StreamBuilder組件實現局部刷新示例講解
日常使用最多的局部刷新為Provider狀態(tài)管理 Selector,今天分享flutter框架自帶的StreamBuilder組件,該組件可做到局部刷新,使用簡單且輕便2022-11-11
Android自定義SwipeRefreshLayout高仿微信朋友圈下拉刷新
這篇文章主要以社交APP的BOSS微信為例,介紹了Android自定義SwipeRefreshLayout高仿微信朋友圈下拉刷新,感興趣的小伙伴們可以參考一下2016-07-07
Android 使用AsyncTask實現多任務多線程斷點續(xù)傳下載
這篇文章主要介紹了Android 使用AsyncTask實現多任務多線程斷點續(xù)傳下載的相關資料,需要的朋友可以參考下2018-05-05

