Android MeasureSpec 详解

151 阅读13分钟

MeasureSpec 是父容器向子 View 传递尺寸约束的唯一载体:一个 32 位 int,高 2 位是测量模式,低 30 位是测量尺寸。


1. 为什么需要 MeasureSpec

Android 的 View 是"父管子"的树形结构:子 View 知道自己内容需要多大,父容器知道这块地盘还剩多大。两者信息不对称,就需要一份协商协议——只传一个 int size 无法区分"硬性要求"和"上限",所以有了 MeasureSpec:尺寸 + 意图。

整条链路分四步:

① 父容器(比如 LinearLayout)
   │  用自己的 MeasureSpec + 子的 LayoutParams,算出子该遵守的 spec
   ▼
② 子 View.measure(childWidthSpec, childHeightSpec)
   │  measure() 内部回调 onMeasure(),子在这里算出「我要多宽多高」
   ▼
③ 子 setMeasuredDimension(w, h)
   │  把算出的结果分别写进 mMeasuredWidth 和 mMeasuredHeight 两个字段
   ▼
④ 父容器读回这两个值,在 onLayout() 里摆放子的位置(测量阶段至此结束)

一句话概括:父出规则 → 子做计算 → 父收结果。规则来自「父的 MeasureSpec + 子的 LayoutParams」,计算发生在 onMeasure(),结果被父容器用在 onLayout() 里。


2. 二进制结构:一个 int 装两样东西

MeasureSpec 本质上是对一个 int 的封装(Java 里是 View 的静态内部类,Kotlin 里可以理解为一个 value class)。

 31 30 | 29 .................................. 0
┌──────┬───────────────────────────────────────┐
│ Mode │              Size                     │
│ 2 bit│              30 bit                   │
└──────┴───────────────────────────────────────┘
   ↑ 高 2 位存模式            ↑ 低 30 位存尺寸

核心常量:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT; // 0xC0000000
​
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;   // 0x00000000
    public static final int EXACTLY     = 1 << MODE_SHIFT;   // 0x40000000
    public static final int AT_MOST     = 2 << MODE_SHIFT;   // 0x80000000
}

打包与解包:

// 打包:把 size 截断到 30 位,和 mode 按位或
public static int makeMeasureSpec(int size, int mode) {
    // 保证 size 不会污染高 2 位的 mode
    return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
​
// 解包
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}
​
public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);  // 即 0x3FFFFFFF,最大约 1G
}

手动构造 spec 的常见写法:

int spec = View.MeasureSpec.makeMeasureSpec(300, View.MeasureSpec.EXACTLY);
view.measure(spec, spec);

3. 三种测量模式

模式常量父容器的含义子 View 应该怎么做
EXACTLY1 << 30你的尺寸已经定了,就是 size直接使用 specSize,不要再争辩
AT_MOST2 << 30你最大不能超过 size取「内容需要的大小」与 specSize 的较小值
UNSPECIFIED0我不管你,你自己随意完全按内容需要返回(可滚动容器常用)

对应到 XML 属性,直觉上:

LayoutParams父为 EXACTLY父为 AT_MOST父为 UNSPECIFIED
match_parentEXACTLY(父可用空间)AT_MOST(父可用空间)UNSPECIFIED(0 或父可用空间)
wrap_contentAT_MOST(父可用空间)AT_MOST(父可用空间)UNSPECIFIED(0 或父可用空间)
100dpEXACTLY(100dp)EXACTLY(100dp)EXACTLY(100dp)

可以看出:具体数值永远变成 EXACTLY,这是最稳定的一条规则。

典型的 UNSPECIFIED 场景:

  • ScrollView 测量唯一子 View 的高度(只能滚一屏,所以高度不设上限)。
  • ListView / RecyclerView 测量 item 的高度。
  • 手动调用 view.measure(0, 0) 或 makeMeasureSpec(0, UNSPECIFIED) 做预测量。
  • PopupWindow 在 WRAP_CONTENT 下测量内容。

