Jetpack Compose의 정렬 선

Compose 레이아웃 모델을 사용하면 AlignmentLine을 사용하여 맞춤 정렬 선을 만들 수 있습니다. 이 맞춤 정렬 선은 하위 요소를 정렬하고 위치를 지정할 때 상위 요소 레이아웃에 사용할 수 있습니다. 예를 들어 Row는 하위 요소의 맞춤 정렬 선을 사용하여 하위 요소를 정렬할 수 있습니다.

레이아웃에서 특정 AlignmentLine 값을 제공하는 경우 레이아웃의 상위 요소는 대응하는 Placeable 인스턴스에 Placeable.get 연산자를 사용하여 이 값을 측정 후 읽어올 수 있습니다. 그러면 AlignmentLine의 위치에 따라 상위 요소는 하위 요소의 위치 지정을 결정할 수 있습니다.

Compose의 일부 컴포저블에는 이미 정렬 선이 함께 제공됩니다. 예를 들어 BasicText 컴포저블은 FirstBaselineLastBaseline 정렬 선을 노출합니다.

다음 예에서 firstBaselineToTop이라는 맞춤 LayoutModifierFirstBaseline을 읽고 첫 번째 기준부터 시작하여 텍스트에 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를 만들 때 맞춤 정렬 선을 제공할 수 있습니다. 그러면 다른 상위 컴포저블이 그 정렬 선을 사용하여 그에 따라 하위 요소를 정렬하고 위치를 지정할 수 있습니다.

다음 예에서는 다른 컴포저블을 차트의 최대 및 최소 데이터 값에 정렬할 수 있도록 두 개의 정렬 선 MaxChartValueMinChartValue를 노출하는 맞춤 BarChart 컴포저블을 보여줍니다. 두 개의 텍스트 요소 MaxMin이 맞춤 정렬 선의 가운데에 정렬되었습니다.

텍스트가 최대 및 최소 데이터 값에 정렬된 BarChart 컴포저블
그림 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입니다.

맞춤 Layout 또는 LayoutModifier를 만들 때, 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)
        )
    }
}