Anroid中的自定义View测量

之前已经讲过了 Android 中 View 的绘制流程,上次主要讲的是onDraw方法,这次主要讲的就是在onMeasure方法中对 View 的大小进行测量。

理解 MeasureSpec

要了解如何在onMeasure方法中对 View 进行测量,我们首先需要了解的就是onMeasure方法传入的两个 int 值:widthMeasureSpecheightMeasureSpec

它们都是32位的 int 值,高2位代表 SpecMode(测量模式),低30位代表 SpecSize(对应模式下的测量大小)。通过以下的代码我们可以了解到 MeasureSpec 的原理:

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
private static final int MODE_SHIFT = 30; // Mode 的移位(高2位也就是左移30位)
// 以下四个都是 Mode 常量
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
private static final int UNSPECIFIED = 0 << MODE_SHIFT;
private static final int EXACTLY = 1 << MODE_SHIFT;
private static final int AT_MOST = 2 << MODE_SHIFT;

// 该方法用于组装 MeasureSpec,其中 sUseBrokenMakeMeasureSpec 是一个兼容参数,如果为 true 时可能会出错(sdk19之后默认走底下的逻辑)
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}

// 获取 Mode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}

// 获取 Size
public static int getSize(int measureSpec) {
return (size & ~MODE_MASK);
}

因为 Android 中会有大量的 View 存在,所以必然会有很多 MeasureSpec,如果将 MeasureSpec 封装成一个对象必然会造成大量的对象内存分配,这也不难理解为什么要将其包装成一个 int 了。

SpecMode

SpecMode 有三类,我们在前面的代码定义中看到了有五个常量,其中两个是作为工具存在的(MODE_SHIFT 和 MODE_MASK),另外三个就是 SpecMode 了。

UNSPECIFIED

该模式下父容器不对 View 的大小有任何限制,一般不做处理。

EXACTLY

父容器已经检测出 View 所需要的精确大小,此时 View 的最终大小就是 SpecSize 指定的大小。

对应 LayoutParams 中match_parent以及具体数值。

AT_MOST

父容器指定了一个 SpecSize,View 不能大于这个值。

它对应于 LayoutParams 中的wrap_content

与 Layout_Params 的关系

在 View 测量的时候,会将 Layout_Params 在父容器的约束下转换成对应的 MeasureSpec,然后根据这个 MeasureSpec 确认 View 测量后的宽高。一旦 MeasureSpec 确认了,在onMesure中就可以确认 View 的测量宽高了。

  • match_parent: 对应 EXACTLY
  • 精确值: 对应 EXACTLY
  • wrap_content: 对应 AT_MOST

measure 过程

measure 过程要分为 View 和 ViewGroup,它们的测量是不同的

View

由其measure方法完成,该方法是final关键字修饰的,无法重写。但measure会调用onMeasure,所以只需要看onMeasure如何实现即可。

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
29
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), HeightMeasureSpec));
}

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.ATMOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}

return result;
}

protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

其逻辑很简单,getDefaultSize方法中可以看出,View 的宽高由 SpecSize 决定。于是我们知道:直接继承 View 的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则使用wrap_content属性是无效的(等同于match_parent)。

所以我们可以这样实现来使得wrap_content生效:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

int realWidth = widthSpecMode == MeasureSpec.AT_MOST ? mWidth : widthSpecSize;
int realHeight = heightSpecMode == MeasureSpec.AT_MOST ? mHeight : heightSpecSize;

setMeasuredDimension(realWidth, realHeight);
}

在如上代码中我们只需要指定默认最小时的mWidth,mHeight即可(wrap_content的默认宽高),其它模式下交给系统测量即可。

需要注意的是onMeasure方法中获取到的测量宽高并不一定就是控件的最终宽高,比如 RelativeLayout 中的控件会有多次测量,LinearLayout 中的子控件如果设置了weight也会有多次测量,那么第一次onMeasure的就不会准了。

ViewGroup

其实就是在测量自己的宽高之后还会调用measureChildren来遍历子控件并且测量子控件的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

ViewGroup 是一个抽象类,其onMeasure方法是没有具体实现的,所以我们继承 ViewGroup 必须重写onMeasure,重写该方法需要进行的步骤如下:

  1. 调用super.onMeasure(widthMeasureSpec, heightMeasureSpec)处理非wrap_content的情况
  2. 单独处理wrap_content,即 SpecMode 为AT_MOST的情况
  3. 遍历子 View,并测量子 View

测量子 View 我们可以使用这几个方法

1
2
3
4
5
6
7
8
9
10
11
// 使用子view自身的测量方法
subView.measure(int wSpec, int hSpec);

// ViewGroup 的测量子 View 方法
// 某一个子view,多宽,多高, 内部加上了 viewGroup 的 padding 值
measureChild(subView, int wSpec, int hSpec);
// 所有子view 都是 多宽,多高, 内部调用了 measureChild 方法
measureChildren(int wSpec, int hSpec);
// 某一个子view,多宽,多高, 内部加上了 viewGroup 的 padding 值、margin 值和传入的宽高 wUsed、hUsed
measureChildWithMargins(subView, intwSpec, int wUsed, int hSpec, int hUsed);

总结

View 的测量基本就是如上所述了,自定义 View 需要重写onMeasure方法并对wrap_content进行特殊处理,其实说起来需要做的并不多,但原理还是满复杂的,全部了解了之后还是觉得学到了不少东西。

作者

Loshine

发布于

2016-04-22

更新于

2024-04-01

许可协议

评论