Administrator
发布于 2026-08-25 / 42 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 44 篇:Jetpack Compose 自定义布局与 Modifier 原理

上一篇聊了 Media3,这篇回到 UI 方向。Compose 的声明式 UI 看着简单,但做到复杂布局和自定义 Modifier 时,不理解底层原理就会卡住。这篇从 Layout 协议到 Modifier 链,拆解 Compose 布局的核心机制。

Compose 布局三阶段

Compose 的渲染流程分三个阶段:

  1. 组合(Composition):执行 @Composable 函数,生成 UI 树(节点树)

  2. 布局(Layout):测量每个节点的尺寸和位置

  3. 绘制(Draw):把节点画到屏幕上

每个阶段都有对应的性能优化手段。比如 remember 减少重组,deferRead 延迟状态读取减少布局,drawWithCache 减少绘制。

布局协议

Compose 的布局是一个"先测量子节点,再确定自身尺寸"的过程,类似 View 体系的 measure/layout,但约束传递方式不同。

核心接口是 Layout composable:

@Composable
fun MyCustomLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables, constraints ->
        // measurables: 子节点列表
        // constraints: 父节点传下来的约束(最小/最大宽高)

        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }

        // 计算自己的尺寸
        val width = placeables.maxOf { it.width }
        val height = placeables.sumOf { it.height }

        layout(width, height) {
            var y = 0
            placeables.forEach { placeable ->
                placeable.place(0, y)
                y += placeable.height
            }
        }
    }
}

这是 Compose 自定义布局的核心模板。measure 返回 Placeable,place 设置子节点位置。

自定义纵向布局示例

写一个带间距的纵向布局(Column 默认不支持 item 间距):

@Composable
fun SpacedColumn(
    spacing: Dp,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables, constraints ->
        val spacingPx = spacing.roundToPx()
        val placeables = measurables.map { it.measure(constraints) }

        val height = placeables.sumOf { it.height } + 
            spacingPx * (placeables.size - 1).coerceAtLeast(0)
        val width = placeables.maxOf { it.width }

        layout(width, height) {
            var y = 0
            placeables.forEach { placeable ->
                placeable.place(0, y)
                y += placeable.height + spacingPx
            }
        }
    }
}

自定义 Modifier

Modifier 是 Compose 的链式装饰器。每个 Modifier 节点负责一部分布局或绘制工作。

fun Modifier.customBackground(color: Color): Modifier = this.then(
    Modifier.drawBehind {
        drawRect(color)
    }
)

drawBehind 是绘制型 Modifier。还有布局型 Modifier 用 layout:

fun Modifier.firstBaselineToTop(distance: Dp): Modifier = this.then(
    Modifier.layout { measurable, constraints ->
        val placeable = measurable.measure(constraints)

        // 获取第一行文字的基线
        val firstBaseline = placeable[FirstBaseline]
        val distancePx = distance.roundToPx()
        val y = distancePx - firstBaseline

        layout(placeable.width, placeable.height + y) {
            placeable.place(0, y)
        }
    }
)

Modifier 链的执行顺序

Modifier 链的执行顺序是从外到内:

Box(
    modifier = Modifier
        .padding(16.dp)     // 1. 先执行 padding
        .background(Color.Red) // 2. 然后画背景
        .size(100.dp)        // 3. 再设尺寸
        .clip(CircleShape)   // 4. 最后裁剪
)

顺序不同,效果完全不同。padding 在 background 前面意味着背景画在 padding 区域外,反之背景画到 padding 区域内。

性能优化:避免不必要的测量

Compose 布局有"不重复测量同深度子节点"的规则(Intrinsic Measurements 除外)。这意味着自定义布局时,measure 的调用次数是可控的。

但如果你写了这样的布局:

// 反面教材:每个子节点测量两次
Layout(content) { measurables, constraints ->
    val firstPass = measurables.map { it.measure(constraints) }
    val maxWidth = firstPass.maxOf { it.width }
    val secondPass = measurables.map { 
        it.measure(constraints.copy(maxWidth = maxWidth)) 
    }
    // ...
}

两次测量会导致性能问题。解决方案是使用 Intrinsic Measurement:

Layout(content) { measurables, constraints ->
    // 先用 minIntrinsicWidth 获取理想宽度
    val idealWidth = measurables.maxOf { 
        it.minIntrinsicWidth(constraints.maxHeight) 
    }
    val placeables = measurables.map { 
        it.measure(constraints.copy(maxWidth = idealWidth)) 
    }
    // 只测量一次
}

SubcomposeLayout

当布局需要根据子节点的尺寸决定显示哪些子节点时(比如 LazyColumn 只渲染可见项),用 SubcomposeLayout:

@Composable
fun SimpleLazyList(content: @Composable () -> Unit) {
    SubcomposeLayout { constraints ->
        // 可以在布局过程中动态创建子节点
        val items = subcompose("items", content)
        items.forEach { it.measure(constraints) }
        layout(constraints.maxWidth, constraints.maxHeight) {
            // 放置
        }
    }
}

LazyColumn、LazyRow 的底层就是 SubcomposeLayout。但它的开销比普通 Layout 大,只在需要动态子节点组合时用。

性能分析工具

  1. Layout Inspector:Android Studio 内置,查看 Compose 树结构和重组次数

  2. Compose Compiler Metrics:在 gradle.properties 中开启:

composeCompilerReports=true
composeCompilerMetrics=true

生成的报告里可以看到每个 Composable 是否稳定(stable)、是否可跳过(skippable)。不稳定或不可跳过的函数会触发不必要的重组。

  1. Macrobenchmark:量化布局性能变化

小结

Compose 自定义布局的核心是理解 Layout 协议(measure → layout → place)。Modifier 链的顺序决定视觉效果,从外到内执行。性能优化重点是减少不必要的测量和重组,用 Intrinsic Measurement 替代多次 measure 调用。复杂场景考虑 SubcomposeLayout。

下一篇聊 ViewBinding 与 DataBinding 的取舍。


评论