compose measurePoliy 笔记

51 阅读4分钟

measurePolicy 是 Compose 布局系统的核心,它定义了布局的“测量与摆放”规则。你可以把它理解为一个布局算法的说明书,它回答了三个关键问题:

  1. 如何测量每个子项?
  2. 布局自身应该多大?
  3. 如何摆放每个子项?

它通常与 Layout 可组合项或 Modifier.layout 修饰符配合使用。

🎯 核心概念与流程

measurePolicy 的工作流程围绕 MeasureScope.measure 方法展开,其输入是约束(Constraints)和子项列表(List<Measurable>,代表所有子组件),输出是布局结果(MeasureResult)。

image.png

📝 基础代码模板

理解上图后,我们通过一个最简单的竖向排列并居中布局的代码来理解其实现:

@Composable
fun VerticalCenteredLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    // 使用 Layout 可组合项,传入内容和自定义的 measurePolicy
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints -> // 这里就是 measurePolicy 的 lambda
        // 1. 测量所有子项(不受约束,用最宽松的)
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
        }

        // 2. 计算自身尺寸:宽度取子项最大宽度,高度为子项高度之和
        val width = placeables.maxOfOrNull { it.width } ?: 0
        val height = placeables.sumOf { it.height }

        // 3. 布局:摆放每个子项,并返回最终布局结果
        layout(width, height) {
            var y = 0
            placeables.forEach { placeable ->
                // 水平居中摆放
                val x = (width - placeable.width) / 2
                placeable.placeRelative(x = x, y = y)
                y += placeable.height // 垂直位置累加
            }
        }
    }
}

// 使用
VerticalCenteredLayout(Modifier.fillMaxSize()) {
    Text("First")
    Text("Second with longer text")
    Button(onClick = {}) { Text("Button") }
}

🛠️ 两种主要使用方式

1. 通过 Layout 可组合项创建自定义布局

如上例所示,这是创建全新布局组件的主要方式。Layout 的最后一个参数 measurePolicy 正是你的布局算法。

2. 通过 Modifier.layout 调整单个组件的测量布局

这种方式只影响被修饰的组件本身及其直接子项(如果它有的话,如 Box),不会创建全新的布局组件。常用于微调。

@Composable
fun PaddingFromBaseline(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables, constraints ->
        val placeable = measurables.first().measure(constraints)
        // 假设从基线向上有 50.dp 的固定内边距要求
        val firstBaseline = placeable[FirstBaseline]
        val padding = if (firstBaseline != AlignmentLine.Unspecified) {
            (50.dp.roundToPx() - firstBaseline).coerceAtLeast(0)
        } else {
            0
        }
        layout(placeable.width, placeable.height + padding) {
            placeable.placeRelative(0, padding)
        }
    }
}

⚙️ 关键技巧与深度解析

1. 理解并使用 Constraints

Constraints 是父布局传递给子项的限制条件,它定义了子项尺寸的允许范围:

  • minWidth / maxWidth
  • minHeight / maxHeight 你可以根据布局逻辑,为每个子项创建新的约束。

示例:让子项占自身宽度的一半

measurable.measure(
    constraints.copy(
        maxWidth = constraints.maxWidth / 2,
        minWidth = constraints.minWidth / 2
    )
)

2. 处理多子项与测量顺序

对于多个子项,你可能需要多次测量或使用 IntrinsicMeasurable 进行固有特性测量。

固有特性测量示例(实现一个所有子项宽度均分的布局):

Layout(content = content) { measurables, constraints ->
    val itemWidth = constraints.maxWidth / measurables.size
    val itemConstraints = constraints.copy(minWidth = itemWidth, maxWidth = itemWidth)
    val placeables = measurables.map { it.measure(itemConstraints) }
    val height = placeables.maxOfOrNull { it.height } ?: constraints.minHeight
    layout(constraints.maxWidth, height) {
        var x = 0
        placeables.forEach { placeable ->
            placeable.placeRelative(x = x, y = 0)
            x += itemWidth
        }
    }
}

3. 使用 Placeable 的扩展能力

Placeable 不仅有宽高,还提供:

  • 对齐线placeable[FirstBaseline] 获取文字的基线。
  • 多层放置placeable.placeWithLayer 支持硬件加速层。
  • 自定义放置:在 layout 块中,你可以完全自由地决定每个子项的位置。

💎 最佳实践与常见误区

最佳实践说明
尽可能只测量一次多次测量(measurable.measure)是昂贵的,可能导致性能问题。
正确传递约束不要随意忽略父级约束(如 minWidth),这可能导致布局在特定情况下崩溃。
使用 layoutId 标识子项在复杂布局中,通过 Modifier.layoutId 标记子项,便于在 measurePolicy 中识别和处理。
为自定义布局提供对齐线通过 AlignmentLine 让外部布局能对齐你的自定义布局内部的特定位置(如文本基线)。

常见误区

// ❌ 错误:忽略了父级的最小高度约束
val placeable = measurable.measure(Constraints.fixedWidth(100))

// ✅ 正确:在固定宽度的同时,尊重高度约束
val placeable = measurable.measure(
    Constraints(
        minWidth = 100,
        maxWidth = 100,
        minHeight = constraints.minHeight, // 传递父级约束
        maxHeight = constraints.maxHeight
    )
)

🆚 与传统 View 系统的对比

传统 View 系统Compose (measurePolicy)
继承 ViewGroup,重写 onMeasureonLayout实现 MeasurePolicy 接口或使用 lambda
使用 MeasureSpec 表示约束使用 Constraints 对象,概念更统一
通过 child.measurechild.layout 操作通过 measurable.measureplaceable.placeRelative
getChildAt 按添加顺序访问通过 measurables 列表访问,顺序由组合决定

总结measurePolicy 是 Compose 布局系统的“算法心脏”。掌握它,你就能创造出完全符合设计需求的任意布局。从简单的堆叠布局到复杂的瀑布流、环形菜单,都基于这一套统一的测量-摆放模型。