第四章-View的工作原理(measure、layout、draw)

View的工作流程主要是指measure、layout、draw這三大流程,即測量、佈局和繪製。其中measure確定View的測量寬/高,layout確定View的最終寬/高和四個頂點的位置,而draw則將View繪製到屏幕上。

1、measure過程
measure過程要分情況來看,如果只是一個原始的View,那麼通過measure方法就可以完成了其測量過程,如果是一個ViewGroup,除了完成自己的測量過程外,還會遍歷去調用所有子元素的measure方法,各個子元素再遞歸去執行這個流程,下面針對這兩種情況分別討論。

a.View的measure過程

View 的measure過程由其measure方法來完成,measure方法是一個final類型的方法,這就意味着子類不能重寫此方法,在View的measure方法中會去調用View的onMeasure方法,因此只需要看onMeasure的實現即可,View的onMeasure方法如下所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	setMeasuredDimension(
			getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
			getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
			//從下面的分析看,只有當前測量模式是系統內部的UNSPECTIFIED時,纔會調用getSuggestMinimumWidth方法。否則就是使用onMeasure形參裏面的Spec設置大小。
}

上面的代碼很簡介,但是簡潔不代表簡單,setMeasuredDimension會設置View寬/高的測量值,因此我們只需要getDefaultSize方法即可。

public static int getDefaultSize(int size, int measureSpec) {
	int result = size;
	int specMode = MeasureSpec.getMode(measureSpec);
	int specSize = MeasureSpec.getSize(measureSpec);

	switch (specMode) {
	case MeasureSpec.UNSPECIFIED:
		result = size;
		break;
	case MeasureSpec.AT_MOST:
	case MeasureSpec.EXACTLY://固定大小和match_parent就是這種模式
		result = specSize;
		break;
	}
	return result;
}

在這裏插入圖片描述

protected int getSuggestedMinimumWidth() {
	return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
	return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

}

在這裏插入圖片描述
總結下:

從getDefaulSize方法的實現來看,View的寬/高由specSize決定(AT_MOST和EXACTLY模式下),所以我們可以得出如下結論:直接繼承View的自定義控件需要重寫onMeasure方法並設置wrap_content時的自身大小,否則在佈局中使用wrap_content就相當於使用match_parent。
爲什麼呢?這個原因需要結合上述代碼和之前的表才能更好地理解。從上述代碼中我們知道,如果View在佈局中使用wrap_content,那麼它的specMode是AT_MOST模式,在這種模式下,它的寬/高等於specSize;查表4-1可知,這種情況下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器當前剩餘的空間大小。很顯然,View的寬/高就等於父容器當前剩餘的空間大小,這種效果和在佈局中使用match_parent完全一致。如何解決這個問題呢?也很簡單,代碼如下所示。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
	int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
	int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
	int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
	if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
		setMeasuredDimension(mWidth, mHeight);//寬高都設置成wrap_content,就重設下,避免使用父容器的寬高,導致和match_parent效果一樣。
	} else if (widthSpecMode == MeasureSpec.AT_MOST) {
		setMeasuredDimension(mWidth, heightSpecSize);//寬設置成wrap_content
	} else if (eightSpecMode == MeasureSpec.AT_MOST) {
		setMeasuredDimension(widthSpecSize, mHeight);//高設置成wrap_content,
	}
}

在這裏插入圖片描述
我們翻下TextView的onMeasure實現看到是對AT_MOST模式做了處理的。
在這裏插入圖片描述

b.ViewGroup的measure過程

對於ViewGroup來說,除了完成自己的measure過程以外,還會遍歷去調用所有子元素的measure方法,各個子元素再通歸去執行這個過程。和View不同的是,ViewGroup是一個抽象類,因此它沒有重寫View的onMeasure方法,但是它提供了一個叫measureChildren。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
	final int size = mChildrenCount;
	final View[] children = mChildren;
	for (int i = 0; i < size; ++i) {
		final View child = children[i];
		if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
			measureChild(child, widthMeasureSpec, heightMeasureSpec);
		}
	}
}

從上述代碼中看到,在ViewGroup的measure時,會對每一個子元素進行測量,那麼這個方法就很好理解了

protected void measureChild(View child, int parentWidthMeasureSpec,
	int parentHeightMeasureSpec) {
	final LayoutParams lp = child.getLayoutParams();

	final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
			mPaddingLeft + mPaddingRight, lp.width);
	final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
			mPaddingTop + mPaddingBottom, lp.height);

	child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

在這裏插入圖片描述

