自定义 View 之 onMeasure

现阶段的目标就是:好好学习自定义 View 如何实现,只知道理论不行,必须实际攻克这个难题了,做出真正有用的自定义 View 出来。先总结一些关于自定义 View 遇到的问题,结合源码分析会更明确。

本节先说测量,即 onMeasure。我们在使用系统提供的控件时,几乎都会使用 layout_width(和 layout_height,下同) 这样的属性来设置控件的宽和高,其实这就是在通过 XML 的方式告诉系统对于该 View 的控件如何去 measure。那么下面就简单自定义一个 View,咱们也给自己的 View 用 layout_width 指定个宽高试试。

自定义View

1
2
3
4
5
6
public class MyView extends View {
// 从 xml 中使用控件必须要写的构造方法
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}

布局文件

1
2
3
4
5
6
7
8
9
10
11
12
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<com.icodeyou.viewmeasure.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#abcdef" />

</RelativeLayout>

我们先后改一下 layout_width 和 layout_height 属性,当其属性为 30dp, match_parent 的时候,发现控件显示的大小确实符合我们的预期。但是,当我们指定其为 wrap_content 的时候,却意外发现和 match_parent 效果是相同的,即铺满了整个父级容器,为什么会这样,我们需要根据源码来看一下了。

在 View 中,有个方法叫做 onMeasure,也就是本节要讨论的主题,源码如下:

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

要看懂以上代码,需要先知道什么是 MeasureSpec。简单来说,这个类可以帮我们保存控件测量的模式和测量的大小,本质上是一个32位的 int 值,其中高2位为测量的模式,低30位为测量的大小。这么说没意思,直接看源码,它是 View 的一个静态内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;

// 使用高2位来保存测量模式
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;

// 根据传入的测量大小和测量模式返回 MeasureSpec 对象,其实就是一个int
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}

// 从传入的 MeasureSpec 中取得测量模式,取其高2位
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}

// 从传入的 MeasureSpec 中取得测量大小,取其低30位
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}

看源代码就很清晰 MeasureSpec 这么短小精悍的类是怎么工作的了,主要就是用到了移位和关系与逻辑运算来操作,下面来具体说说三种 MeasureSpec Mode 的区别是什么。

  • EXACTLY
    精确模式,当 layout_width 指定为 100dp 和 match_parent 时,即使用这种模式。

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

  • UNSPECIFIED
    想多大就多大,至今没用过。。。

另外,specSize 的单位是 px,而不是 dp,可以自己输出看一下。(我320dpi的模拟器,输出结果为 xml 中指定 dp 数值的2倍)看懂了什么是 MeasureSpec,下面可以再回顾我们刚才贴出来的 View 中 onMeasure 默认的代码了:

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

其实就是通过 setMeasureDimension 来设置 View 的大小的,里面传入的分别是宽和高,可以自己重写这个方法然后写个具体的值进去看看效果(其实就是无论在 xml 中怎么指定大小,都会以你自己在这里写的值为准)。那么继续看 getDefaultSize() 这个方法干了些什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
// 注意 AT_MOST 后面没有 break
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

好好看看这段代码,发现 AT_MOST 和 EXACTLY 两种模式都是返回的 specSize 啊,当指定为 wrap_content 的时候就是 AT_MOST 模式,那最大值就是父容器的大小了,所以会出现我们在文章开始提到的那个问题–指定为 wrap_content 时控件会铺满父级容器。

跟着源码过了一遍原理,那么下面我们再来提一个需求–当指定为 wrap_content 的时候,将控件大小设置为 200 px,而不是任由它铺满整个父级容器,想一想这个应该怎么来实现呢?

首先,肯定是要重写 onMeasure() 方法了,在其 setMeasuredDimension() 方法中传入我们设置好的宽和高,就可以实现我们的需求了,那么子 View 的 onMeasure 代码如下:

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

我们又自己写了两个方法当做 setMeasuredDimension 的参数,下面分析其中的一个即可,比如 measureWidth():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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{
result = 200;
// wrap_content 的话,根据刚才的需求,大小只能是 200 和 specSize 的最小值
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}

// 注意返回值单位是 px,不是 dp
return result;
}

此时,当我们在 xml 中指定控件大小为 wrap_content 的时候,大小就会是 200 px了,而不是铺满了整个父级容器,效果如图:

image

此篇文章浅层次分析了 View 的测量,重点是 onMeasure 方法的作用,下篇文章准备继续深度说说 View 的测量流程。

个人GitHub: http://github.com/icodeu

CSDN博客:http://blog.csdn.net/icodeyou

个人微信号:qqwanghuan 技术交流

image