4. 核心 API 速查

// ---- 打包 / 解包 ----
View.MeasureSpec.makeMeasureSpec(int size, int mode)  // 构造一个 measuicSpec
View.MeasureSpec.getMode(int measureSpec)            // 取模式
View.MeasureSpec.getSize(int measureSpec)            // 取尺寸
View.MeasureSpec.toString(int measureSpec)           // 调试打印,如 "EXACTLY 1080"
​
// ---- 父容器用它给子 View 生成 spec ----
ViewGroup.getChildMeasureSpec(int spec, int padding, int childDimension)
​
// ---- 子 View 用它裁剪自己的测量结果 ----
View.getDefaultSize(int size, int measureSpec)
View.resolveSize(int size, int measureSpec)                    // 返回纯尺寸
View.resolveSizeAndState(int size, int ms, int childMeasuredState) // 带状态位
​
// ---- 子 View 上报结果 ----
View.setMeasuredDimension(int measuredWidth, int measuredHeight)
View.getMeasuredWidth() / getMeasuredHeight()
View.getMeasuredState()

5. MeasureSpec 从哪来:测量规则的向下传递

5.1 树顶:DecorView / ViewRootImpl

测量从根节点开始。ViewRootImpl.performMeasure() 会拿到窗口可用尺寸,然后调用:

