针对自定义View的尺寸限制,需要直观的考虑以下几个问题:
- 自定义的View最好不要超过其父控件的大小
- 自定义的View(如果是ViewGroup)的子控件最好不要超过该View的大小
- 如果明确了自定义View的尺寸,则按该尺寸来测量
一、父容器的限制与MeasureSpec
比如:有一个父容器,假设其宽高是200dp*200dp,那么其子View的宽高可能有下面三种情况:
情况一:宽高都是match_parent
这样的情况,那么我们期望的子View的宽高就应该是200dp*200dp
android:layout_width="match_parent"
android:layout_height="match_parent"
情况二:宽高都是100dp
这样的情况,我们期望的子View的宽高都是100dp
android:layout_width="100dp"
android:layout_height="100dp"
情况三:宽高都是wrap_content
这样的情况,我们期望子View的宽高是按照自己需求的尺寸来确定,但是最好不要超过200dp
android:layout_width="wrap_content"
android:layout_height="wrap_content"
父控件通过其自身的MeasureSpec告诉子View父控件本身的一个述求,父控件传给子View的MeasureSpec其实就是给子View做一个参考,具体子View的宽高还是需要子View自己决定,可以不根据该参考来确定。
MeasureSpec构成
MeasureSpec是由size和mode组成,MeasureSpec是一个int类型的值,长度是32位,其高两位是mode的值,低30位是size的值。
MeasureSpec的mode有三种类型:UNSPECIFIED、EXACTLY、AT_MOST,size就是父控件给子View的一个参考大小。
- UNSPECIFIED(未指定),父控件对子控件不加任何束缚,子元素可以得到任意想要的大小,这种MeasureSpec一般是由父控件自身的特性决定的。比如ScrollView,它的子View可以随意设置大小,无论多高,都能滚动显示,这个时候,size一般就没什么意义。
- EXACTLY(完全),父控件为子View指定确切大小,希望子View完全按照自己给定尺寸来处理,跟上面的场景1跟2比较相似,这时的MeasureSpec一般是父控件根据自身的MeasureSpec跟子View的布局参数来确定的。一般这种情况下size>0,有个确定值。
- AT_MOST(至多),父控件为子元素指定最大参考尺寸,希望子View的尺寸不要超过这个尺寸,跟上面场景3比较相似。这种模式也是父控件根据自身的MeasureSpec跟子View的布局参数来确定的,一般是子View的布局参数采用wrap_content的时候。
源码确定child的MeasureSpec的构造
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
// 构建子View的MeasureSpec
// 在getChildMeasureSpec内部计算size的时候会减去padding
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
所有的View都是支持padding,所以在为子View设置参考尺寸的时候,需要去除其padding值,这样得到的大小size才是子View能够放置的区域。
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);
}
上面代码的关系,总的来看,可以总结为下面这个表:
image.png
当子View接收到父控件传递来的MeasureSpec之后,就可以知道父控件对自身的要求是怎么样的,而具体的传递是在自定义View的onMeasure方法中进行,如果是自定义ViewGroup,则需要考虑该自定义View的子View测量。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
getSuggestedMinimumHeight方法是根据设置的背景和最小尺寸大小给出的一个建议尺寸
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的时候没有重写onMeasure方法,那么自定义View的尺寸在AT_MOST和EXACTLY两种模式下是一样的,都是由其父控件的MeasureSpec给定的参考大小来确定。
二、自定义尺寸的确定
在onMeasure方法中测量尺寸是最合理的时机,如果自定义View不是ViewGroup,那么只需要参照父控件传递下来的MeasureSpec结合自身尺寸测量规则,最终调用setMeasuredDimension即可。如果自定义View是一个ViewGroup的话,那么就需要结合考虑ViewGroup中的子View的布局和排版以及子View的宽高尺寸,然后结合子View在该自定义ViewGroup中的布局排版计算该自定义ViewGroup的宽高。
以FlowLayout流式布局为例,自定义ViewGroup中,FlowLayout流式布局是一个最常用的例子:
FlowLayout需要知道以下几点:
- 父容器传递给FlowLayout的MeasureSpec
- FlowLayout中所有子View的宽高
- 结合MeasureSpec以及FlowLayout自身需求(比如FlowLayout中的子View的高度是match_parent)
ViewGroup.java源码中也提供了比较简洁的方法,有两个比较常用的measureChildren跟resolveSize,在之前的分析中我们知道measureChildren会调用getChildMeasureSpec为子View创建MeasureSpec,并通过measureChild测量每个子View的尺寸。那么resolveSize呢,看下面源码,resolveSize(int size, int measureSpec)的两个输入参数,第一个参数:size,是View自身希望获取的尺寸,第二参数:measureSpec,其实父控件传递给View,推荐View获取的尺寸,resolveSize就是综合考量两个参数,最后给一个建议的尺寸:
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) {
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);
}
- 如果父控件传递给的MeasureSpec的mode是MeasureSpec.UNSPECIFIED,就说明,父控件对自己没有任何限制,那么尺寸就选择自己需要的尺寸size
- 如果父控件传递给的MeasureSpec的mode是MeasureSpec.EXACTLY,就说明父控件有明确的要求,希望自己能用measureSpec中的尺寸,这时就推荐使用MeasureSpec.getSize(measureSpec)
- 如果父控件传递给的MeasureSpec的mode是MeasureSpec.AT_MOST,就说明父控件希望自己不要超出MeasureSpec.getSize(measureSpec),如果超出了,就选择MeasureSpec.getSize(measureSpec),否则用自己想要的尺寸就行了
其实设置自定义ViewGroup的尺寸,只需要三步:
- 测量所有子View,获取到所有子View的尺寸
- 根据自身特点和排版布局,依赖所有子View的尺寸计算自身的尺寸
- 对比计算得到的尺寸和父容器传递的MeasureSpec的参考尺寸,得到一个合适的值
三、顶层View的MeasureSpec是谁指定
传递给子View的MeasureSpec是父容器根据自己的MeasureSpec及子View的布局参数所确定的,那么根MeasureSpec是谁创建的呢?我们用最常用的两种Window来解释一下,Activity与Dialog,DecorView是Activity的根布局,传递给DecorView的MeasureSpec是系统根据Activity或者Dialog的Theme来确定的,也就是说,最初的MeasureSpec是直接根据Window的属性构建的,一般对于Activity来说,根MeasureSpec是EXACTLY+屏幕尺寸,对于Dialog来说,如果不做特殊设定会采用AT_MOST+屏幕尺寸。这里牵扯到WindowManagerService跟ActivityManagerService,感兴趣的可以跟踪一下WindowManager.LayoutParams ,后面也会专门分析一下,比如,实现最简单试的全屏的Dialog就跟这些知识相关。
网友评论