RecyclerView繪製流程

本次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內容

RecyclerView設置最大高度、寬度

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()部分

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章