在自定义控件的过程中,系统在绘制View前,必须对View进行测量,已使后面的onLayout(设置View的放置位置)能够顺利进行。而对VIew的测量的过程则是在onMeasure()中进行的。可能这时有的同学就发现问题了,说,自己以前自定义的View没有重写onMeasure()方法,仍然可以正常运行,这是因为什么呢?
让我们先从头说起,android系统给我们提供了一个设计短小精悍却功能强大的类———MeasureSpec类,通过它来帮助我们测量View;
测量规格,包含测量要求和尺寸的信息,有三种模式
- UNSPECIFIED
父视图不对子视图有任何约束,它可以达到所期望的任意尺寸。比如 ListView、ScrollView,一般自定义 View 中用不到。 - EXACTLY
父视图为子视图指定一个确切的尺寸,而且无论子视图期望多大,它都必须在该指定大小的边界内,对应的属性为 match_parent 或具体值,比如 100dp,父控件可以通过MeasureSpec.getSize(measureSpec)
直接得到子控件的尺寸。 - AT_MOST
父视图为子视图指定一个最大尺寸。子视图必须确保它自己所有子视图可以适应在该尺寸范围内,对应的属性为 wrap_content,这种模式下,父控件无法确定子 View 的尺寸,只能由子控件自己根据需求去计算自己的尺寸,这种模式就是我们自定义视图需要实现测量逻辑的情况。
相信从上面的三种模式就可以解答上面有些同学的疑问了,下面揭晓答案,View类的onMeasure()方法只支持EXACTLY模式,所以如果在自定义控件的时候不重写onMeasure()方法的话,就只能使用EXACTLY模式。控件可以响应你指定的具体的宽高值或者是match_parent属性,(这就像是上面的同学说的那样,没有重写onMeasure()方法),而如果要让自定义View支持wrap_content属性,那么就必须需要重写onMeasure()方法来指定wrap_content时的大小。
下面我们来看一个简单的实例,演示如何进行View的测量,首先要重写onMeasure()方法,该方法如下所示。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
在IDE中按住Ctrl键查看super。onMeasure()方法。可以发现,系统最终调用setMeasuredDimension(int measuredWidth,int measuredHeight)方法将测量后的宽高值设置进去,从而完成测量工作,所以在重写onMeasure()方法后,最终要做的工作是把测量后的宽高值作为参数设置给setMeasuredDimension()方法。
代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec));
}
在onMeasure()方法中,我们使用自定义的measureWidth()和measuredHeight()方法,对宽高进行重新的定义,参数分别是宽和高的MeasureSpec对象,MeasureSpec对象中包含了测量的模式和测量值的大小。
以measureWidth()为例,第一步,从MeasureSpc对象中提取出具体的测量模式和大小
int specMode=MeasureSpec.getMode(widthMeasureSpec);
int specSize=MeasureSpec.getSize(widthMeasureSpec);
接下来通过判断测量的模式,给出不同的测量值。当specMode为EXACTLY时,直接使用指定的specSize即可;当specMode为其他两个模式时,需要给它一个默认的大小。特别的,如果指定wrap_content属性,即AT_MOST模式,即需要取出我们指定的大小和specSize中最小的一个作为最后的测量值,measureWidth()方法的代码如下所示,
private int measureWidth(int widthMeasureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
//这样,当时用wrap_content时,View就获得一个默认值200px,而不是填充整个父布局。
result = 200;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
好了,今天的View的测量就讲到这里,后面有时间会讲一下View的绘制流程。