本次RecyclerView繪製流程分析是基於源碼28版本的,RecyclerView繼承ViewGroup,作爲自定義View無疑會經過onMeasure()、onLayout()、onDraw()流程,所以可以從這裏進行入手。這裏我們假想RecyclerView.LayoutManager 爲LinearLayoutManager,具體實現可到該manager進行查看
onMeasure()
protected void onMeasure(int widthSpec, int heightSpec) {
// 1. 判斷是否設置layoutManager
if (this.mLayout == null) {
// 如果不設置LayuytManager那麼Rv寬高就只爲padding,顯示爲空白
this.defaultOnMeasure(widthSpec, heightSpec);
} else {
// 2. 判斷layoutManager是否是自動測量
if (!this.mLayout.isAutoMeasureEnabled()) {
// .... 暫不分析
} else {
// layoutManager中isAutoMeasureEnabled()爲true則進行該分支
// 如linearLayout中isAutoMeasureEnabled()確實爲true
// 獲取寬高測量模式
int widthMode = MeasureSpec.getMode(widthSpec);
int heightMode = MeasureSpec.getMode(heightSpec);
// ** 這裏首先會完成一次默認的測量,最終會走到chooseSize()
// 按照默認規則設置一次尺寸
this.mLayout.onMeasure(this.mRecycler, this.mState, widthSpec, heightSpec);
// MeasureSpec.EXACTLY = 1073741824
boolean measureSpecModeIsExactly = widthMode == 1073741824 && heightMode == 1073741824;
// 3. 如果寬高都是精確模式,直接返回,執行佈局
// 第一次默認測量的寬高就是最終Rv的寬高
if (measureSpecModeIsExactly || this.mAdapter == null) {
return;
}
// 4. 初始化默認值都是1
if (this.mState.mLayoutStep == 1) {
// 作用:
//1、對adapter進行更新對mState進行一些初始化
//2、初始化ItemAnimator並決定使用哪種動畫
//3、設置mLayoutStep值爲2
this.dispatchLayoutStep1();
}
// 在layoutManager中記錄下寬高
this.mLayout.setMeasureSpecs(widthSpec, heightSpec);
// 標誌正在測量
this.mState.mIsMeasuring = true;
// 5. 真正執行LayoutManager繪製的地方
this.dispatchLayoutStep2();
// 6. 經過測量子控件再對自身進行測量
this.mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// 寬高都不確定的時候,會繪製兩次
if (this.mLayout.shouldMeasureTwice()) {
this.mLayout.setMeasureSpecs(MeasureSpec.makeMeasureSpec(this.getMeasuredWidth(), 1073741824), MeasureSpec.makeMeasureSpec(this.getMeasuredHeight(), 1073741824));
this.mState.mIsMeasuring = true;
this.dispatchLayoutStep2();
this.mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
}
}
}
LinearLayoutManager中isAutoMeasureEnabled實現
public class LinearLayoutManager extends LayoutManager{
//...
public boolean isAutoMeasureEnabled() {
return true;
}
}
onMeasure()做了如下工作:
首先會判斷是否綁定LayoutManager,如果manager爲null,會依據測量模式僅對自身進行測量,如果不爲null 會接着判斷layoutManager是否允許自動測量,如LinearLayoutManager 設定的值爲true,進入該分支,然後校驗測量模式,對自身做一個默認的測量,寬高的測量模式都爲Exactly精確模式,onMeasure()直接返回,寬高固定不需要根據子view的寬高來確定自身的寬高;否則會先後執行 dispatchLayoutStep1()和dispatchLayoutStep2(),dispatchLayoutStep1()會對adapter進行更新,對state進行一些初始化和itemAnimator的選定,然後將step切換爲2,再然後會調用dispatchLayoutStep2(),執行對子view的測量以及擺放
// RecyclerView.java
private void dispatchLayoutStep2() {
//將adapter中重寫的 getItemCount()返回給count
this.mState.mItemCount = this.mAdapter.getItemCount();
// recyclerView將佈局的繪製交給了Laymanager
this.mLayout.onLayoutChildren(this.mRecycler, this.mState);
// ....
}
dispatchLayoutStep2
dispatchLayoutStep2()我們需要注意這裏將我們具體實現的getItemCount()返回給了state中的ItemCount變量,然後將測量繪製任務交給了layoutManager。接下來我們去看看LinearLayoutManager中的 onLayoutChildren究竟做了哪些工作
// LinearLayoutManager
// 這個方法好難懂啊,查閱了下
// 先尋找頁面當前的錨點
// 以這個錨點未基準,向上和向下分別填充
// 填充完後,如果還有剩餘的可填充大小,再填充一次
public void onLayoutChildren(Recycler recycler, State state) {
//
this.mLayoutState.mInfinite = this.resolveIsInfinite();
// ...
this.fill(recycler, this.mLayoutState, state, false);
}
會依據正逆序尋找錨點,然後決定從上向下還是從下往上進行填充item。
int fill(Recycler recycler, LinearLayoutManager.LayoutState layoutState, State state, boolean stopOnFocusable) {
// 循環執行layoutChunk()
while((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
this.layoutChunk(recycler, state, layoutState, layoutChunkResult);
}
}
void layoutChunk(Recycler recycler, State state, LinearLayoutManager.LayoutState layoutState, LinearLayoutManager.LayoutChunkResult result) {
// 創建或者 ,從緩存中取View
// 該方法最終會執行...createViewHolder
View view = layoutState.next(recycler);
LayoutParams params = (LayoutParams)view.getLayoutParams();
if (layoutState.mScrapList == null) {
// 判斷是正序還是逆序
if (this.mShouldReverseLayout == (layoutState.mLayoutDirection == -1)) {
this.addView(view);
} else {
this.addView(view, 0);
}
} else if (this.mShouldReverseLayout == (layoutState.mLayoutDirection == -1)) {
this.addDisappearingView(view);
} else {
this.addDisappearingView(view, 0);
}
this.measureChildWithMargins(view, 0, 0);
// ...一些計算
this.layoutDecoratedWithMargins(view, left, top, right, bottom);
}
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)child.getLayoutParams();
//這裏insets就是ItemDecoration中getItemoffsets設置的空間
Rect insets = this.mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
int widthSpec = getChildMeasureSpec(this.getWidth(), this.getWidthMode(), this.getPaddingLeft() + this.getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width, this.canScrollHorizontally());
int heightSpec = getChildMeasureSpec(this.getHeight(), this.getHeightMode(), this.getPaddingTop() + this.getPaddingBottom() + lp.topMargin + lp.bottomMargin + heightUsed, lp.height, this.canScrollVertically());
if (this.shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
Rect getItemDecorInsetsForChild(View child) {
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)child.getLayoutParams();
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
} else if (this.mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
return lp.mDecorInsets;
} else {
Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
int decorCount = this.mItemDecorations.size();
for(int i = 0; i < decorCount; ++i) {
this.mTempRect.set(0, 0, 0, 0);
((RecyclerView.ItemDecoration)this.mItemDecorations.get(i)).getItemOffsets(this.mTempRect, child, this, this.mState);
//獲得空隙
insets.left += this.mTempRect.left;
insets.top += this.mTempRect.top;
insets.right += this.mTempRect.right;
insets.bottom += this.mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
}
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right, int bottom) {
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)child.getLayoutParams();
Rect insets = lp.mDecorInsets;
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin, right - insets.right - lp.rightMargin, bottom - insets.bottom - lp.bottomMargin);
}
void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
int count = this.getChildCount();
if (count == 0) {
this.mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
} else {
int minX = 2147483647;
int minY = 2147483647;
int maxX = -2147483648;
int maxY = -2147483648;
for(int i = 0; i < count; ++i) {
View child = this.getChildAt(i);
Rect bounds = this.mRecyclerView.mTempRect;
this.getDecoratedBoundsWithMargins(child, bounds);
if (bounds.left < minX) {
minX = bounds.left;
}
if (bounds.right > maxX) {
maxX = bounds.right;
}
if (bounds.top < minY) {
minY = bounds.top;
}
if (bounds.bottom > maxY) {
maxY = bounds.bottom;
}
}
this.mRecyclerView.mTempRect.set(minX, minY, maxX, maxY);
this.setMeasuredDimension(this.mRecyclerView.mTempRect, widthSpec, heightSpec);
}
}
dispatchLayoutStep2作用
1、依據layoutManager排列方向計算錨點,以錨點開始填充RecyclerView
2、執行fill方法,判斷RecyclerView是否還有空間,如果有,執行layoutChunk方法,直至填充滿。
3、layoutChunk方法中,尋找到當前要添加的子view,add到RecyclerView中。
4、對子view進行measure和layout
OnLayout
//RecyclerView.java
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection("RV OnLayout");
this.dispatchLayout();
TraceCompat.endSection();
this.mFirstLayoutComplete = true;
}
// RecyclerView.java
void dispatchLayout() {
if (this.mAdapter == null) {
Log.e("RecyclerView", "No adapter attached; skipping layout");
} else if (this.mLayout == null) {
// 如果不存在layout直接返回
Log.e("RecyclerView", "No layout manager attached; skipping layout");
} else {
this.mState.mIsMeasuring = false;
if (this.mState.mLayoutStep == 1) {
// layoutStep標誌默認爲1,當寬高都爲精確模式將走該分支
// 解釋了即使寬高確定在onMeasure()中返回依舊可以擺放子view
this.dispatchLayoutStep1();
this.mLayout.setExactMeasureSpecsFrom(this);
this.dispatchLayoutStep2();
} else if (!this.mAdapterHelper.hasUpdates() && this.mLayout.getWidth() == this.getWidth() && this.mLayout.getHeight() == this.getHeight()) {
this.mLayout.setExactMeasureSpecsFrom(this);
} else {
// 如果自身測量的寬高與通過子view測量的寬高來確定自身的寬高不一致重新執行dispatchLayoutStep2()進行擺放
this.mLayout.setExactMeasureSpecsFrom(this);
this.dispatchLayoutStep2();
}
// 爲動畫保存View的相關信息; 觸發動畫; 相應的清理工作
this.dispatchLayoutStep3();
}
}
Q1 不設置LayoutManager爲何爲空白?
mLayout == null => defaultOnMeasure => chooseSize
void defaultOnMeasure(int widthSpec, int heightSpec) {
int width = RecyclerView.LayoutManager.chooseSize(widthSpec, this.getPaddingLeft() + this.getPaddingRight(), ViewCompat.getMinimumWidth(this));
int height = RecyclerView.LayoutManager.chooseSize(heightSpec, this.getPaddingTop() + this.getPaddingBottom(), ViewCompat.getMinimumHeight(this));
this.setMeasuredDimension(width, height);
}
getMinimumWidth() 就是我們在xml中設置的minWidth屬性,加載佈局時會讀取屬性,默認最小值爲0。
public static int chooseSize(int spec, int desired, int min) {
int mode = MeasureSpec.getMode(spec);
int size = MeasureSpec.getSize(spec);
switch(mode) {
case View.MeasureSpec.AT_MOST:
return Math.min(size, Math.max(desired, min));
case View.MeasureSpec.UNSPECIFIED:
default:
return Math.max(desired, min);
case View.MeasureSpec.EXACTLY:
return size;
}
}
chooseSize()該方法還是很重要的,
測量模式爲
EXACTLY時,RecyclerView的大小爲指定的大小,
AT_MOST時,取RecyclerView測量值和max(desired,min)(padding值和RecyclerView最小寬高教大值)的最小值,
UNSPECIFIED,取paddding值和RecyclerView寬高最小值的較大值。
void dispatchLayout() {
if (this.mAdapter == null) {
Log.e("RecyclerView", "No adapter attached; skipping layout");
} else if (this.mLayout == null) {
Log.e("RecyclerView", "No layout manager attached; skipping layout");
}....
}
佈局時當layoutManager不存在也就會結束layout,不會將item測量擺放繪製到RecyclerView上,即便是Rv存在一定的寬高
小結
當RecyclerView不設置manager其雖然存在寬高但是無子view內容
Q2:RecyclerView高度也受子view的佈局高度的影響,並非取決於RecyclerView自身的測量高度?
protected void onMeasure(int widthSpec, int heightSpec) {
//....
this.mLayout.setMeasureSpecs(widthSpec, heightSpec);
this.mState.mIsMeasuring = true;
this.dispatchLayoutStep2();
this.mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
//....
}
void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
int count = this.getChildCount();
if (count == 0) {
this.mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
} else {
//... for循環獲取view排列後的總寬高
this.mRecyclerView.mTempRect.set(minX, minY, maxX, maxY);
this.setMeasuredDimension(this.mRecyclerView.mTempRect, widthSpec, heightSpec);
}
}
如上2塊代碼所示,在置頂完dispatchLayoutStep2()即執行完測量擺放子view後會依次測量的子view數據重新設置Rv的寬高。而正如Q1中的chooseSize代碼所示,當測量模式爲AT_MOST時,對應RecyclerView寬/高爲WRAP_CONTENT時,Rv初次測量自身的高度爲Default_size = 1000,而子view由於條目較少高度爲500,那麼最終綜合設置的Rv高度爲500。總之測量模式爲AT_MOST取最小值。
RecyclerView機制解析: Measure
AndroidX RecyclerView總結-測量佈局
未完待續,後續得增加onDraw()部分