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

이 예에서는 FirstBaseline을 읽기 위해 측정 단계에서 placeable [FirstBaseline]이 사용되었습니다.

맞춤 정렬 선 만들기

맞춤 Layout 컴포저블 또는 맞춤 LayoutModifier를 만들 때 맞춤 정렬 선을 제공할 수 있습니다. 그러면 다른 상위 컴포저블이 그 정렬 선을 사용하여 그에 따라 하위 요소를 정렬하고 위치를 지정할 수 있습니다.

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

그림 2. 텍스트가 최대 및 최소 데이터 값에 정렬된 BarChart 컴포저블

맞춤 정렬 선은 프로젝트에서 최상위 수준의 변수로 정의됩니다.

import kotlin.math.max
import kotlin.math.min
import androidx.compose.ui.layout.HorizontalAlignmentLine

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

/**
 * AlignmentLine defined by the minimum data value in a [BarChart]
 */
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
fun BarChart(
    dataPoints: List<Int>,
    modifier: Modifier = Modifier
) {
    var maxValueBaseline by remember { mutableStateOf(Float.MAX_VALUE) }
    var minValueBaseline by remember { mutableStateOf(Float.MIN_VALUE) }

    Layout(
        modifier = modifier,
        content = {
            // ... Logic to draw the chart in a Canvas ...
            // maxValueBaseline and minValueBaseline are updated here
        }
    ) { measurables, constraints ->
        val placeable = measurables[0].measure(constraints)
        layout(
            width = constraints.maxWidth,
            height = constraints.maxHeight,
            // Custom AlignmentLines are set here. These are propagated
            // to direct and indirect parent composables.
            alignmentLines = mapOf(
                MinChartValue to minValueBaseline.roundToInt(),
                MaxChartValue to maxValueBaseline.roundToInt()
            )
        ) {
            placeable.placeRelative(0, 0)
        }
    }
}

이 컴포저블의 직접 및 간접 상위 요소는 정렬 선을 사용할 수 있습니다. 다음 컴포저블은 두 개의 Text 슬롯과 데이터 포인트를 매개변수로 취하는 맞춤 레이아웃을 만들고, 두 텍스트를 최대 및 최소 차트 데이터 값에 정렬합니다. 이 컴포저블의 미리보기는 그림 2와 같습니다.

@Composable
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
fun ChartDataPreview() {
    MaterialTheme {
        BarChartMinMax(
            dataPoints = listOf(4, 24, 15),
            maxText = { Text("Max") },
            minText = { Text("Min") },
            modifier = Modifier.padding(24.dp)
        )
    }
}