4、onMeasure源码分析:View的默认onMeasure实现、ViewGroup的measureChildren流程、自定义View时如何重写onMeasure
聊到View绘制流程,onMeasure是绕不开的第一道坎。我刚开始接触自定义View时,总觉得这步很玄乎——不就是量个尺寸吗?后来踩了几个坑才明白,measure阶段要是没搞对,后面画得再漂亮也是白搭。
今天咱们就掰开揉碎,把onMeasure的源码逻辑、ViewGroup怎么给孩子量尺寸、以及我们自己写自定义View时该怎么重写,一次性说清楚。
4.1 View的默认onMeasure实现
先看最基础的。View类里有个默认的onMeasure方法,长这样:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
说白了,它调了两个关键方法:getDefaultSize 和 getSuggestedMinimumWidth/Height。
getSuggestedMinimumWidth 返回的是什么呢?看源码:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
逻辑很简单——如果有背景,取背景的最小宽度和android:minWidth的较大值;没背景就直接返回minWidth。嗯,这里要注意,如果你既没设背景也没设minWidth,那返回的就是0。
再看 getDefaultSize:
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.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
看到没?对于AT_MOST和EXACTLY,它直接返回了MeasureSpec里的size。这意味着什么?
为什么会这样?因为AT_MOST模式下,父容器给了你一个最大尺寸,默认实现就直接拿这个最大尺寸当结果了。它根本没考虑内容应该多大。
4.2 ViewGroup的measureChildren流程
ViewGroup作为容器,它的职责是给所有子View量尺寸。核心流程在 measureChildren 方法里:
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);
}
}
}
逻辑很直白——遍历所有非GONE的子View,逐个调用 measureChild。
measureChild 才是真正干活的地方:
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);
}
这里的关键是 getChildMeasureSpec。它根据父容器的MeasureSpec、padding、子View的LayoutParams,生成子View的MeasureSpec。
我画了一张图,帮你理清这个决策逻辑:
你看,子View的LayoutParams不同,生成的MeasureSpec就完全不同。比如子View写死了100dp,那不管父容器是什么模式,子View拿到的都是EXACTLY + 100px。但如果是WRAP_CONTENT,父容器就会给一个AT_MOST,限制最大尺寸。
我个人习惯在自定义ViewGroup时,如果布局逻辑比较复杂,会直接重写 onMeasure 而不是用 measureChildren。因为默认的measure流程只考虑了padding,没考虑margin。你想想看,如果你的子View设了margin,用 measureChildren 就会漏掉这部分空间。
4.3 自定义View时如何重写onMeasure
好了,前面铺垫了这么多,终于到实战环节了。重写onMeasure,核心就两件事:
- 计算自身内容需要多大尺寸
- 根据MeasureSpec的约束,确定最终尺寸
我直接给一个通用模板,你以后写自定义View可以直接套:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 计算内容需要的尺寸
int contentWidth = calculateContentWidth();
int contentHeight = calculateContentHeight();
// 2. 根据MeasureSpec确定最终尺寸
int width = resolveSize(contentWidth, widthMeasureSpec);
int height = resolveSize(contentHeight, heightMeasureSpec);
// 3. 保存结果
setMeasuredDimension(width, height);
}
private int resolveSize(int contentSize, int measureSpec) {
int result = contentSize;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.EXACTLY:
// 父容器说了算,直接拿specSize
result = specSize;
break;
case MeasureSpec.AT_MOST:
// 不能超过父容器给的最大值,但也不能小于内容
result = Math.min(contentSize, specSize);
break;
case MeasureSpec.UNSPECIFIED:
// 随便你多大,那就用内容尺寸
result = contentSize;
break;
}
return result;
}
resolveSize 方法,就在 View 类里。你直接调 resolveSize(contentSize, measureSpec) 就行,不用自己写switch。我上面写出来是为了让你理解内部逻辑。
我在项目中遇到过这样一个场景:做一个圆形进度条,宽高必须相等。这时候onMeasure就要特殊处理:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
// 取较小值,保证正方形
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}
嗯,这里要注意,上面这个写法其实有隐患。如果父容器给的是AT_MOST,直接拿size没问题。但如果父容器给的是EXACTLY,你这样强行改小,父容器可能不认。更稳妥的做法是结合resolveSize:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int size = Math.min(width, height);
int resolvedWidth = resolveSize(size, widthMeasureSpec);
int resolvedHeight = resolveSize(size, heightMeasureSpec);
// 再次取小,保证正方形
int finalSize = Math.min(resolvedWidth, resolvedHeight);
setMeasuredDimension(finalSize, finalSize);
}
我曾经就因为没处理好这个,在ConstraintLayout里用自定义View时,尺寸死活不对。后来加了个日志打印MeasureSpec,才发现是EXACTLY模式下被父容器限制了。
最后总结一下重写onMeasure的几个要点:
| 场景 | 做法 | 常见坑 |
|---|---|---|
| 简单自定义View | 计算内容尺寸 + resolveSize | 忘记处理wrap_content |
| 需要固定比例 | 根据宽或高计算另一个 | MeasureSpec模式冲突 |
| 自定义ViewGroup | 遍历子View + 累加尺寸 | 漏掉margin和padding |
| 需要精确控制 | 直接setMeasuredDimension | 超出父容器限制 |
记住,onMeasure的核心就是「协商」。父容器给你一个约束,你根据自己的内容,给出一个合理的尺寸。别自作主张,也别完全听命。找到那个平衡点,你的自定义View才能在各种布局里游刃有余。
公众号:蓝海资料掘金营,微信deep3321