20、对齐线

27 阅读2分钟

Jetpack Compose 中的对齐线

在 Jetpack Compose 布局模型中,对齐线(AlignmentLine)允许创建自定义对齐参照,供父布局用来对齐和定位其子项。例如,Row 可以使用子项的自定义对齐线来对齐子项。

使用现有对齐线

某些 Compose 可组合项已自带对齐线。例如,BasicText 可组合项会公开 FirstBaselineLastBaseline 对齐线。以下示例展示了如何创建一个自定义修饰符,向文本添加基于其第一条基线的内边距:

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
private fun TextWithPaddingToBaseline() {
    MaterialTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

注意:Compose 库已经提供了 paddingFrom 修饰符,允许指定相对于任何对齐线的内边距。

创建自定义对齐线

创建自定义 Layout 可组合项或 LayoutModifier 时,可以提供自定义对齐线,供父级可组合项用来对齐和定位子项。

以下示例展示了一个自定义 BarChart 可组合项,它公开了两条对齐线:MaxChartValueMinChartValue,使其他可组合项可以对齐到图表的最大和最小数据值。

首先,定义自定义对齐线:

/**
 * 由[BarChart]中最大数据值定义的对齐线
 */
private val MaxChartValue = HorizontalAlignmentLine(merger = { old, new -> min(old, new) })

/**
 * 由[BarChart]中最小数据值定义的对齐线
 */
private val MinChartValue = HorizontalAlignmentLine(merger = { old, new -> max(old, new) })

这些是 HorizontalAlignmentLine 类型,因为它们用于垂直对齐子项。合并策略考虑了 Compose 布局系统坐标(原点在左上角,正方向向下),因此最大值基线使用 min 策略,最小值基线使用 max 策略。

然后,在自定义布局中提供这些对齐线的值:

@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) {
            // ...
            // 计算基线
            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,
                        // 在这里设置自定义对齐线,这些对齐线会传播到直接和间接父可组合项
                        alignmentLines = mapOf(
                            MinChartValue to minYBaseline.roundToInt(),
                            MaxChartValue to maxYBaseline.roundToInt()
                        )
                    ) {}
                }
            }
        }
    }
}

使用自定义对齐线

父级布局可以使用子项提供的对齐线。以下示例创建了一个自定义布局,它将两个 Text 元素与 BarChart 的最大和最小数据值对齐:

@Composable
private fun BarChartMinMax(
    dataPoints: List<Int>,
    maxText: @Composable () -> Unit,
    minText: @Composable () -> Unit,
    modifier: Modifier = Modifier,
) {
    Layout(
        content = {
            maxText()
            minText()
            // 设置固定大小以便于示例理解
            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]
        
        // 从 BarChart 获取对齐线以定位文本
        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)
        )
    }
}