18、自定义布局

5 阅读3分钟

Compose 中的自定义布局

在 Compose 中,界面元素由可组合函数表示,每个元素在界面树中有特定位置和尺寸。布局过程分为三个步骤:测量所有子项、确定自身尺寸、放置子项。

使用布局修饰符

可以使用 layout 修饰符自定义单个元素的测量和布局方式:

fun Modifier.customLayoutModifier() = layout { measurable, constraints ->
    // 自定义测量和布局逻辑
}

示例:实现 paddingFromBaseline 修饰符

以下代码实现了一个控制从顶部到第一行文本基线距离的修饰符:

fun Modifier.firstBaselineToTop(firstBaselineToTop: Dp) = layout { measurable, constraints ->
    // 测量可组合项
    val placeable = measurable.measure(constraints)
    
    // 检查可组合项是否有第一条基线
    check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
    val firstBaseline = placeable[FirstBaseline]
    
    // 计算可组合项带内边距的高度
    val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
    val height = placeable.height + placeableY
    
    layout(placeable.width, height) {
        // 放置可组合项
        placeable.placeRelative(0, placeableY)
    }
}

验证代码是否正常工作:

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
    MyApplicationTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

@Preview
@Composable
fun TextWithNormalPaddingPreview() {
    MyApplicationTheme {
        Text("Hi there!", Modifier.padding(top = 32.dp))
    }
}

创建自定义布局

若要测量和布置多个可组合项,使用 Layout 可组合项:

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // 测量和定位子项的逻辑
    }
}

实现自定义 Column

以下代码实现了一个基本的垂直布局容器:

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // 测量所有子项
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }
        
        // 设置布局大小
        layout(constraints.maxWidth, constraints.maxHeight) {
            // 跟踪已放置子项的 y 坐标
            var yPosition = 0
            
            // 放置子项
            placeables.forEach { placeable ->
                placeable.placeRelative(x = 0, y = yPosition)
                yPosition += placeable.height
            }
        }
    }
}

使用自定义布局:

@Composable
fun CallingComposable(modifier: Modifier = Modifier) {
    MyBasicColumn(modifier.padding(8.dp)) {
        Text("MyBasicColumn")
        Text("places items")
        Text("vertically.")
        Text("We've done it by hand!")
    }
}

布局方向

可以通过修改 LocalLayoutDirection CompositionLocal 更改布局方向。在手动放置可组合项时,LayoutDirectionlayout 修饰符或 Layout 可组合项的 LayoutScope 的一部分。

使用 place 而非 placeRelative 可以避免根据布局方向(从左到右或从右到左)自动调整位置:

// 根据布局方向自动调整
placeable.placeRelative(x = 0, y = yPosition)

// 不根据布局方向调整
placeable.place(x = 0, y = yPosition)

重要注意事项

  1. 单次测量限制:Compose 不允许多次测量同一子项。布局元素不能为了尝试不同配置而多次测量任何子元素。

  2. 作用域限制:只能在测量和布局传递期间测量布局,且只能在布局传递期间(测量后)放置子项。这些限制由 Compose 作用域(如 MeasureScopePlacementScope)在编译时强制执行。

  3. 与 View 系统对比:在 View 系统中,创建自定义布局需要扩展 ViewGroup 并实现测量和布局函数。而在 Compose 中,只需使用 Layout 可组合项编写一个函数。

实际应用

要深入了解布局和修饰符,可以参考:

Compose 布局系统提供了灵活而强大的方式来创建自定义 UI 组件,同时保持了代码的简洁性和可维护性。