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)做预测量。 PopupWindow在WRAP_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 → EXACTLY、WRAP_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 或父可用) |
三条记忆口诀:
- 具体数值永远 EXACTLY(三个格子一模一样)。
wrap_content在父有边界时永远是 AT_MOST(EXACTLY 和 AT_MOST 两行一样)。- 父 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)), ...);
}
三个必须记住的结论:
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:
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_MOST时getDefaultSize直接返回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()返回值 ,但父容器(如LinearLayout、TextView的 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 拿到的是上一轮的值 |
如果不想手写 when,finalW / 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。