之前已经讲过了 Android 中 View 的绘制流程,上次主要讲的是onDraw
方法,这次主要讲的就是在onMeasure
方法中对 View 的大小进行测量。
理解 MeasureSpec
要了解如何在onMeasure
方法中对 View 进行测量,我们首先需要了解的就是onMeasure
方法传入的两个 int 值:widthMeasureSpec 和 heightMeasureSpec。
它们都是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;
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;
public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } }
public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); }
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
,重写该方法需要进行的步骤如下:
- 调用
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
处理非wrap_content
的情况
- 单独处理
wrap_content
,即 SpecMode 为AT_MOST
的情况
- 遍历子 View,并测量子 View
测量子 View 我们可以使用这几个方法
1 2 3 4 5 6 7 8 9 10 11
| subView.measure(int wSpec, int hSpec);
measureChild(subView, int wSpec, int hSpec);
measureChildren(int wSpec, int hSpec);
measureChildWithMargins(subView, intwSpec, int wUsed, int hSpec, int hUsed);
|
总结
View 的测量基本就是如上所述了,自定义 View 需要重写onMeasure
方法并对wrap_content
进行特殊处理,其实说起来需要做的并不多,但原理还是满复杂的,全部了解了之后还是觉得学到了不少东西。