private static int getRootMeasureSpec(int windowSize, int measurement) {
    int measureSpec;
    switch (measurement) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // 撑满窗口 → 硬性要求
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // 最多撑满窗口 → 上限
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // 具体数值 → 硬性要求
            measureSpec = MeasureSpec.makeMeasureSpec(measurement, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}

这就是"根 MeasureSpec 的生成规则",也是整个映射表的第一行原型:MATCH_PARENT → EXACTLY、WRAP_CONTENT → AT_MOST、固定值 → EXACTLY。

5.2 中间层:ViewGroup.getChildMeasureSpec

每一个 ViewGroup 在测量自己的孩子时,都要把「自己的 spec」和「孩子的 LayoutParams」合成「孩子的 spec」。这段逻辑被抽取成了公共静态方法:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
​
    // 可用空间 = 父给的尺寸 - 已占用的 padding / margin
    int size = Math.max(0, specSize - padding);
​
    int resultSize = 0;
    int resultMode = 0;
​
    switch (specMode) {
        // 父容器自己被强制固定大小
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                // 子 View 要具体数值,满足它
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 子 View 想和父一样大 → 正好和父一样大
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 子 View 想自己决定,但绝不能超过父
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
​
        // 父容器自己也有上限
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 想撑满,但父自己都没定下来 → 至少不能超过父的上限
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
​
        // 父容器让子 View 随便:模式一定还是 UNSPECIFIED,
        // size 那一格填 0 还是父的可用尺寸,要看框架内部的兼容开关
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

padding 参数传入的一般是:父的 padding + 子在父中的 margin + 已消耗的空间(如 LinearLayout 已排在前面的兄弟高度)。

6. 父 → 子 映射矩阵

把 5.2 的 switch 整理成表,就是测量体系的真值表,建议背下来:

父 Mode ↓ \ 子 LayoutParams →具体数值 100dpmatch_parentwrap_content
EXACTLYEXACTLY(100dp)EXACTLY(父可用)AT_MOST(父可用)
AT_MOSTEXACTLY(100dp)AT_MOST(父可用)AT_MOST(父可用)
UNSPECIFIEDEXACTLY(100dp)UNSPECIFIED(0 或父可用)UNSPECIFIED(0 或父可用)

三条记忆口诀:

  1. 具体数值永远 EXACTLY(三个格子一模一样)。
  2. wrap_content 在父有边界时永远是 AT_MOST(EXACTLY 和 AT_MOST 两行一样)。
  3. 父 UNSPECIFIED 时,match_parent 会退化成 UNSPECIFIED(约束链条断了);size 那格填 0 还是父的可用尺寸,由框架内部的开关决定。

7. measure() / onMeasure() / setMeasuredDimension() 调用链

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

    // 优化:spec 没变、也不需要强制布局时,直接复用上次结果
    boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
            || heightMeasureSpec != mOldHeightMeasureSpec;
    final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
            && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
    final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
            && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
    final boolean needsLayout = specChanged
            && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

    if (forceLayout || needsLayout) {
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        // ... 这里会调用 onMeasure 的协商逻辑
        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            // 命中测量缓存(本质是 LongSparseLongArray,key 是两个 spec 拼成的 long)
            long value = mMeasureCache.valueAt(cacheIndex);
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }

    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;

    // 把两个 spec 拼成一个 long 作为缓存 key
    mMeasureCache.put((((long) widthMeasureSpec << 32) | (heightMeasureSpec & 0xffffffffL)), ...);
}

三个必须记住的结论:

  1. measure() 是 final 的,不能重写;只能重写 onMeasure()。
  2. measure() 有缓存:key 是两个 spec 拼成的 long。如果 spec 没变且没有 PFLAG_FORCE_LAYOUT,onMeasure() 根本不会被调用。 → 这解释了一个经典困惑: "我手动调用两次 view.measure(),第二次没反应" 。因为 spec 相同,走了缓存。要么传不同的 spec,要么先 requestLayout()。
  3. 必须调用 setMeasuredDimension() ,否则抛 IllegalStateException: View with id ... onMeasure() did not set the measured dimension by calling setMeasuredDimension()。

setMeasuredDimension 内部会走 setMeasuredDimensionRaw:

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

注意 mMeasuredWidth 里除了尺寸还打包了 MEASURED_STATE_* 状态位(由 resolveSizeAndState 产生),所以:

// 取纯尺寸用 getMeasuredWidth()
// 取带状态的结果用 getMeasuredWidthAndState()

8. getDefaultSize 与 resolveSize:尺寸裁剪的核心

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

View.onMeasure() 的默认实现:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

这就是那个著名坑的来源:AT_MOST 时 getDefaultSize 直接返回 specSize,也就是"父给你多少你就占多少"。 所以一个不重写 onMeasure 的自定义 View,wrap_content 的表现和 match_parent 完全相同。 正确的做法是:min(specSize, 内容需要的大小)。

8.2 resolveSize / resolveSizeAndState —— 推荐用法

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL; // 打上"被挤小了"的标记
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}
  • resolveSize(size, spec) = resolveSizeAndState(size, spec, 0) & MEASURED_SIZE_MASK。
  • MEASURED_STATE_TOO_SMALL 只影响高 8 位的状态区,不影响 getMeasuredWidth() 返回值,但父容器(如 LinearLayout、TextView 的 ellipsize 判断)会读它。
  • childMeasuredState 用于把子 View 的状态沿着树向上传播,这样 View.mergeMeasuredState 类逻辑可以判断整条链路上是否有被压缩的节点。

9. 实战:自定义 View 的三种典型写法

9.1 标准模板(推荐)

class SquareTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        val width = measuredWidth
        val height = measuredHeight

        // 复用 super 的结果,按需修正
        val size = min(width, height)
        setMeasuredDimension(
            resolveSize(size, widthMeasureSpec),
            resolveSize(size, heightMeasureSpec)
        )
    }
}

9.2 严格按 MeasureSpec 三模式处理(最规范)

9.1 是"先让 super 测一遍,再修正结果",好处是省事;但它默认接受了 super 的判断,遇到 wrap_content 这类需要"按内容算"的场景就不够用。要完全掌控尺寸,就得自己把三个模式都写清楚。

下面把 9.1 的 SquareTextView 补成完整可用的类:内容尺寸由文字实际宽高决定(这样才能覆盖三种模式),最后再把渲染区域收敛成正方形。

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import kotlin.math.max
import kotlin.math.min

/**
 * 方形文字标签:内容尺寸由文字决定,最终渲染区域收敛成正方形。
 * 因为尺寸依赖内容,所以必须完整处理 EXACTLY / AT_MOST / UNSPECIFIED 三种模式。
 */
class SquareTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.DKGRAY
        textSize = sp(14f)
        textAlign = Paint.Align.CENTER
    }

    private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#11000000")
    }

    private val bgRect = RectF()

    /** 文字内容。改内容必须重新测量,所以要调 requestLayout() */
    var text: CharSequence = ""
        set(value) {
            if (field != value) {
                field = value
                requestLayout()
                invalidate()
            }
        }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val wMode = MeasureSpec.getMode(widthMeasureSpec)
        val wSize = MeasureSpec.getSize(widthMeasureSpec)
        val hMode = MeasureSpec.getMode(heightMeasureSpec)
        val hSize = MeasureSpec.getSize(heightMeasureSpec)

        // ① 按默认字号先测出文字宽高
        var size = sp(14f)
        val minSize = sp(8f)
        textPaint.textSize = size
        var textW = if (text.isEmpty()) 0f else textPaint.measureText(text.toString())
        var textH = if (text.isEmpty()) 0f else textPaint.fontMetrics.let { it.descent - it.ascent }

        // ② 若文字宽度大于高度,等比缩小字号让其能放进正方形
        if (text.isNotEmpty() && textW > textH && size > minSize) {
            size = max(minSize, size * (textH / textW))
            textPaint.textSize = size
            textW = textPaint.measureText(text.toString())
            textH = textPaint.fontMetrics.let { it.descent - it.ascent }

        }

        // ③ 计算加上 padding / suggestedMinimum 后的正方形边长
        val contentSide = max(textW.toInt(), textH.toInt())
        val sideWithPaddingW = contentSide + paddingLeft + paddingRight
        val sideWithPaddingH = contentSide + paddingTop + paddingBottom
        val desired = max(
            max(sideWithPaddingW, suggestedMinimumWidth),
            max(sideWithPaddingH, suggestedMinimumHeight)
        )

        // ④ 根据父约束收尾:EXACTLY 父说了算,AT_MOST 不能超,UNSPECIFIED 按内容
        val allowW = when (wMode) {
            MeasureSpec.EXACTLY -> wSize
            MeasureSpec.AT_MOST -> min(desired, wSize)
            else -> desired
        }
        val allowH = when (hMode) {
            MeasureSpec.EXACTLY -> hSize
            MeasureSpec.AT_MOST -> min(desired, hSize)
            else -> desired
        }

        // ⑤ 收敛成正方形:取两边的较小值
        val side = min(allowW, allowH)
        setMeasuredDimension(
            resolveSize(side, widthMeasureSpec),
            resolveSize(side, heightMeasureSpec)
        )
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        // 背景撑满整个测量区域(这里才用 getWidth/measuredWidth,且已在 layout 之后)
        bgRect.set(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat())
        canvas.drawRoundRect(bgRect, 8f, 8f, bgPaint)

        // 水平居中靠 textAlign = CENTER,垂直居中靠基线 = 中心 − (ascent + descent) / 2
        val fm = textPaint.fontMetrics
        val baseline = measuredHeight / 2f - (fm.ascent + fm.descent) / 2f
        canvas.drawText(text, 0, text.length, measuredWidth / 2f, baseline, textPaint)
    }

    private fun sp(value: Float): Float =
        TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP, value, resources.displayMetrics
        )
}

**为什么最后又要 `resolveSize` 一次?** `side` 取的是"两个方向里更小的那个",直接用它会丢掉 `EXACTLY` 的约束——父容器明确要求宽 1000、高 300 时,硬凑正方形反而违反了父的要求。再各自 `resolveSize` 一遍,`EXACTLY` 方向会还原成父给的尺寸,只有 `AT_MOST` / `UNSPECIFIED` 方向才收敛到 `side`。这与 9.1 末尾的处理方式一致。

用法:
<com.example.SquareTextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="12dp" />
squareTextView.text = "待审批"    


**四个必须做到的点**

