第四章-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);
    }

在这里插入图片描述

发布了126 篇原创文章 · 获赞 42 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/gaopinqiang/article/details/105169341