3.2 View的测量

那么在绘制View之前,我们还需要思考一下,系统是如何绘制出这些View的。相信大家都曾经玩过这样一个游戏:一个人蒙着眼睛,拿笔去画板上画出一个指定的图案,另一个人则通过说话来指导他如何去画。比如你会指导他,在距画板边缘一掌宽的地方画一个边长大概10厘米的正方形,而如果你只告诉他,画一个矩形,那么你的同伴就无法准确地画出这个图形了。其实,Android就好像那个蒙着眼睛画画的人,你必须精确地告诉它该如何去画,它才能绘制出你想要的图形。

在现实生活中,如果我们要去画一个图形,就必须知道它的大小和位置。同样,Android系统在绘制View前,也必须对View进行测量,即告诉系统该画一个多大的View。这个过程在onMeasure()方法中进行。

Android系统给我们提供了一个设计短小精悍却功能强大的类——MeasureSpec类,通过它来帮助我们测量View。MeasureSpec是一个32位的int值,其中高2位为测量的模式,低30位为测量的大小,在计算中使用位运算的原因是为了提高并优化效率。

测量的模式可以为以下三种。

  • EXACTLY

即精确值模式,当我们将控件的layout_width属性或layout_height属性指定为具体数值时,比如andorid:layout_width="100dp",或者指定为match_parent属性时(占据父View的大小),系统使用的是EXACTLY模式。

  • AT_MOST

即最大值模式,当控件的layout_width属性或layout_height属性指定为wrap_content时,控件大小一般随着控件的子空间或内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可。

  • UNSPECIFIED

这个属性比较奇怪——它不指定其大小测量模式,View想多大就多大,通常情况下在绘制自定义View时才会使用。

View类默认的onMeasure()方法只支持EXACTLY模式,所以如果在自定义控件的时候不重写onMeasure()方法的话,就只能使用EXACTLY模式。控件可以响应你指定的具体宽高值或者是match_parent属性。而如果要让自定义View支持wrap_content属性,那么就必须重写onMeasure()方法来指定wrap_content时的大小。

通过MeasureSpec这一个类,我们就获取了View的测量模式和View想要绘制的大小。有了这些信息,我们就可以控制View最后显示的大小。

下面我们就来看一个简单的实例,演示如何进行View的测量。首先,要重写onMeasure()方法,该方法如下所示。

  1. @Override
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  3. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  4. }

在IDE中按住Ctrl键查看super.onMeasure()方法。可以发现,系统最终会调用setMeasuredDimension(int measuredWidth, int measuredHeight)方法将测量后的宽高值设置进去,从而完成测量工作。所以在重写onMeasure()方法后,最终要做的工作就是把测量后的宽高值作为参数设置给setMeasuredDimension()方法。

通过上面的分析,重写的onMeasure()方法代码如下所示。

  1. @Override
  2. protected void onMeasure(int widthMeasureSpec,
  3. int heightMeasureSpec) {
  4. setMeasuredDimension(
  5. measureWidth(widthMeasureSpec),
  6. measureHeight(heightMeasureSpec));
  7. }

在onMeasure()方法中,我们调用自定义的measureWidth()方法和measureHeight()方法,分别对宽高进行重新定义,参数则是宽和高的MeasureSpec对象,MeasureSpec对象中包含了测量的模式和测量值的大小。

下面我们就以measureWidth()方法为例,讲解如何自定义测量值。

第一步,从MeasureSpec对象中提取出具体的测量模式和大小,代码如下所示。

  1. int specMode = MeasureSpec.getMode(measureSpec);
  2. int specSize = MeasureSpec.getSize(measureSpec);

接下来,通过判断测量的模式,给出不同的测量值。当specMode为EXACTLY时,直接使用指定的specSize即可;当specMode为其他两种模式时,需要给它一个默认的大小。特别地,如果指定wrap_content属性,即AT_MOST模式,则需要取出我们指定的大小与specSize中最小的一个来作为最后的测量值,measureWidth()方法的代码如下所示。这段代码基本上也可以作为模板代码。

  1. private int measureWidth(int measureSpec) {
  2. int result = 0;
  3. int specMode = MeasureSpec.getMode(measureSpec);
  4. int specSize = MeasureSpec.getSize(measureSpec);
  5.  
  6. if (specMode == MeasureSpec.EXACTLY) {
  7. result = specSize;
  8. } else {
  9. result = 200;
  10. if (specMode == MeasureSpec.AT_MOST) {
  11. result = Math.min(result, specSize);
  12. }
  13. }
  14. return result;
  15. }

measureHeight()方法与measureWidth()基本一致,这里就不再给出代码了,通过这两个方法,我们就完成了对宽高值的自定义。最后,可以在程序中验证以上的分析。

在布局文件中,先指定确定的宽高值400px,程序运行效果如图3.4所示。

3.2 View的测量 - 图1 图3.4 指定宽高值为400px

当指定宽高属性为match_parent属性时,程序运行效果如图3.5所示。

3.2 View的测量 - 图2 图3.5 宽高属性为match_parent

当指定宽高属性为wrap_content属性时,如果不重写onMeasure()方法,那么系统就不知道该使用默认多大的尺寸。因此,它就会默认填充整个父布局,所以重写onMeasure()方法的目的,就是为了能够给View一个wrap_content属性下的默认大小,程序运行效果如图3.5a所示。

3.2 View的测量 - 图3 图3.5a 宽高属性为wrap_content

可以发现,当指定wrap_content属性时,View就获得了一个默认值200px,而不是再填充父布局了。

通过这个小的实例,相信大家应该对View的测量不再陌生了,它并没有什么高深莫测的东西,它的整个过程与我们在生活中精确绘图的过程基本是一样的。