Jetpack Compose 中的对齐线

Compose 布局模型允许您使用AlignmentLine创建自定义对齐线,父布局可以使用这些对齐线来对齐和定位其子元素。例如,Row 可以使用其子元素的自定义对齐线来对齐它们。

当布局为特定的AlignmentLine提供值时,布局的父布局可以在测量后使用相应的Placeable实例上的Placeable.get运算符读取此值。根据AlignmentLine的位置,父布局可以决定子元素的位置。

Compose 中的一些可组合项已经自带对齐线。例如,BasicText 可组合项公开了FirstBaselineLastBaseline对齐线。

在下面的示例中,一个名为firstBaselineToTop的自定义LayoutModifier读取FirstBaseline,以从其第一条基线开始向Text添加填充。

图 1. 显示了向元素添加普通填充与向文本元素的基线应用填充之间的区别。

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp,
) = layout { measurable, constraints ->
    // Measure the composable
    val placeable = measurable.measure(constraints)

    // Check the composable has a first baseline
    check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
    val firstBaseline = placeable[FirstBaseline]

    // Height of the composable with padding - first baseline
    val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
    val height = placeable.height + placeableY
    layout(placeable.width, height) {
        // Where the composable gets placed
        placeable.placeRelative(0, placeableY)
    }
}

@Preview
@Composable
private fun TextWithPaddingToBaseline() {
    MaterialTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

为了在示例中读取FirstBaseline,在测量阶段使用了placeable [FirstBaseline]

创建自定义对齐线

在创建自定义Layout可组合项或自定义LayoutModifier时,您可以提供自定义对齐线,以便其他父可组合项可以使用它们来相应地对齐和定位其子元素。

以下示例显示了一个自定义BarChart可组合项,它公开了两条对齐线,MaxChartValueMinChartValue,以便其他可组合项可以对齐到图表的最大和最小数据值。两个文本元素“Max”和“Min”已对齐到自定义对齐线的中心。

图 2. BarChart可组合项,文本对齐到最大和最小数据值。

自定义对齐线在项目中定义为顶级变量。

/**
 * AlignmentLine defined by the maximum data value in a [BarChart]
 */
private val MaxChartValue = HorizontalAlignmentLine(merger = { old, new ->
    min(old, new)
})

/**
 * AlignmentLine defined by the minimum data value in a [BarChart]
 */
private val MinChartValue = HorizontalAlignmentLine(merger = { old, new ->
    max(old, new)
})

用于创建示例的自定义对齐线类型为HorizontalAlignmentLine,因为它们用于垂直对齐子元素。如果多个布局为这些对齐线提供值,则会将合并策略作为参数传递。由于 Compose 布局系统坐标和Canvas坐标表示[0, 0](左上角)并且xy轴为正向下,因此MaxChartValue值将始终小于MinChartValue。因此,最大图表数据值基线的合并策略为min,最小图表数据值基线的合并策略为max

在创建自定义LayoutLayoutModifier时,请在MeasureScope.layout方法中指定自定义对齐线,该方法采用alignmentLines: Map<AlignmentLine, Int>参数。

@Composable
private fun BarChart(
    dataPoints: List<Int>,
    modifier: Modifier = Modifier,
) {
    val maxValue: Float = remember(dataPoints) { dataPoints.maxOrNull()!! * 1.2f }

    BoxWithConstraints(modifier = modifier) {
        val density = LocalDensity.current
        with(density) {
            // ...
            // Calculate baselines
            val maxYBaseline = // ...
            val minYBaseline = // ...
            Layout(
                content = {},
                modifier = Modifier.drawBehind {
                    // ...
                }
            ) { _, constraints ->
                with(constraints) {
                    layout(
                        width = if (hasBoundedWidth) maxWidth else minWidth,
                        height = if (hasBoundedHeight) maxHeight else minHeight,
                        // Custom AlignmentLines are set here. These are propagated
                        // to direct and indirect parent composables.
                        alignmentLines = mapOf(
                            MinChartValue to minYBaseline.roundToInt(),
                            MaxChartValue to maxYBaseline.roundToInt()
                        )
                    ) {}
                }
            }
        }
    }
}

此可组合项的直接和间接父级可以使用对齐线。以下可组合项创建一个自定义布局,该布局将两个Text插槽和数据点作为参数,并将这两个文本对齐到最大和最小图表数据值。此可组合项的预览如图 2 所示。

@Composable
private fun BarChartMinMax(
    dataPoints: List<Int>,
    maxText: @Composable () -> Unit,
    minText: @Composable () -> Unit,
    modifier: Modifier = Modifier,
) {
    Layout(
        content = {
            maxText()
            minText()
            // Set a fixed size to make the example easier to follow
            BarChart(dataPoints, Modifier.size(200.dp))
        },
        modifier = modifier
    ) { measurables, constraints ->
        check(measurables.size == 3)
        val placeables = measurables.map {
            it.measure(constraints.copy(minWidth = 0, minHeight = 0))
        }

        val maxTextPlaceable = placeables[0]
        val minTextPlaceable = placeables[1]
        val barChartPlaceable = placeables[2]

        // Obtain the alignment lines from BarChart to position the Text
        val minValueBaseline = barChartPlaceable[MinChartValue]
        val maxValueBaseline = barChartPlaceable[MaxChartValue]
        layout(constraints.maxWidth, constraints.maxHeight) {
            maxTextPlaceable.placeRelative(
                x = 0,
                y = maxValueBaseline - (maxTextPlaceable.height / 2)
            )
            minTextPlaceable.placeRelative(
                x = 0,
                y = minValueBaseline - (minTextPlaceable.height / 2)
            )
            barChartPlaceable.placeRelative(
                x = max(maxTextPlaceable.width, minTextPlaceable.width) + 20,
                y = 0
            )
        }
    }
}
@Preview
@Composable
private fun ChartDataPreview() {
    MaterialTheme {
        BarChartMinMax(
            dataPoints = listOf(4, 24, 15),
            maxText = { Text("Max") },
            minText = { Text("Min") },
            modifier = Modifier.padding(24.dp)
        )
    }
}