Android MeasureSpec 详解

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


1. 为什么需要 MeasureSpec

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

整条链路分四步:

scss 复制代码
① 父容器(比如 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)。

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

核心常量:

arduino 复制代码
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
}

打包与解包:

arduino 复制代码
// 打包:把 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 的常见写法:

ini 复制代码
int spec = View.MeasureSpec.makeMeasureSpec(300, View.MeasureSpec.EXACTLY);
view.measure(spec, spec);

3. 三种测量模式

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

对应到 XML 属性,直觉上:

LayoutParams 父为 EXACTLY 父为 AT_MOST 父为 UNSPECIFIED
match_parent EXACTLY(父可用空间) AT_MOST(父可用空间) UNSPECIFIED(0 或父可用空间)
wrap_content AT_MOST(父可用空间) AT_MOST(父可用空间) UNSPECIFIED(0 或父可用空间)
100dp EXACTLY(100dp) EXACTLY(100dp) EXACTLY(100dp)

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

典型的 UNSPECIFIED 场景:

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

4. 核心 API 速查

scss 复制代码
// ---- 打包 / 解包 ----
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() 会拿到窗口可用尺寸,然后调用:

arduino 复制代码
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 → EXACTLYWRAP_CONTENT → AT_MOST固定值 → EXACTLY

5.2 中间层:ViewGroup.getChildMeasureSpec

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

ini 复制代码
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 → 具体数值 100dp match_parent wrap_content
EXACTLY EXACTLY(100dp) EXACTLY(父可用) AT_MOST(父可用)
AT_MOST EXACTLY(100dp) AT_MOST(父可用) AT_MOST(父可用)
UNSPECIFIED EXACTLY(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() 调用链

ini 复制代码
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_LAYOUTonMeasure() 根本不会被调用。 → 这解释了一个经典困惑: "我手动调用两次 view.measure(),第二次没反应" 。因为 spec 相同,走了缓存。要么传不同的 spec,要么先 requestLayout()
  3. 必须调用 setMeasuredDimension() ,否则抛 IllegalStateException: View with id ... onMeasure() did not set the measured dimension by calling setMeasuredDimension()

setMeasuredDimension 内部会走 setMeasuredDimensionRaw

ini 复制代码
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

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

arduino 复制代码
// 取纯尺寸用 getMeasuredWidth()
// 取带状态的结果用 getMeasuredWidthAndState()

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

8.1 getDefaultSize ------ 最"偷懒"但也最常用的取法

arduino 复制代码
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() 的默认实现:

scss 复制代码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

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

8.2 resolveSize / resolveSizeAndState ------ 推荐用法

arduino 复制代码
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() 返回值 ,但父容器(如 LinearLayoutTextView 的 ellipsize 判断)会读它。
  • childMeasuredState 用于把子 View 的状态沿着树向上传播 ,这样 View.mergeMeasuredState 类逻辑可以判断整条链路上是否有被压缩的节点。

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

9.1 标准模板(推荐)

kotlin 复制代码
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 补成完整可用的类:内容尺寸由文字实际宽高决定(这样才能覆盖三种模式),最后再把渲染区域收敛成正方形。

scss 复制代码
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.ceil
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()
            }
        }

    /** 文字本身需要的宽度(不含 padding) */
    private val contentWidth: Int
        get() = if (text.isEmpty()) 0
        else ceil(textPaint.measureText(text.toString())).toInt()

    /** 文字本身需要的高度(不含 padding)------用 fontMetrics 量,比 textSize 准 */
    private val contentHeight: Int
        get() = if (text.isEmpty()) 0
        else {
            val fm = textPaint.fontMetrics
            ceil((fm.descent - fm.ascent).toDouble()).toInt()
        }

    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)

        // ① 先算「内容希望多大」:文字尺寸 + 自身 padding,并兜住 minWidth / minHeight
        val desiredW = max(
            paddingLeft + paddingRight + contentWidth,
            suggestedMinimumWidth
        )
        val desiredH = max(
            paddingTop + paddingBottom + contentHeight,
            suggestedMinimumHeight
        )

        // ② 三种模式分别处理,得到各方向允许的尺寸
        val allowW = when (wMode) {
            MeasureSpec.EXACTLY -> wSize                    // 父说了算,照用
            MeasureSpec.AT_MOST -> min(desiredW, wSize)     // 是上限,不能超
            else -> desiredW                                // UNSPECIFIED:忽略 size,按内容
        }
        val allowH = when (hMode) {
            MeasureSpec.EXACTLY -> hSize
            MeasureSpec.AT_MOST -> min(desiredH, hSize)
            else -> desiredH
        }

        // ③ 收敛成正方形:取两边的较小值,再各自回落一次以尊重父约束
        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 末尾的处理方式一致。