首先,我們來看一下LinearLayout的onMeasure方法(這個是重寫了View的onMeasure方法,我們每次測量子view的時候都會調到這裏,child.measure–>onMeasure)。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	if (mOrientation == VERTICAL) {
		measureVertical(widthMeasureSpec, heightMeasureSpec);
	} else {
		measureHorizontal(widthMeasureSpec, heightMeasureSpec);
	}
}

上述的代碼很簡單我們選擇一個來看下,比如選中豎直方向的LinearLayout測量過程,即measureVertical,他的源碼還比較長,我們看:

// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
	final View child = getVirtualChildAt(i);
	if (child == null) {
		mTotalLength += measureNullChild(i);
		continue;
	}
	...
	final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
	measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
			heightMeasureSpec, usedHeight);

	final int childHeight = child.getMeasuredHeight();
	if (useExcessSpace) {
		// Restore the original height and record how much space
		// we've allocated to excess-only children so that we can
		// match the behavior of EXACTLY measurement.
		lp.height = 0;
		consumedExcessSpace += childHeight;
	}

	final int totalLength = mTotalLength;
	mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
		   lp.bottomMargin + getNextLocationOffset(child));

從上面的代碼可以看出,系統會遍歷子元素並對每一個子元素執行measureChildBeforeLayout方法,這個方法內部會調用子元素的measure方法,這樣各個子元素就開始依次進入measure過程,並且系統通過mTotalLength這個變量來存儲LinearLayout在豎直方向上的初步高度,每測量一個子元素,mTotalLength就會增加,增加的部分主要包括子元素的高度以及豎直方向上的margin等,當子元素測量完畢之後,LinearLayout會測量自己的大小,看源碼:

在這裏插入圖片描述
在這裏插入圖片描述

總結下:
在這裏插入圖片描述

  • (1)Activity/View#onWindowFocusChanged。(會多次調用)

onWindowFocusChanged這個方法的含義是:View已經初始化完畢了,寬/高已經準備好了,這個時候去獲取寬/高是沒問題的。需要注意的是,onWindowFocusChanged會被調用多次,當Activity的窗口得到焦點和失去焦點時均會被調用一次。具體來說,當Activity繼續執行和暫停執行時,onWindowFocusChanged均會被調用,如果頻繁地進行onResume和onPause,那麼onWindowFocusChanged也會被頻繁地調用。代碼就是重寫這個方法,可自己寫下。

  • (2)view.post(runnable)(推薦使用)

通過post可以將一個runnable投遞到消息隊列,然後等到Lopper調用runnable的時候,View也就初始化好了,典型代碼如下:

@Override
protected void onStart() {
	super.onStart();

	mTextView.post(new Runnable() {
		@Override
		public void run() {
			int width = mTextView.getMeasuredWidth();
			int height = mTextView.getMeasuredHeight();
		}
	});
}
  • (3)ViewTreeObserver(回調方法可能多次調用)

使用ViewTreeObserver的衆多回調可以完成這個功能,比如使用OnGlobalLayoutListener這個接口,當View樹的狀態發生改變或者View樹內部的View的可見性發生改變,onGlobalLayout方法就會回調,因此這是獲取View的寬高一個很好的例子,需要注意的是,伴隨着View樹狀態的改變,這個方法也會被調用多次

  • (4)view.measure(int widthMeasureSpec , int heightMeasureSpec)(手動測量,個人覺得沒必要)

通過手動測量View的寬高,這種方法比較複雜,這裏要分情況來處理,根據View的LayoutParams來處理

match_parent:

直接放棄,無法測量出具體的寬高,根據View的測量過程,構造這種measureSpec需要知道parentSize,即父容器的剩下空間,而這個時候我們無法知道parentSize的大小,所以理論上我們不可能測量出View的大小

具體的數值,比如寬高都是100dp,那我們可以這樣:

int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);//確定模式
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
mTextView.measure(widthMeasureSpec,heightMeasureSpec);

warap_content

int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
mTextView.measure(widthMeasureSpec,heightMeasureSpec);

在這裏插入圖片描述

2、layout過程
在這裏插入圖片描述

public void layout(int l, int t, int r, int b) {
	if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
		onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
		mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
	}

	int oldL = mLeft;
	int oldT = mTop;
	int oldB = mBottom;
	int oldR = mRight;

	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);
		mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

		ListenerInfo li = mListenerInfo;
		if (li != null && li.mOnLayoutChangeListeners != null) {
			ArrayList<OnLayoutChangeListener> listenersCopy =
					(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
			int numListeners = listenersCopy.size();
			for (int i = 0; i < numListeners; ++i) {
				listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
			}
		}
	}

	mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
	mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

在這裏插入圖片描述

protected void onLayout(boolean changed, int l, int t, int r, int b) {
	if (mOrientation == VERTICAL) {
		layoutVertical(l, t, r, b);
	} else {
		layoutHorizontal(l, t, r, b);
	}
}

