Căn chỉnh dòng trong Jetpack Compose

Mô hình bố cục trong Compose cho phép bạn sử dụng AlignmentLine để tạo các dòng căn chỉnh tuỳ chỉnh có thể được bố cục mẹ sử dụng để căn chỉnh và đặt vị trí thành phần con cháu. Ví dụ: Row có thể dùng các dòng căn chỉnh tuỳ chỉnh của thành phần con cháu để căn chỉnh.

Khi bố cục cung cấp một giá trị cho một AlignmentLine cụ thể, thành phần mẹ của bố cục có thể đọc giá trị này sau khi đo lường, sử dụng toán tử Placeable.get trên thực thể Placeable tương ứng. Sau đó, dựa vào vị trí của AlignmentLine, thành phần mẹ có thể quyết định vị trí của thành phần con cháu.

Một số thành phần kết hợp trong Compose đã có dòng căn chỉnh. Ví dụ: thành phần kết hợp BasicText hiển thị dòng căn chỉnh FirstBaselineLastBaseline.

Trong ví dụ bên dưới, LayoutModifier tuỳ chỉnh có tên firstBaselineToTop đọc FirstBaseline để thêm khoảng đệm vào Text bắt đầu từ đường cơ sở đầu tiên.

Hình 1. Thể hiện sự khác biệt giữa việc thêm khoảng đệm thông thường vào một thành phần và áp dụng khoảng đệm vào đường cơ sở của thành phần văn bản.

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

Để đọc FirstBaseline trong ví dụ, placeable [FirstBaseline] được sử dụng trong giai đoạn đo lường.

Tạo đường căn chỉnh tuỳ chỉnh

Khi tạo một thành phần kết hợp Layout tuỳ chỉnh hoặc một LayoutModifier tuỳ chỉnh, bạn có thể cung cấp dòng căn chỉnh tuỳ chỉnh để các thành phần kết hợp mẹ khác có thể sử dụng để căn chỉnh và định vị thành phần con cháu phù hợp.

Ví dụ sau đây thể hiện một thành phần kết hợp BarChart tuỳ chỉnh hiển thị 2 dòng căn chỉnh, MaxChartValueMinChartValue, để các thành phần kết hợp khác có thể căn chỉnh theo giá trị dữ liệu tối đa và tối thiểu của biểu đồ. Hai thành phần văn bản, Tối đaTối thiểu, đã được căn chỉnh ở chính giữa các dòng căn chỉnh tuỳ chỉnh.

Hình 2. BarChart có thể kết hợp với Văn bản được căn chỉnh theo giá trị dữ liệu tối đa và tối thiểu.

Các dòng căn chỉnh tuỳ chỉnh được xác định là các biến cấp cao nhất trong dự án của bạn.

/**
 * 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)
})

Các dòng căn chỉnh tuỳ chỉnh để làm ví dụ thuộc loại HorizontalAlignmentLine, được dùng để căn chỉnh thành phần con cháu theo chiều dọc. Chính sách hợp nhất được truyền khi một thông số trong trường hợp nhiều bố cục cung cấp một giá trị cho các dòng căn chỉnh này. Khi toạ độ hệ thống bố cục của Compose và tọa độ Canvas đại diện cho [0, 0], thì góc trên cùng bên trái cùng trục xy là chiều dương hướng xuống dưới, vì vậy, giá trị MaxChartValue sẽ luôn nhỏ hơn MinChartValue. Do đó, chính sách hợp nhất là min đối với đường cơ sở giá trị dữ liệu biểu đồ tối đa và max đối với đường cơ sở giá trị dữ liệu biểu đồ tối thiểu.

Khi tạo Layout hoặc LayoutModifier tuỳ chỉnh, hãy chỉ định các dòng căn chỉnh tuỳ chỉnh trong phương thức MeasureScope.layout Phương thức này sẽ có một thông số 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()
                        )
                    ) {}
                }
            }
        }
    }
}

Thành phần mẹ trực tiếp và gián tiếp của thành phần kết hợp này có thể sử dụng dòng căn chỉnh. Thành phần kết hợp sau đây tạo ra một bố cục tuỳ chỉnh có thông số hai khe Text và các điểm dữ liệu, đồng thời sắp xếp hai văn bản này theo giá trị dữ liệu biểu đồ tối đa và tối thiểu. Bản xem trước của thành phần kết hợp này là nội dung biểu thị trong Hình 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)
        )
    }
}