View类中的onMeasure()方法是用来测量当前View的宽度和高度的,当我们给View设置了LayoutParams以后,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。有3种测量模式:
MeasureSpec.UNSPECIFIED:不确定值,表示子布局想要多大就多大,很少使用
MeasureSpec.AT_MOST:最大值,表示子布局限制在一个最大值内,一般为WRAP_CONTENT
MeasureSpec.EXACTLY:完全准确值,一般是设置了明确的值或者是MATCH_PARENT
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
onMeasure()方法中的两个参数widthMeasureSpec和heightMeasureSpec包含宽和高的信息,一个int整数里,放了测量模式和尺寸大小。我们知道一个int型数据占用32bit,也就是int数据的前面2bit用于区分不同的布局模式,后面的30bit存放的是尺寸的数据。
顶级View(DecorView)和普通View(我们布局中的View)转换MeasureSpec的过程稍有一些不同。顶级View的MeasureSpec由窗口的尺寸和自身的LayoutParams共同确定;普通View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同确定。
顶级View:在ViewRootImpl中,我们可以看到对顶级View的MeasureSpec的设置:
/**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize
* The available width or height of the window
*
* @param rootDimension
* The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
可以看到,顶级View的MeasureSpec根据它自身的LayoutParams参数来划分:
LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。
LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
固定尺寸:精确模式,大小为LayoutParams指定的大小。
普通View:View的measure过程由ViewGroup来传递,在ViewGroup中通过measureChildWithMargins方法来对子元素进行measure,
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
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,
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
*
* The goal is to combine information from our MeasureSpec with the
* LayoutParams of the child to get the best possible results. For example,
* if the this view knows its size (because its MeasureSpec has a mode of
* EXACTLY), and the child has indicated in its LayoutParams that it wants
* to be the same size as the parent, the parent should ask the child to
* layout given an exact size.
*
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension
* @return a MeasureSpec integer for the child
*/
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) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
通过上边代码我们可以总结出一个父容器的MeasureSpec和子容器的MeasureSpec对应关系:
exactly | at_most | unspecified | |
---|---|---|---|
固定值 | exactly: size等于LayoutParams中指定的大小 |
exactly: size等于LayoutParams中指定的大小 |
exactly: size等于LayoutParams中指定的大小 |
match_parent | exactly: size等于父容器的剩余空间 |
at_most: size不会超过父容器的剩余空间 |
unspecified |
wrap_content | at_most: size不会超过父容器的剩余空间 |
at_most: size不会超过父容器的剩余空间 |
unspecified |
左侧是子View的LayoutParams,顶侧是父View的LayoutParams。View是固定值时,不管父容器什么模式,View都是exactly,大小也是其指定的值;View是match_parent时,如果父容器是exactly,View也是exactly,View的size是父容器的剩余空间;如果父容器是at_most,View也是at_most,View的size不会超过父容器的剩余空间;View是wrap_content时,不管父容器是exactly还是at_most,View都是at_most,View的size是不会超过父容器的剩余空间。在实际开发中,unspecified模式主要用于系统内部,一般我们不需要关注。
在自定义View的时候,系统会帮我们测量高度和宽度,当我们设置了明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果;当我们设置为WRAP_CONTENT或MATCH_PARENT时,系统帮我们测量的结果就是MATCH_PARENT的长度,所以当我们设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMeasure()方法。