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],左上角以及 x 轴和 y 轴的正方向都是向下的,因此 MaxChartValue 值将始终小于 MinChartValue。因此,对于最大图表数据值基线,合并策略为 min;对于最小图表数据值基线,合并策略为 max

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

@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)
        )
    }
}