4.3 View的工作流程
View
的工作流程主要是指measure
,layout
,draw
这三大流程,即测量,布局和绘制,其中measure
确定View
的测量宽/高,layout
确定View
的最终宽/高和四个顶点的位置,而draw
则将View
绘制到屏幕上。
4.3.1 measure过程
measure
过程要分情况来看,如果只是一个原始的View
,那么通过measure
方法就完成了其测量过程,如果是一个ViewGroup
,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure
方法,各个子元素再递归去执行这个流程。
1.View的measure过程
View
的measure
过程由其measure
方法来完成,measure
方法是一个final
类型的方法,这意味着子类不能重写此方法,在View
的measure
方法中会去调用View
的onMeasure
方法,因此只需要看onMeasure
的实现即可。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
上述代码很简洁,但是简洁并不代表简单,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:
result = specSize;
break;
}
return result;
}
可以看出,getDefaultSize
这个方法的逻辑很简单,对于我们来说,我们只需要看AT_MOST
和EXACTLY
这两种情况,其实getDefaultSize
返回的大小就是measureSpec
中的specSize
,而这个specSize
就是View
测量后的大小,这里多次提到测量后的大小,是因为View
最终的大小是在layout
阶段确定的,所以这里必须要加以区分,但是几乎所有情况下View
的测量大小和最终大小是相等的。
至于UNSPECIFIED
这种情况,一般用于系统内部的测量过程,在这种情况下,View
的大小为getDefaultSize
的第一个参数size
,即宽/高分别为getSuggestedMinimumWidth()
和getSuggestedMinimumHeight()
这两个方法的返回值,看一下他们的源码:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
这里只分析getSuggestedMinimumWidth()
方法的实现,从getSuggestedMinimumWidth()
的代码可以看出,如果View
没有设置背景,那么View
的宽度为mMinWidth
,而mMinWidth
对应于android:minWidth
这个属性所指定的值,因此View
的宽度即为android:minWidth
属性所指定的值。这个属性如果不指定,那么mMinWidth
则默认为0
,如果View
指定了背景,则View
的宽度为max(mMinHeight, mBackground.getMinimumHeight())
。mMinWidth
的含义我们已经清楚了,那么mBackground.getMinimumHeight()
是什么呢?
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
getMinimumWidth()
返回的就是Drawable
的原始宽度,前提是这个Drawable
有原始宽度,否则就返回0
。那么Drawable
在什么情况下有原始宽度呢?这里先举个例子说明一下,ShapeDrawable
无原始宽/高,而BitmapDrawable
有原始宽/高(图片的尺寸)。
从getDefaultSize
方法的实现来看,View
的宽/高由SpecSize
决定,所以我们可以得出如下结论:直接继承View
的自定义控件需要重写onMeasure()
方法并设置wrap_content
时的自身大小,否则在布局中使用wrap_content
就相当于使用match_parent
。为什么呢?这个原因需要结合上述代码和上一节的表才能更好地理解。从上述代码中我们知道,如果View
在布局中使用wrap_content
,那么它的SpecMode
是AT_MOST
模式,在这种模式下,它的宽/高等于SpecSize
,由表可知,这种情况下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.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (eightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
在上面的代码中,我们只需要给View
指定一个默认的内部宽/高(mWidth和mHeight)
,并在wrap_content
时设置此宽/高即可。对于非wrap_content
情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可。如果查看TextView
,ImageView
等的源码就可以知道,针对wrap_content
情形,它们的onMeasure
方法均做了特殊处理。
2. 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
时,会对每一个子元素进行measure
,measureChild
这个方法的实现也很好理解。
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);
}
很显然,measureChild
的思想就是取子元素的LayoutParams
,然后再通过getChildMeasureSpec
来创建子元素的MeasureSpec
,接着将MeasureSpec
直接传递给View
的measure
方法来进行测量。
我们知道,ViewGroup
并没有定义其测量的具体过程,这是因为ViewGroup
是一个抽象类,其测量过程的onMesure
方法需要各个子类去具体实现,比如LinearLayout
,RelativeLayout
等,为什么ViewGroup
不像View
一样对其onMeasure
方法做统一的实现呢?那是因为不同的ViewGroup
子类有不同的布局特性,这导致它们的测量细节各不相同,比如LinearLayout
和RelativeLayout
这两者的布局特性显然不同,因此ViewGroup
无法做统一实现。
下面就通过LinearLayout
的onMeasure
方法来分析ViewGroup
的measure
过程。
首先来看LinearLayout
的onMeasure()
方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
上述代码很简单,我们选择一个来看一下,比如选择查看竖直布局的LinearLayout
的测量过程,即measureVertical()
方法,measureVertical()
的源码比较长,下面只描述其大概逻辑,首先看一段代码:
/**
* 在设置此LinearLayout的方向时测量孩子 {@link #VERTICAL}.
*
* @param widthMeasureSpec由父级强加的水平空间需求.
* @param heightMeasureSpec由父级强加的垂直空间要求.
*
* @see #getOrientation()
* @see #setOrientation(int)
* @see #onMeasure(int, int)
*/
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
int maxWidth = 0;
int childState = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
float totalWeight = 0;
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean matchWidth = false;
boolean skippedMeasure = false;
final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
int consumedExcessSpace = 0;
// 看看每个人有多高。 还记得最大宽度。
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// 优化:不要打扰只使用多余空间布置的儿童。
// 如果我们有空间分发,这些观点将在稍后测量。
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
从上面这段代码可以看出,系统会遍历子元素并对每个子元素执行measureChildBeforeLayout()
方法,这个方法内部会调用子元素的measure()
方法,这样各个子元素就开始依次进入measure
过程,并且系统会通过mTotalLength
这个变量来存储LinearLayout
在竖直方向的初步高度。每测量一个子元素,mTotalLength
就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的margin
等。当子元素测量完毕后,LinearLayout
会测量自己的大小,源码如下所示:
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
···
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),heightSizeAndState);
这里对上述代码进行说明,当子元素测量完毕后,LinearLayout
会根据子元素的情况来测量自己的大小。针对竖直的LinearLayout
而言,它在水平方向的测量过程遵循View
的测量过程,在竖直方向的测量过程则和View
有所不同。具体来说是指,如果它的布局中高度采用的是match_parent
或者具体数值,那么它的测量过程和View
一致,即高度为specSize
,如果它的布局中高度采用的是wrap_content
,那么它的高度是所有子元素所占用的高度总和,但是仍然不能超过它的父容器的剩余空间,当然它的最终高度还需要考虑其在竖直方向的padding
,这个过程可以进一步参看如下源码:
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
View
的measure
过程是三大流程中最复杂的一个,measure
完成以后,通过getMeasuredWidth/Height
方法就可以正确地获取到View
的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次measure
才能确定最终的测量宽/高,在这种情形下,在onMeasure
方法中拿到的测量宽/高很可能是不准确的。一个比较好的习惯是在onLayout
方法中去获取View
的测量宽/高或者最终宽/高。
现在考虑一种情况,比如我们想在`Activity`已启动的时候就做一件任务,但是这一件任务需要获取某个`View`的宽/高。你可能会说,这很简单啊,在`onCreate()`或者`onResume()`里面去获取这个`View`的宽/高不就行了?实际上在`onCreate()`,`onStart()`,`onResume()`中均无法正确得到某个`View`的宽/高信息,这是因为`View`的`measure`过程和`Activity`的生命周期方法不是同步执行的,因此无法保证`Activity`执行了`onCreate()`,`onStart()`,`onResume()`时某个`View`已经测量完毕了,如果`View`还没有测量完毕,那么获得的宽/高就是`0`,有没有什么方法能解决这个问题呢?答案是有的。
这里给出四种解决方法
- (1) Activity/View#onWindowFocusChanged
onWindowFocusChanged()
这个方法的含义是:View
已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽/高是没问题的。需要注意的是,onWindowFocusChanged()
会被调用多次,当Activity
的窗口得到焦点和失去焦点时均会被调用一次。具体来说,当Activity
继续执行和暂停执行时,onWindowFocusChanged()
均会被调用,如果频繁地进行onResume()
和onPause()
,那么onWindowFocusChanged()
也会被频繁低调用。
典型代码如下:
public void onWindowFocusChanged(boolean hasWindowFocus) {
InputMethodManager imm = InputMethodManager.peekInstance();
if (!hasWindowFocus) {
if (isPressed()) {
setPressed(false);
}
if (imm != null && (mPrivateFlags & PFLAG_FOCUSED) != 0) {
imm.focusOut(this);
}
removeLongPressCallback();
removeTapCallback();
onFocusLost();
} else if (imm != null && (mPrivateFlags & PFLAG_FOCUSED) != 0) {
imm.focusIn(this);
}
refreshDrawableState();
}
- (2) View.post(runnable)
通过
post
可以将一个runnable
投递到消息队列的尾部,然后等待Looper
调用此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
树的状态改变等,OnGlobalLayout
会被调用多次。
典型代码如下:
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = mTextView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mTextView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = mTextView.getMeasuredWidth();
int height = mTextView.getMeasuredHeight();
}
});
}
- (4) View.measure(int widthMeasureSpec,int heightMeasureSpec)
通过手动对
View
进行measure
来得到View
的宽/高。这种方法比较复杂,这里要分情况处理,根据View
的LayoutParams
来分:
match_parent
直接放弃,无法measure
出具体的宽/高。原因很简单,根据View
的measure
过程,如前一篇的表所示,构造此种MeasureSpec
需要知道parentSize
,即父容器的剩余空间,而这个时候我们无法知道parentSize
的大小,所以理论上不可能测量出View
的大小。
具体的数值(dp/px)
比如宽/高都是100px,如下measure:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
mTextView.measure(widthMeasureSpec,heightMeasureSpec);
wrap_content
如下measure
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);
注意到(1 << 30) -1
,通过分析MeasureSpec
的实现可以知道,View
的尺寸使用30
位二进制表示,也就是说最大是30个1(即2^30 - 1)
,也就是(1 << 30) -1
,在最大化模式下,我们用View
理论上能支持的最大值去构造MeasureSpec
是合理的。
关于View
的measure
,网络上有两个错误的用法。为什么说是错误的,首先其违背了系统的内部实现规范(因为无法通过错误的MeasureSpec
去得出合法的SpecMode
,从而导致measure
过程出错),其次不能保证一定能measure
出正确的结果。
第一种错误:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED);
mTextView.measure(widthMeasureSpec,heightMeasureSpec);
第二种错误用法:
mTextView.measure(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);