
最近我在做一个车载智能设备的项目,这个小设备有个小的圆屏幕要显示一些 UI 元素。
很快,我发现问题来了。
在这个小圆屏上,几乎所有现有的标准 UI 组件都不能用。
Box 的方形布局在圆形屏幕上显得突兀,Column 和 Row 的线性排列在有限空间里也施展不开。最后只能大量依赖自定义控件,自己测量、自己布局、自己绘制。
这就引出了本篇文章的主题:Compose 到底是怎么决定"一个控件该画多大、放在哪里"的?
答案就是 MeasurePolicy。
它是 Compose 布局系统的骨架,掌握了它,你就能构建任意形状、任意逻辑的自定义布局------不管是圆形屏幕上的弧形排列,还是复杂的数据可视化图表。
渲染的三阶段
Jetpack Compose 采用了声明式的 UI 模型,整个渲染过程被拆分为三个阶段:
- 组合(Composition):决定显示哪些 UI(构建 UI 树)
- 布局(Layout):决定 UI 放在哪(测量和定位节点)
- 绘制(Drawing):决定怎么画(把像素画到屏幕上)
而 MeasurePolicy 就是布局阶段的核心。
无论是 Box、Column、Row 这些标准布局,还是你自己写的任何自定义布局,底层都在和 MeasurePolicy 打交道。
MeasurePolicy 是什么
简单来说,MeasurePolicy 就是一套"布局规则"。
它告诉 Compose:怎么测量我的子元素、我自己该占多大、把子元素放在哪里。
它是一个函数式接口,简化后的定义长这样:
kotlin
fun interface MeasurePolicy {
fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult
// ... 固有测量方法 ...
}
正因为它是 fun interface(函数式接口),你在调用 Layout 组合函数时,可以直接用一个 lambda 就把测量逻辑写出来,非常简洁。
measure :布局的核心
measure 函数是整个机制的核心,它接收两个参数:
- measurables: List<Measurable>:你所有的子元素。注意,这时候它们还只是"可被测量的对象",还没真正被测量过。
- constraints: Constraints:父布局给你的尺寸限制------你的最小/最大宽度和高度是多少。
单次测量
Jetpack Compose 有一个很关键的设计:单次测量(single-pass measurement)。
简单说就是:父布局对每个子元素只测量一次。
官方文档的原话是:
父布局和子布局之间没有测量协商:一旦子元素选定了自己的大小,父布局就必须正确处理。

这跟传统的 Android View 系统不一样。
在旧的 View 系统里,父 View 和子 View 可能会来回测量好几次,直到大家达成共识。这种"协商"虽然灵活,但性能开销很大。
Compose 选择了"只测一次",牺牲了一点灵活性,换来了更好的性能。
父子之间怎么交互
父子之间的交互过程如下图所示:

具体流程是这样的:
- 你调用
measurables[i].measure(constraints)来测量一个子元素; - 这时候,
Measurable就变成了Placeable; - 所有子元素都测量完之后,你把算出的总尺寸传给
layout(width, height) { ... },生成最终的MeasureResult; - 最后通过
placeable.placeRelative(x, y)把子元素放到指定位置。
这里有个关键的概念转换:
- Measurable:子元素被测量之前的状态------它"可以被测量",但还没测,不知道自己多大
- Placeable :子元素被测量之后的状态------它已经知道了自己精确的
width和height,随时可以被放到屏幕上
实战 ClockFaceLayout
理论讲完了,来点实际的。
我们来实现一个特别简单的布局 ------ ClockFaceLayout,该布局的子元素会像表盘上的数字一样排列。
Kotlin
/**
* 表盘布局 - 将子元素按照钟表的12个小时位置排列
* 适用于圆形屏幕或需要环形排列的场景
*/
@Composable
fun ClockFaceLayout(
modifier: Modifier = Modifier,
radiusFraction: Float = 0.4f, // 子元素距离中心的半径比例
content: @Composable () -> Unit,
) {
Layout(
modifier = modifier, content = content
) { measurables, constraints ->
// 1. 测量所有子元素 - 使用固定的子元素大小
val childSize = min(constraints.maxWidth, constraints.maxHeight) / 6
val childConstraints = Constraints.fixed(childSize, childSize)
val placeables = measurables.map { measurable ->
measurable.measure(childConstraints)
}
// 2. 布局大小等于父容器大小
val layoutSize = min(constraints.maxWidth, constraints.maxHeight)
val finalSize = layoutSize.coerceIn(constraints.minWidth, constraints.maxWidth)
// 3. 按照表盘位置放置子元素
layout(finalSize, finalSize) {
val centerX = finalSize / 2
val centerY = finalSize / 2
val radius = (finalSize * radiusFraction).roundToInt()
placeables.forEachIndexed { index, placeable ->
// 计算角度:12点方向是 -90 度(顶部),顺时针旋转
// 每个小时间隔 30 度 (360 / 12 = 30)
val angleInDegrees = (index * 30) - 90
val angleInRadians = Math.toRadians(angleInDegrees.toDouble())
// 计算子元素的中心点位置
val itemCenterX = centerX + (radius * cos(angleInRadians)).roundToInt()
val itemCenterY = centerY + (radius * sin(angleInRadians)).roundToInt()
// 转换为左上角坐标(placeRelative 需要的是左上角)
val x = itemCenterX - placeable.width / 2
val y = itemCenterY - placeable.height / 2
placeable.placeRelative(x = x, y = y)
}
}
}
}
这个 lambda 就是我们的 MeasurePolicy。逻辑很清晰,分三步走:测量 → 算大小 → 放置。效果如下图所示:

当然,我们还需要提供一些子元素:
Kotlin
ClockFaceLayout(
modifier = Modifier.fillMaxSize(), radiusFraction = 0.38f
) {
// 12 个小时位置
val hourLabels = remember {
listOf(
"12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"
)
}
val colors = remember {
listOf(
Color(0xFFE53935), // 12
Color(0xFFEF6C00), // 1
Color(0xFFFDD835), // 2
Color(0xFF43A047), // 3
Color(0xFF1E88E5), // 4
Color(0xFF8E24AA), // 5
Color(0xFFE53935), // 6
Color(0xFFEF6C00), // 7
Color(0xFFFDD835), // 8
Color(0xFF43A047), // 9
Color(0xFF1E88E5), // 10
Color(0xFF8E24AA), // 11
)
}
// 12 个子元素
hourLabels.forEachIndexed { index, label ->
Box(
modifier = Modifier
.size(44.dp)
.clip(CircleShape)
.background(colors[index]), contentAlignment = Alignment.Center
) {
Text(
text = label, color = Color.White, fontWeight = FontWeight.Bold, fontSize = 16.sp
)
}
}
}
先有鸡还是先有蛋
对绝大多数自定义布局来说,重写 measure 函数就够了。
但 Compose 的单次测量有个限制:父布局只能测一次子元素。
这带来一个问题 ------ 假如你有一个 Row,想让所有子元素的高度都等于最高的那个子元素的高度。问题是:你得先测一遍才知道谁最高,但你又不能测两遍。
这就陷入了一个"先有鸡还是先有蛋"的困境。
为了解决这类场景,MeasurePolicy 提供了固有测量(Intrinsic Measurement) 方法:
minIntrinsicWidth(最小固有宽度)minIntrinsicHeight(最小固有高度)maxIntrinsicWidth(最大固有宽度)maxIntrinsicHeight(最大固有高度)
这些方法的作用是:让你在不真正测量的情况下,问子元素"你大概需要多大?"。

比如 maxIntrinsicHeight 会告诉父布局:"如果没有任何限制,这个子元素最多需要多高?"
默认情况下,MeasurePolicy 会尝试通过标准 measure 方法来估算这些值。但对于那些动态计算大小的自定义布局,这种估算可能会出问题(卡顿甚至崩溃)。
我写过一篇文章 - 实战 Compose 中的 IntrinsicSize,如果你想详细了解 IntrinsicSize,可以看看。
这里我给一点实际开发中的建议:如果你写的自定义布局比较复杂,而且可能会被用在 IntrinsicSize 修饰符中(比如 Modifier.height(IntrinsicSize.Max)),那就应该显式重写这些方法,根据你的布局逻辑返回准确的理论值。
总结
回到我开头说的那个车载小圆屏项目。理解了 MeasurePolicy 之后,我发现很多事情变得清晰了:
标准布局不够用?自己写一个
Row、Column、Box 只是 Compose 给你的"开箱即用"的布局。当你需要特殊的排列方式(比如圆形布局、瀑布流、自定义网格),你完全可以自己实现一个 MeasurePolicy,逻辑完全由你控制。
性能很重要
Compose 的"单次测量"设计不是没有道理的。它避免了旧 View 系统里那种父子之间反复测量的开销。所以你在写自定义布局时,也要遵循这个原则------尽量一次性搞定,别来回测。
这在我那个小电子设备上尤其有用,毕竟那个设备只有 4G 内存,还是个 4 核机器。
固有测量是你的后手
当你遇到"需要在测量前就知道子元素大小"的场景,固有测量方法(minIntrinsicWidth、maxIntrinsicHeight 等)就是你的救命稻草。
从简单的开始
别一上来就想搞个完美的自定义布局系统。先从模仿 Column 或 Row 开始,把 measure → layout → placeRelative 这个流程跑通,然后再慢慢加功能。
下次需要写自定义布局时,记住这个套路:测量子元素 → 算出自己的大小 → 放置子元素。
就这么简单,剩下的都是细节,慢慢解决。