自定義控制元件(四) 原始碼分析 layout 和 draw 流程

飯小龍發表於2017-12-22

系列文章傳送門 (持續更新中..) :

自定義控制元件(一) Activity的構成(PhoneWindow、DecorView)

自定義控制元件(二) 從原始碼分析事件分發機制

自定義控制元件(三) 原始碼分析measure流程


在上一篇中我們詳細分析了 View 工作三大流程中最複雜的 measure 流程, 掌握了 measure 流程後, layout 和 draw 流程就相對比較簡單些了。

  • 在佈局流程中, 當 ViewGroup 的 layout 方法被父容器呼叫後它的位置將被確定下來, 然後它在 onLayout 中遍歷所有的子元素並呼叫它的 layout 方法對子元素進行擺放, 而在 layout 中 onLayout 方法又被呼叫, 如此反覆直到佈局完成。

簡單講:layout 方法確定 View 本身的位置, onLayout 確定所有子元素的位置

layout 過程 :

在看原始碼之前,先提出一個問題, View 的 getWidth()getMeasuredWidth() 有什麼區別?

public void layout(int l, int t, int r, int b) {
	...
	boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame (l, t, r, b) : setFrame(l, t, r, b);
	if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
	        onLayout(changed, l, t, r, b);
	...
}
複製程式碼

layout 方法中, 先呼叫 setFrame() 給自己的四個頂點賦值, 就確定了自己的位置。然後呼叫 onLayout 方法對子元素進行擺放

protected boolean setFrame(int left, int top, int right, int bottom) {
	...
	mLeft = left;
	mTop = top;
	mRight = right;
	mBottom = bottom;
	...
}
複製程式碼

看到沒,mLeft、mTop 、mRight 、mBottom 就是這個 View 的四個頂點,當四個頂點的值唄確定,View 的位置就擺放完了。 由於在 View 中 onLayout() 方法是空實現, ViewGroup 的 onLayout() 是抽象方法, 所以就挑一個 ViewGroup 常用的子類 FrameLayout 看一下 (其它都類似, 自己可以去看下):

#FrameLayout - onLayout

protected void onLayout(boolean changed, int left, int top, int right, int bottom){
    layoutChildren(left, top, right, bottom, false);
}
複製程式碼

onLayout 的引數直接傳給 layoutChildren,繼續走:

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    ...
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();
            ...
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
            ...
複製程式碼

很明顯, 在 layoutChildren 遍歷所有的子元素, 並呼叫其 layout 方法來擺放子元素,這樣父容器在 layout 方法中完成自己的擺放後,通過 onLayout 方法去遍歷呼叫子元素的 layout 方法,子元素又會通過 layout 方法確定自己的位置,這樣一層一層傳遞下去從而完成整個 View 樹的 layout 過程。

  • 注意看呼叫 layout 傳的引數,這裡傳入的 widthheight, 其實就是這個 view 的測量寬/高。前面分析了 layout 中傳入的引數會對 View 的四個頂點(mLeft、mTop 、mRight 、mBottom)賦值來確定位置,這裡我們來看一下 getWidth() 的返回值:
public final int getWidth() {
    return mRight - mLeft;
}

public final int getHeight() {
    return mBottom - mTop;
}
複製程式碼
  • 結合我們剛剛的原始碼分析不難看出來,mRight - mLeft 和 mBottom - mTop 的返回值不就分別是 view 的測量寬高麼, 所以在系統 View 的預設實現中,以及開發中我們可以直接認為 getWidth() = getMeasuredWidth(),只是賦值時間不同, getHeight 和 getMeasuredHeight 同理getMeasuredWidth() 是在 onMeasure() 方法中執行完成測量流程後並儲存尺寸的時候被賦值,getWidth() 是在 layout 方法中確定自己位置的時候被賦值。

  • 當然也存在兩種情況會出現不相等:一種是某些極端情況系統需要多次執行measure流程,這時則除了最後一次measure,前幾次的measure結果就可能存在不相等。另一種則是在 onLayout() 中呼叫 layout 時, 對傳入的四個頂點值做了一些運算處理, 則這兩個值也是不相等的,如下

protected void onLayout(boolean changed, int left, int top, int right, int bottom){	
	...
    child.layout(childLeft, childTop, childLeft + width + 100, childTop + height + 100);
    ...
}
// 或重寫 layout 方法
public void layout(int l, int t, int r, int b) {
    super.layout(l, t, r + 100, b + 100);
}
複製程式碼

draw 過程 :

繪製過程就是將 View 繪製到螢幕上, 它分為4步:

  • (1) 繪製背景(私有方法不能重寫)
  • (2) 主體繪製(一般重寫此方法)
  • (3) 繪製子元素
  • (4) 繪製前景和滑動相關(繪製前景的支援是在 Android 6.0 之後)

具體從 draw 方法中可以明瞭的看出來:

public void draw(Canvas canvas) {
	...
	drawBackground(canvas);
	...
	onDraw(canvas);
	...
	dispatchDraw(canvas);
	...
	onDrawForeground(canvas);
	...
}
複製程式碼

繪製過程的傳遞是通過 dispatchDraw 來實現,dispatchDraw 中會遍歷所有子元素的 draw 方法,如此反覆下去直到繪製完成。

setWillNotDraw :

這是 View 的一個特殊方法,具體看原始碼:

/**
 * If this view doesn't do any drawing on its own, set this flag to
 * allow further optimizations. By default, this flag is not set on
 * View, but could be set on some View subclasses such as ViewGroup.
 *
 * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
 * you should clear this flag.
 *
 * @param willNotDraw whether or not this View draw on its own
 */
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
複製程式碼

從註釋大致能看出來,如果一個 View 不需要對自己進行任何繪製,設定這個標誌位為 true 後,系統會進行相應的優化,即繞過 draw() 方法,換而直接執行 dispatchDraw(),以此來簡化繪製流程。預設情況下 View 沒有設定這個標誌位, 而 ViewGroup 預設會啟動這個標誌位。

  • 在實際開發中, 如果自定義控制元件繼承於 ViewGroup 並且本身不具備繪製功能時,就可以開啟這個標誌讓系統進行繪製優化。 但是當明確知道一個 ViewGroup 需要在它的除 dispatchDraw() 以外的任何一個繪製方法內繪製內容,你顯示的關閉這個 WILL_NOT_DRAW 這個標誌位:View.setWillNotDraw(false)

覺得有用的話,點個贊再走唄~

相關文章