欢迎光临
我们一直在努力

Android MeasureSpec 详解

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 应该怎么做
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) 做预测量。
  • 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
EXACTLY EXACTLY(100dp) EXACTLY(父可用) AT_MOST(父可用)
AT_MOST EXACTLY(100dp) AT_MOST(父可用) AT_MOST(父可用)
UNSPECIFIED EXACTLY(100dp) UNSPECIFIED(0 或父可用) UNSPECIFIED(0 或父可用)

三条记忆口诀:

  • 具体数值永远 EXACTLY(三个格子一模一样)。
  • wrap_content 在父有边界时永远是 AT_MOST(EXACTLY 和 AT_MOST 两行一样)。
  • 父 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)), ...);
    }

    三个必须记住的结论:

  • measure() 是 final 的,不能重写;只能重写 onMeasure()。
  • measure() 有缓存:key 是两个 spec 拼成的 long。如果 spec 没变且没有 PFLAG_FORCE_LAYOUT,onMeasure() 根本不会被调用。
    → 这解释了一个经典困惑:“我手动调用两次 view.measure(),第二次没反应”。因为 spec 相同,走了缓存。要么传不同的 spec,要么先 requestLayout()。
  • 必须调用 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.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 末尾的处理方式一致。

    用法:

    <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 标准骨架只有四步:

    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。
    赞(0)
    未经允许不得转载:171主机测评 » Android MeasureSpec 详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址