LinearLayout中onLayout的實現邏輯和onMeasure的實現邏輯類似,這裏選擇layoutVertical繼續講解,爲了更好地理解其邏輯,這裏給出了主要邏輯代碼:

void layoutVertical(int left, int top, int right, int bottom) {
	...

	final int count = getVirtualChildCount();

	final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
	final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

	switch (majorGravity) {
	   case Gravity.BOTTOM:
		   // mTotalLength contains the padding already
		   childTop = mPaddingTop + bottom - top - mTotalLength;
		   break;

		   // mTotalLength contains the padding already
	   case Gravity.CENTER_VERTICAL:
		   childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
		   break;

	   case Gravity.TOP:
	   default:
		   childTop = mPaddingTop;
		   break;
	}

	for (int i = 0; i < count; i++) {
		final View child = getVirtualChildAt(i);
		if (child == null) {
			childTop += measureNullChild(i);
		} else if (child.getVisibility() != GONE) {
			final int childWidth = child.getMeasuredWidth();
			final int childHeight = child.getMeasuredHeight();

			final LinearLayout.LayoutParams lp =
					(LinearLayout.LayoutParams) child.getLayoutParams();

			...

			if (hasDividerBeforeChildAt(i)) {
				childTop += mDividerHeight;
			}

			childTop += lp.topMargin;
			setChildFrame(child, childLeft, childTop + getLocationOffset(child),
					childWidth, childHeight);
			childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

			i += getChildrenSkipCount(child, i);
		}
	}
}

在這裏插入圖片描述

private void setChildFrame(View child, int left, int top, int width, int height) {        
	child.layout(left, top, left + width, top + height);
}

我們注意到setChildFrame中的width和height實際上就是子元素測量寬高,從下面的代碼可以看出

final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(0, childHeight), MeasureSpec.EXACTLY);
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,lp.width);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

而在Layout方法中通過setFrame去設置子元素的四個頂點位置,方法中有這麼幾句:

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;

從getWidth和getHeight的源碼再結合mLeft、mTop、mRight、mBottom這四個變量的賦值過程來看,getWidth的返回值剛好就是View測量的寬度,而getHeight方法的返回值也剛好就是View的測量高度。
所以我們可以回答這個問題了:在View的默認實現中,View的測量寬高和最終寬高是一樣的,只不過一個是measure過程,一個是layout過程,而最終形成的是layout過程,即兩者的賦值時機不同,測量寬高的賦值時機,稍微早一些。因此,在日常開發中,我們可用認爲他們是相等的,但是還是有些不相同的。

舉例說明: 如果重寫View的layout方法,代碼如下:

public void layout(int l,int t,int r, int b){
	super.layout(l,t,t+100,b+100);//人爲強制加了100px,導致4個座標和測量的不一樣。
}

在這裏插入圖片描述

3、draw過程
在這裏插入圖片描述

public void draw(Canvas canvas) {
	final int privateFlags = mPrivateFlags;
	mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

	/*
	 * Draw traversal performs several drawing steps which must be executed
	 * in the appropriate order:
	 *
	 *      1. Draw the background
	 *      2. If necessary, save the canvas' layers to prepare for fading
	 *      3. Draw view's content
	 *      4. Draw children
	 *      5. If necessary, draw the fading edges and restore layers
	 *      6. Draw decorations (scrollbars for instance)
	 */

	// Step 1, draw the background, if needed
	int saveCount;

	drawBackground(canvas);

	// skip step 2 & 5 if possible (common case)
	final int viewFlags = mViewFlags;
	boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
	boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
	if (!verticalEdges && !horizontalEdges) {
		// Step 3, draw the content
		onDraw(canvas);

		// Step 4, draw the children
		dispatchDraw(canvas);

		drawAutofilledHighlight(canvas);

		// Overlay is part of the content and draws beneath Foreground
		if (mOverlay != null && !mOverlay.isEmpty()) {
			mOverlay.getOverlayView().dispatchDraw(canvas);
		}

		// Step 6, draw decorations (foreground, scrollbars)
		onDrawForeground(canvas);

		// Step 7, draw the default focus highlight
		drawDefaultFocusHighlight(canvas);

		if (debugDraw()) {
			debugDrawFocus(canvas);
		}

		// we're done...
		return;
	}
	...
}

View的繪製過程的傳遞是通過dispatchDraw來實現的,dispatchDraw會遍歷調用所有子元素的draw方法,如此draw事件就一層層地傳遞了下去。
View有個特殊的方法setWillNotDraw,先看下它的源碼,如下所示。

/**
     * 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);
    }

在這裏插入圖片描述

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