用法:

ini 复制代码
<com.example.SquareTextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="12dp" />
ini 复制代码
squareTextView.text = "待审批"

四个必须做到的点

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

如果不想手写 whenfinalW / finalH 两段可以等价替换成:

ini 复制代码
val finalW = resolveSize(desiredW, widthMeasureSpec)
val finalH = resolveSize(desiredH, heightMeasureSpec)

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

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

kotlin 复制代码
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。规范写法:

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

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

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

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


10. 实战:自定义 ViewGroup

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

kotlin 复制代码
class FlowLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // ① 先算出自己的可用宽度
        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 totalHeight = 0
        var lineHeight = 0

        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (child.visibility == GONE) continue

            // ② 给每个子 View 生成 spec 并测量
            val lp = child.layoutParams
            val childSpec = getChildMeasureSpec(
                widthMeasureSpec,
                paddingLeft + paddingRight,
                lp.width
            )
            val childHeightSpec = getChildMeasureSpec(
                heightMeasureSpec,
                paddingTop + paddingBottom,
                lp.height
            )
            child.measure(childSpec, childHeightSpec)

            val cw = child.measuredWidth
            val ch = child.measuredHeight

            if (lineWidth + cw > availableWidth) {
                // 换行
                totalHeight += lineHeight
                lineWidth = 0
                lineHeight = 0
            }
            lineWidth += cw
            lineHeight = max(lineHeight, ch)
        }
        totalHeight += lineHeight

        // ③ 上报自己的尺寸
        setMeasuredDimension(
            resolveSize(widthSize, widthMeasureSpec),
            resolveSize(totalHeight + paddingTop + paddingBottom, heightMeasureSpec)
        )
        // ④ 缓存换行结果,onLayout 里复用(略)
    }
}

要点:

  • 永远用 getChildMeasureSpec(...),不要自己拼 makeMeasureSpec ,否则 match_parent / wrap_content 会失效。
  • padding 参数要算全:父自己的 padding + 子的 margin + 已占用空间。
  • 测量完必须在 onLayout 里用同样的算法 摆位置,别在 onLayout 里重新算(会把测量缓存和布局结果搞得不一致)。
  • 想支持 wrap_content 宽度,必须正确处理 availableWidth = Int.MAX_VALUE 这种情况,否则会拿到 availableWidth = 0
相关推荐
又见情义2 小时前
RK3568 Android 13 本地 U 盘 OTA 升级实战:基于 RKUpdateService 的完整流程
android
阳光宅男@李光熠2 小时前
【电子通识】一起学习TDK的EMC基础——电池兼容设计方法概述
java·前端·数据库
张宜强2 小时前
025 安居租赁 · 房屋租赁平台
java·课程设计
Java_2017_csdn3 小时前
ReentrantLock 与 Redis RLock 对比解读
java
YDS8293 小时前
Small Spring IOC篇:实现 Bean 的定义、注册、获取
java·spring
边境悍匪3 小时前
蜗牛学苑 Java 智能体学习 Day45|Knife4j+SpringBoot3、书城项目整合 MyBatis 思维导图复盘
java·开发语言·vue.js·学习·spring
Godikov3 小时前
Android10后台弹窗与APK自动更新
android
需要8263 小时前
Arthas 一个命令排查线上问题
java·jvm·spring·spring cloud
ai2work3 小时前
ch11 持久化:DataStore + Gson 多会话
kotlin