| 要点                            | 说明                                                                                                             |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------- |
| ① `desired` 里要算全              | padding + 内容尺寸,还要和 `suggestedMinimumWidth/Height`(即 `minWidth` / `minHeight`)取 `max`,否则 `android:minWidth` 会失效 |
| ② `AT_MOST` 必须 `min()`        | 这是"上限"的语义所在。漏了这一步,`wrap_content` 就会变成吃满父容器                                                                     |
| ③ `UNSPECIFIED` 忽略 `size`     | 直接用内容尺寸。这里的 `size` 可能是 0,也可能是父容器给的提示值,都不该拿去当尺寸                                                                 |
| ④ 先 `setMeasuredDimension` 再读 | 在它之前读 `measuredWidth` 拿到的是上一轮的值                                                                                |

如果不想手写 `when`,`finalW` / `finalH` 两段可以等价替换成:

val finalW = resolveSize(desiredW, widthMeasureSpec) val finalH = resolveSize(desiredH, heightMeasureSpec)


`resolveSize` 内部就是同样的三分支(见 8.2 节),只是额外带了 `MEASURED_STATE_TOO_SMALL` 状态位。**手写的价值在于:每个分支的意图都写在注释里,出问题时一眼能看出哪条走错了。**

### 9.3 固定宽高比(如 16:9 的 Banner)

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val width = MeasureSpec.getSize(widthMeasureSpec) // 高度不参考父的 heightSpec,自己按比例算出来,然后强制 EXACTLY val height = (width * 9f / 16f).toInt() setMeasuredDimension( width, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) ) }


> 注意这里直接给 `setMeasuredDimension` 的第二个参数是 **spec 而不是尺寸**——这是 `setMeasuredDimension` 的一个陷阱:它接收的是**已经解析后的尺寸值**,但如果你传 spec,恰好也能跑(数值是 spec 的 int)。所以上面必须**显式构造 spec 拿到 `getSize`**,或者干脆传 `height`。规范写法:

val height = (width * 9f / 16f).toInt() val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) setMeasuredDimension(width, MeasureSpec.getSize(heightSpec))


### 9.4 强制测量:想知道"内容自然尺寸"

// 让 View 在无约束下测一次,得到它的"理想尺寸" val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) view.measure(spec, spec) val naturalWidth = view.measuredWidth


这个模式在「文字气泡自适应宽度」「给自定义容器算 item 大小时」非常常用。

* * *

## 10. 实战:自定义 ViewGroup

自定义 ViewGroup 的 `onMeasure` 标准骨架只有四步:

import android.content.Context import android.util.AttributeSet import android.view.View import android.view.ViewGroup import com.jzh.mvvm.R

