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)
    );
}

说白了,它调了两个关键方法:getDefaultSizegetSuggestedMinimumWidth/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。这意味着什么?

⚠️ 坑来了: 如果你直接继承View而不重写onMeasure,在wrap_content模式下,View会表现得和match_parent一样——铺满父容器。我当年第一次写自定义View时就掉进这个坑,折腾了半天才发现是默认实现的问题。

为什么会这样?因为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。

我画了一张图,帮你理清这个决策逻辑:

getChildMeasureSpec 决策流程 父容器 MeasureSpec 子View LayoutParams lp.width/height = 固定值 (如100dp) MATCH_PARENT = -1 WRAP_CONTENT = -2 EXACTLY size = 固定值 取决于父容器模式 EXACTLY/AT_MOST AT_MOST size = 父容器剩余空间 最终生成子View的 MeasureSpec (mode + size)

你看,子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,核心就两件事:

  1. 计算自身内容需要多大尺寸
  2. 根据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;
}
💡 小技巧: Android框架其实已经提供了 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