MeasureSpec——View测量过程中的建桥
一.什么是MeasureSpec
在View工作的流程中,measure过程决定view的宽高,在view的measure过程中,MeasureSpec起到了至关重要的作用,它参与了Measure的测量过程。
我们知道,一个view的宽高有时受到父容器的影响,在测量工程中,系统会将view的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,再根据这个MeasureSpec测量出View的宽高,所以View很大程度上决定了view的尺寸规格。所以这个很抽象的MeasureSpec我们可以把它理解为“策略规格书”。
二.探索MeasureSpec
先看下View的内部类MeasureSpec的内容:
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
MeasureSpec代表一个32位的int值,通过SpecMode和SpecSize组合成一个int来节约内存,其中高2位代表SpecMode,指测量模式,低30位代表SpecSize,指该测量模式下的规格大小。通过getMode和getSize可以得到其中的值。
通过让0,1,2向左移位30定义了三个常量作为SpecMode的三种类型:
EXACTLY
检查出view的精确大小,此时view的宽高就等于SpecSize的宽高。
AT_MOST
父容器指定了一个可用大小的SpecSize,view宽高不能大于这个值。
用于wrapcontent时,内容包裹大小,但view大小不会超过父容器大小。
UNSPECIFIED
对view不做任何限制,不常出现,多用于scrollview这种要显示内容远大于可显示区域的情况。
二.MeasureSpec与LayoutParams
举个例子,这是自定义view的时候重写onMeasure中的一小段:
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int height;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = getPaddingTop() + mBound.height() + getPaddingBottom();
if(heightMode==MeasureSpec.AT_MOST){
height = Math.min(height,heightSize);
}
}
可以看出,这里通过条件判断SpecMode对view的高进行了约束,一般情况下,我们可以给view设置LayoutParams,比如宽高或者wrapcontent,细心的同学会发现重写onMeasure的时候我们会发现传了两个参数,宽高的MeasureSpec,这两个参数是伴随父容器传下来的,它会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后根据这个MS来确定View的宽高。
对于一般的View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定。
那对于顶级View(DecorView)来说没有上层父容器了,所以其MeasureSpec由窗口的尺寸和自身的LayoutParams来决定。
三.MeasureSpec的创建过程
顶级view的MeasureSpec的创建过程与普通view有所不同,MeasureSpec由窗口的尺寸和自身的LayoutParams来决定,很好理解,父容器=屏幕。这里代码就不贴了。
总来就说就是跟屏幕尺寸有关:
- 如果设置matchparent:大小就是屏幕大小。
- wrapcontent:内容包裹,大小不能超过屏幕大小。
- 固定大小(android:layout_height=”100dp”):指定多大就是多大。
那么对于普通View来说,measure过程由viewGroup传递过来,
viewGroup中的measureChildWithMargin方法:
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);//调用子元素的measure方法
}
注意child.measure传的参数就是我们之前所说的重写onMeasure方法得到的那两个参数,通过getChildMeasureSpec方法(注意传的参数)来得到子元素的MeasureSpec,所以解释了为什么子元素MeasureSpec的创建与父容器的的MeasureSpec和子元素和子元素本身的LayoutParams有关,还与View的margin和padding有关:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//size为子容器可用大小,为父容器减去padding的大小
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
......//省略部分
}
上述measureChildWithMargin方法传递了参数为父容器的MeasureSpec和子容器的padding=mPaddingTop+mPaddingBottom+lp.topMargin+lp.bottomMargin+ heightUsed(还有一组宽度),通过父容器的MeasureSpec结合view本身的LayoutParams来确定子元素的MeasureSpec。
那么总结来说:
- view采用固定模式:
- 不管父容器的SpecMode是什么,view的MeasureSpec都是EXACTLY,大小就是开发者设置的大小。
- View是matchparent:
- 父容器为EXACTLY,那么view也是EXACTLY,大小是父容器的剩余空间。
- 父容器是AT_MOST,view也是AT_MOST,大小不能超过父容器的剩余大小。
- view是wrapcontent:
- 不管父容器为什么,view的模式总是AT_MOST,大小不超过父容器的剩余大小。
- view为UNSPECIFIED:
- 理论没有限制,可以无限大。