1.measure和onMeasure
View中和测量过程相关的方法有三个,measure、onMeasure和setMeasuredDimension。
1.View与ViewGroup的不同
- View的measure调用onMeasure.
- View 中的onMeasure只调用了setMeasuredDimension来设置自身高度。
- 在ViewGroup中,onMeasure方法并没有被重写法,所以继承ViewGroup自定义ViewGroup一定要重写onMeasure来测量子View,否则不会测量子View。
- 在自定义View中,onMeasure方法是在一般情况下使用父容器提供的宽高,除了UNSPECIFIED情况下使用最小尺度。继承View的自定义要根据业务onMeasure要选择性重写。
2.View的onMeasure
2.1 原生的onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
2.1.1 getDefaultSize
其中获取宽高的主要方法是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;
}
当父容器传来的MeasureSpec为UNSPECIFIED时,数值取自己的最小值;为AT_MOST和EXACTLY时,取父容器传递的数值,也就是Wrap_Content和Match_Paraent
2.1.2 setMeasuredDimension
setMeasuredDimension方法是设置view宽高的方法,也是onMeasure必须要调用的方法。
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
setMeasuredDimensionRaw是最终被调用的方法,保存宽高,修改标识符。
如果在onMeasure中没有调用setMeasuredDimension来设置宽高,在measure中,调用onMeasure后,会通过mPrivateFlags来判断是否设置了宽高,没有设置就会抛出异常。所以说setMeasuredDimension是必须要在onMeasure中被调用。
2.2 自定义View重写onMeasure
从getDefaultSize方法实现来看,直接继承View的自定义控件要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。
使用 resolveSize() 来让子 View 的计算结果符合父 View 的限制(当然,如果你想用自己的方式来满足父 View 的限制也行)。
public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
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) {
//标记量算完的尺寸没有达到了View想要的宽度
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);
}
相比原生的的onMeasure中的getDefaultSize,resolveSize对于Wrap_Content情况有了特别处理,在不大于父容器要求的尺寸下,使用用户自己规定的尺寸。
上面的result就是保存的measureSpec的mode值,唯一不同的是第31位被用于标示尺寸是不是达到了View想要的宽度,如果不满足,则标为1。下面把2位标示直观表示下:
UNSPECIFIED 00
EXACTLY 01
AT_MOST 10
result = AT_MOST | MEASURED_STATE_TOO_SMALL 11
resolveSizeAndState的结果在resolveSize进行了截取,只取了数值部分,用来标记量算完的尺寸是不是达到了View想要的宽度的高八位没有保留。
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredWidthAndState() {
return mMeasuredWidth;
}
而getMeasuredWidth和getMeasuredWidthAndState的区别也在此,getMeasuredWidth是获取存数值,而getMeasuredWidthAndState还带有高位state。
线性布局中就用了resolveSizeAndState来获取宽高。自己使用时可配合父容器使用。
3.ViewGroup的onMeasure
虽然ViewGroup没有实现omMeasure的过程,但是它提供了两个工具方法:measureChildren()和getChildMeasureSpec()。
我们都知道父容器会调用子View的measure方法来测量子View,那传入的参数int widthMeasureSpec, int heightMeasureSpec是怎么来的呢?
3.1 理解MeasureSpec
系统会将View的LayoutParams根据父容器所施加的规则转成对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽和高。
MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。
SpecMode:UNSPECIFIED,EXACTLY, AT_MOST(wrap_content)
UNSPECIFIED 00
EXACTLY 01
AT_MOST 10
MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);
通过上面这个方法将尺寸和mode拼接成MeasureSpec。
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
可以看出,MODE_MASK就是32位int值,高两位为1,通过与MODE_MASK的与或来获取mode和size再拼接。
其实View中的MEASURED_SIZE_MASK = ~MODE_MASK,MEASURED_STATE_MASK = MODE_MASK;
public static final int MEASURED_SIZE_MASK = 0x00ffffff;
public static final int MEASURED_STATE_MASK = 0xff000000;
3.2 ViewGroup的getChildMeasureSpec
getChildMeasureSpec就是获取要传递给子View的MeasureSpec的方法,要传给子View的MeasureSpec就是通过这个获取的。它通过
MeasureSpec与子View的宽度来获取值。
public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;
所以在LayoutParams中,设置了固定长度(有意义)的View宽高是不为负数的。
3.2.1 子View设置了layout_width(height)
if (childDimension >= 0) {
//子类自己决定size
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
无论哪种specMode,对于设置了精确长度的子View,取设置的宽高,且mode为EXACTLY。
3.2.2 layout_width(height)="wrap_content"
if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子类自己决定size,但不能超过父类
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
除了specMode为UNSPECIFIED,子View为wrap_content时,取得宽高为方法参数中的尺寸(例如LinearLayout中为其父容器测量传递的MeasureSpec),mode为AT_MOST。
3.2.3 layout_width(height)="match_content"
不考虑UNSPECIFIED
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension == LayoutParams.MATCH_PARENT) {
// 等于父容器的宽度
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension == LayoutParams.MATCH_PARENT) {
// 子View想和父容器尺寸一样,但父容器自己也还没确定尺寸
resultSize = size;
// 子View尺寸肯定不能大于父容器
resultMode = MeasureSpec.AT_MOST;
}
父容器如果有确定的size,那子View就和父View一样;如果没有确定尺寸,那测量子View的MeasureSpec和父容器一样。