2. View的测量(Measure)机制:MeasureSpec的三种模式与onMeasure重写
大家好,我是老张。今天我们来聊聊View测量机制里最核心的东西——MeasureSpec。说实话,我见过不少开发者在自定义View时,一碰到onMeasure就发怵。其实没那么玄乎,咱们把它拆开揉碎了讲清楚。
2.1 MeasureSpec到底是什么?
MeasureSpec,说白了就是一个32位的int值。它把尺寸信息打包成了两部分:高2位存模式,低30位存大小。你可能会问,为什么不用两个单独的变量?嗯,这是Android设计者为了性能考虑,减少对象创建的开销。
核心公式:MeasureSpec = 模式(2位) + 大小(30位)
我在项目中遇到过一个问题:有同事直接拿MeasureSpec.getSize()的值去设置View的宽高,结果在某些设备上显示异常。后来才发现,他没考虑模式,导致UNSPECIFIED模式下拿到了错误的值。
2.2 三种模式详解
| 模式 | 二进制值 | 含义 | 典型场景 |
|---|---|---|---|
| EXACTLY | 00 | 精确值,父View已经确定了子View的大小 | match_parent或具体dp值 |
| AT_MOST | 01 | 最大值,子View不能超过这个尺寸 | wrap_content |
| UNSPECIFIED | 10 | 未指定,子View想要多大就多大 | ScrollView、ListView内部 |
你想想看,这三种模式其实对应了三种不同的布局约束。EXACTLY就是「你必须这么大」,AT_MOST是「你最大只能这么大」,UNSPECIFIED则是「你随意」。我刚开始学的时候,总觉得UNSPECIFIED没什么用,直到做自定义滑动容器时才明白它的价值。
2.3 从源码看MeasureSpec的创建
我们来看看系统是怎么生成MeasureSpec的。核心代码在ViewGroup的getChildMeasureSpec方法里:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// ... 其他模式类似
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这段逻辑其实很清晰:父View把自己的MeasureSpec和子View的LayoutParams结合起来,算出子View的MeasureSpec。我个人习惯把这段源码背下来,面试经常考,而且写自定义ViewGroup时也经常用到。
2.4 onMeasure方法的重写
当我们自定义View时,重写onMeasure是家常便饭。为什么要重写?因为系统默认的测量逻辑不一定符合我们的需求。比如一个圆形ImageView,我们希望它的宽高相等,那就得自己处理。
我的经验:重写onMeasure时,一定要调用setMeasuredDimension()来保存测量结果。忘了这步,View会显示不出来,我当年就踩过这个坑。
来看一个实际例子——自定义正方形View:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
// 取宽高中较小的值,保证正方形
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}
这段代码看起来简单,但有个问题:如果父View给的是EXACTLY模式,我们强行改成正方形,可能会超出父View的约束。所以更严谨的做法是:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 处理UNSPECIFIED的情况
if (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = 200; // 默认值
}
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = 200;
}
// 取最小值保证正方形
int size = Math.min(widthSize, heightSize);
setMeasuredDimension(size, size);
}
注意:千万不要在onMeasure里直接new对象,这个方法可能会被频繁调用,造成内存抖动。我曾经在一个列表项的onMeasure里创建了Paint对象,结果滑动时卡成PPT。
2.5 自定义ViewGroup的测量
如果你要自定义ViewGroup,那测量逻辑就更复杂了。你需要遍历所有子View,调用measureChild(),然后根据子View的尺寸算出自己的尺寸。
我给大家画个流程图,看看整个测量过程是怎么串起来的:
看明白了吗?整个流程就是从ViewRootImpl开始,一层层往下传MeasureSpec。每个ViewGroup都要负责给子View算MeasureSpec,然后子View再根据自己的情况决定最终尺寸。
2.6 实战中的避坑指南
我总结几个实际开发中容易踩的坑:
- 坑一:在onMeasure里直接返回固定值。我曾经有个同事写了个自定义View,onMeasure里直接setMeasuredDimension(100, 100),结果在wrap_content时显示正常,match_parent时反而出问题。正确的做法是根据MeasureSpec的模式做判断。
- 坑二:忘记调用super.onMeasure()。如果你不需要特殊处理,最好还是调一下父类的方法,否则可能连基本的padding都不生效。
- 坑三:在onMeasure里做耗时操作。这个方法在布局阶段会多次调用,尤其是嵌套布局时。我见过有人在里面做Bitmap解码,直接导致界面卡顿。
我的建议:写自定义View时,先想清楚你要支持哪些布局参数。如果只支持固定尺寸,那onMeasure可以很简单。如果要支持wrap_content和match_parent,那就得把三种模式都处理到位。
2.7 测量结果的使用
测量完成后,我们可以通过getMeasuredWidth()和getMeasuredHeight()拿到结果。但要注意,这两个值在onMeasure之后、onLayout之前才有效。如果你在构造函数里调用,拿到的肯定是0。
我记得有个项目,需要在View创建时就拿到宽高。有同事在构造函数里调了measure()方法手动触发测量,结果因为父View还没准备好,拿到的值完全不对。正确的做法是在onSizeChanged()里获取,或者使用ViewTreeObserver监听。
好了,关于View的测量机制,核心就是这些。MeasureSpec的三种模式要理解透彻,onMeasure的重写要谨慎处理。下一节我们聊聊布局(Layout)机制,看看测量完的尺寸是怎么被安排到界面上的。
公众号:蓝海资料掘金营,微信deep3321