Android View的测量(onMeasure)
前言
平常我们在写控件的时候除了指定具体的宽高之外,还会经常使用到wrap_content和match_parent。那么控件究竟是怎样知道究竟自己需要多大的宽高呢?这就得从测量规格开始说起了。
MeasureSpec
MeasureSpec代表了控件宽和高的测量规格。总所周知,一个int占4个字节,32位。所以谷歌用高2位来表示测量模式,剩余的低30位来表示测量大小。测量模式目前有三种:
- UNSPECIFIED
父布局不限制控件的大小,控件想多大就多大。一般只用于可以滑动的父控件(比如ScrollView)传给控件。 - EXACTLY
由父布局决定一个准确的大小给控件,一般对应控件的match_parent和具体的宽高。 - AT_MOST
控件可以根据需求来确定自身的大小,但最大不能超过父布局给的范围,对应控件的wrap_content。
这里的父布局指的是当前控件所在的ViewGroup
如何生成MeasureSpec
MeasureSpec不是控件自己就可以指定的,而是由父布局和控件共同决定的,具体可以参考ViewGroup的getChildMeasureSpec():
/**
* @param spec 当前ViewGroup的测量规格
* @param padding 当前ViewGroup的padding值
* @param childDimension 子控件指定的尺寸
* @return 返回子控件的MeasureSpec
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取测量规格中的测量模式
int specMode = MeasureSpec.getMode(spec);
//获取测量规格中的测量大小
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//子控件指定了具体的尺寸,ViewGroup也是具体的尺寸
//则子控件的测量大小就是指定的尺寸,测量模式就是EXACTLY。
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子控件是MATCH_PARENT,则使用ViewGroup的大小,测量模式也是EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子控件是WRAP_CONTENT,则需要子控件自己测量大小
//但最大不能比ViewGroup大,测量模式是AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//子控件指定了具体的尺寸,ViewGroup是AT_MOST
//ViewGroup的大小最终是由子控件的大小决定的
//所以子控件的测量大小就是指定的尺寸,测量模式就是EXACTLY
//这样在子控件测量出自己的大小之后ViewGroup再测量自己该多大
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子控件是MATCH_PARENT,但是ViewGroup自己的大小也还没确定
//所以只能给子控件的大小约束为不能超过ViewGroup的大小
//子控件的测量模式所以是AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子控件是WRAP_CONTENT,则最大不能比ViewGroup大,测量模式是AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//该测量模式对子控件没限制,子控件想多大就多大
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//子控件指定了具体的尺寸,则子控件的测量大小就是指定的尺寸,测量模式就是EXACTLY。
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//View.sUseZeroUnspecifiedMeasureSpec是一个常量,值为false
//所以子控件大小为ViewGroup大小,测量模式是UNSPECIFIED
//但是一般子控件都是自己测量大小,不会直接使用这个size
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//同上
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//将测量大小和测量模式合成一个int的测量规格
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
由此可见,控件的MeasureSpec是由父控件的MeasureSpec和控件自身设置的大小共同决定的。
生成了MeasureSpec之后是怎样传递到onMeasure()的
那么控件的MeasureSpec是怎样从父控件传递下来的呢?让我们来看下最简单的FrameLayout的onMeasure():
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
int maxHeight = 0;
int maxWidth = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
}
}
}
可以看到里面调用了measureChildWithMargins(),并把自身的MeasureSpec传了进去,那看看measureChildWithMargins():
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
可以看到这里正是调用了getChildMeasureSpec()来获取MeasureSpec,并调用child的measure()当做参数传了进去,而在measure()里面又会调用到onMeasure(),从而实现了父控件根据自身的MeasureSpec和子控件设置的宽高值生成MeasureSpec,并传递到子控件的onMeasure()中。
UNSPECIFIED是怎样产生的
那么问题来了,在getChildMeasureSpec()中我们知道只有父控件的测量模式是UNSPECIFIED的时候,生成给子控件的测量模式才有可能是UNSPECIFIED。但是我们使用的控件是通过setContentView()添加到id为content的FrameLayout里面的,它的测量模式是EXACTLY,那UNSPECIFIED岂不是不会生成了?那当然不是,前面介绍UNSPECIFIED的时候说过类似ScrollView这种就会生成,那么我们来看下ScrollView的measureChildWithMargins()方法:
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
在生成childHeightMeasureSpec的时候它并没有直接使用getChildMeasureSpec()生成MeasureSpec,而是指定了测量模式为UNSPECIFIED,通过makeSafeMeasureSpec来生成。从而使子控件的测量模式变为UNSPECIFIED,因为它作为一个可以滑动的控件,当然是无论子控件想有多高都可以啦。
如何处理MeasureSpec
首先来看下View默认是怎样处理的:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
它直接把getDefaultSize()的值当做最终的测量大小,那么来看下getDefaultSize()和用到的getSuggestedMinimumWidth():
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;
}
//如果View没有设置背景,那么返回android:minWidth这个属性的值,这个值默认为0
//如果View设置了背景,那么返回android:minWidth和背景最小宽度两者中的最大值。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
对于UNSPECIFIED,则result是等于getSuggestedMinimumWidth()的值,其他两种模式则result都是等于测量规格中的大小,这显然是很难满足实际需求的。那么一般控件测量的时候对于各种MeasureSpec是怎么处理的呢,让我们来看下最常用的TextView的onMeasure():
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
height = heightSize;
mDesiredHeightAtMeasure = -1;
} else {
//获取显示文字所需的高度
int desired = getDesiredHeight();
height = desired;
mDesiredHeightAtMeasure = desired;
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);
}
}
}
这里只看它是如何处理高度的,首先获取到测量模式和大小,然后对测量模式进行判断。如果是EXACTLY,那高度直接就等于从测量规格取出的高度。否则高度等于文字所需的高度,此时对应测量模式是UNSPECIFIED的情况。最后再判断一下测量模式是不是AT_MOST,如果是则取desired和heightSize的最小值,因为这种模式下的大小不能大于heightSize的。
所以当我们自定义View的时候除非都是明确指定大小的,否则一定要根据具体需求来对AT_MOST和UNSPECIFIED的情况进行测量。因为自定义View都是根据特定需求来的,就像TextView的wrap_content是适应文字大小,ImageView的是适应图片大小,要不最后显示出来的效果往往会差强人意。所以这里并没有给出自定义View的onMeasure()方法应该怎么写,因为并没有一个统一的标准,需要的是开发者根据具体需求来自行实现。