/**

  • 流式布局(FlowLayout)

  • 支持特性:

    • 自动换行
    • 水平间距 itemSpacing
    • 垂直行间距 lineSpacing
    • 子 View margin
    • padding
    • wrap_content / match_parent */ class FlowLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : ViewGroup(context, attrs, defStyleAttr) {

    /** 同一行内子 View 之间的水平间距 */ var itemSpacing: Int = 0 private set

    /** 行与行之间的垂直间距 */ var lineSpacing: Int = 0 private set

    /** 缓存测量阶段得到的换行信息,供 onLayout 复用 */ private val lines = mutableListOf()

    private data class Line( val children: MutableList = mutableListOf(), var width: Int = 0, var height: Int = 0 )

    init { context.theme.obtainStyledAttributes( attrs, R.styleable.FlowLayout, defStyleAttr, 0 ).apply { try { itemSpacing = getDimensionPixelSize(R.styleable.FlowLayout_itemSpacing, 0) lineSpacing = getDimensionPixelSize(R.styleable.FlowLayout_lineSpacing, 0) } finally { recycle() } } }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { lines.clear()

     val widthMode = MeasureSpec.getMode(widthMeasureSpec)
     val widthSize = MeasureSpec.getSize(widthMeasureSpec)
     val availableWidth = if (widthMode == MeasureSpec.UNSPECIFIED) {
         Int.MAX_VALUE
     } else {
         widthSize - paddingLeft - paddingRight
     }
    
     var lineWidth = 0
     var lineHeight = 0
     var totalWidth = 0
     var totalHeight = 0
     var currentLine = Line()
    
     for (i in 0 until childCount) {
         val child = getChildAt(i)
         if (child.visibility == GONE) continue
    
         measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
         val lp = child.layoutParams as MarginLayoutParams
         val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
         val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin
    
         if (currentLine.children.isEmpty()) {
             currentLine.children.add(child)
             currentLine.width = childWidth
             currentLine.height = childHeight
             lineWidth = childWidth
             lineHeight = childHeight
         } else {
             val expectedWidth = lineWidth + itemSpacing + childWidth
             if (expectedWidth > availableWidth) {
                 // 当前行放不下,先结束当前行
                 lines.add(currentLine)
                 totalWidth = maxOf(totalWidth, lineWidth)
                 totalHeight += lineHeight
    
                 // 开启新行
                 currentLine = Line(mutableListOf(child), childWidth, childHeight)
                 lineWidth = childWidth
                 lineHeight = childHeight
             } else {
                 // 可以继续放在当前行
                 currentLine.children.add(child)
                 currentLine.width = expectedWidth
                 currentLine.height = maxOf(currentLine.height, childHeight)
                 lineWidth = expectedWidth
                 lineHeight = maxOf(lineHeight, childHeight)
             }
         }
     }
    
     if (currentLine.children.isNotEmpty()) {
         lines.add(currentLine)
         totalWidth = maxOf(totalWidth, lineWidth)
         totalHeight += lineHeight
     }
    
     val verticalExtra = if (lines.size > 1) (lines.size - 1) * lineSpacing else 0
     val desiredWidth = totalWidth + paddingLeft + paddingRight
     val desiredHeight = totalHeight + verticalExtra + paddingTop + paddingBottom
    
     val measuredWidth = when (widthMode) {
         MeasureSpec.EXACTLY -> widthSize
         MeasureSpec.UNSPECIFIED -> desiredWidth
         else -> minOf(desiredWidth, widthSize)
     }
     val measuredHeight = resolveSize(desiredHeight, heightMeasureSpec)
    
     setMeasuredDimension(measuredWidth, measuredHeight)
    

    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { var top = paddingTop for (line in lines) { var left = paddingLeft for (child in line.children) { val lp = child.layoutParams as MarginLayoutParams

             left += lp.leftMargin
    
             // 在当前行内垂直居中
             val verticalSpace = line.height - (child.measuredHeight + lp.topMargin + lp.bottomMargin)
             val childTop = top + lp.topMargin + verticalSpace / 2
    
             child.layout(
                 left,
                 childTop,
                 left + child.measuredWidth,
                 childTop + child.measuredHeight
             )
    
             left += child.measuredWidth + lp.rightMargin + itemSpacing
         }
         top += line.height + lineSpacing
     }
    

    }

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams { return MarginLayoutParams(context, attrs) }

    override fun generateDefaultLayoutParams(): LayoutParams { return MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) }

    override fun generateLayoutParams(p: LayoutParams): LayoutParams { return MarginLayoutParams(p) }

    override fun checkLayoutParams(p: LayoutParams): Boolean { return p is MarginLayoutParams } }


要点:

-   **永远用 `getChildMeasureSpec(...)`,不要自己拼 `makeMeasureSpec`**,否则 `match_parent` / `wrap_content` 会失效。
-   `padding` 参数要算全:父自己的 padding + 子的 margin + 已占用空间。
-   测量完必须在 `onLayout` 里用**同样的算法**摆位置,别在 `onLayout` 里重新算(会把测量缓存和布局结果搞得不一致)。
-   想支持 `wrap_content` 宽度,必须正确处理 `availableWidth = Int.MAX_VALUE` 这种情况,否则会拿到 `availableWidth = 0`。