본문 바로가기
Android/Compose

[Android] Compose의 정렬선(Alignment lines)

by 태크민 2025. 6. 2.

Jetpack Compose의 정렬선(Alignment lines)

Compose의 레이아웃 모델은 AlignmentLine을 사용하여 사용자 정의 정렬선을 만들 수 있도록 지원합니다. 이러한 정렬선은 부모 레이아웃이 자식들을 정렬하거나 배치할 때 사용할 수 있습니다. 예를 들어, Row는 자식들이 제공하는 커스텀 정렬선을 이용해 자식들을 정렬할 수 있습니다.

 

특정 AlignmentLine에 대한 값을 레이아웃이 제공하면, 부모는 측정 후 해당 자식의 Placeable 인스턴스를 통해 정렬선 위치 값을 읽을 수 있습니다 (Placeable.get 연산자 사용). 부모는 이렇게 얻은 정렬선의 위치를 기반으로 자식의 배치 위치를 결정할 수 있습니다.

일부 Compose 컴포저블은 이미 기본적으로 정렬선을 제공합니다. 예를 들어, BasicText 컴포저블은 FirstBaseline과 LastBaseline이라는 정렬선을 노출하고 있죠.

 

아래 예시에서는 firstBaselineToTop이라는 커스텀 LayoutModifier를 통해 Text의 첫 번째 베이스라인(FirstBaseline) 값을 읽어, 해당 베이스라인부터 위쪽으로 패딩을 적용하는 방식이 구현되어 있습니다.

그림 1. 일반적인 패딩을 요소에 적용한 경우와, 텍스트 요소의 베이스라인에 패딩을 적용한 경우의 차이를 보여줍니다.

 

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp,
) = layout { measurable, constraints ->
    // 컴포저블을 측정
    val placeable = measurable.measure(constraints)

    // 컴포저블이 FirstBaseline 값을 가지고 있는지 확인
    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))
    }
}

 

예제에서 FirstBaseline을 읽기 위해, 측정 단계에서 placeable[FirstBaseline]이 사용됩니다.

 

단, "firstBaselineToTop은 학습용으로 만든 예제일 뿐이고, 실제로는 Compose에 이미 그런 기능이 내장된 paddingFrom()이라는 Modifier가 있습니다.

Modifier.paddingFrom(alignmentLine = FirstBaseline, before = 32.dp)

 

즉, 실제 앱에서는 굳이 firstBaselineToTop() 같은 Modifier를 직접 만들 필요 없이, 처럼 paddingFrom() 을 사용해서 정렬선 기준 패딩이 가능합니다.

 


커스텀 정렬선 만들기

커스텀 Layout 컴포저블이나 LayoutModifier를 만들 때, 커스텀 정렬선(alignment line)을 제공할 수 있으며, 이를 통해 부모 컴포저블이 자식들을 정렬하거나 배치하는 데 사용할 수 있습니다.

 

아래 예시는 MaxChartValue와 MinChartValue라는 두 개의 커스텀 정렬선을 노출하는 BarChart 컴포저블을 보여줍니다. 이 정렬선을 기준으로 Max와 Min이라는 두 개의 텍스트 요소가 각각 차트의 최대값과 최소값 중심에 정렬되어 있습니다.

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

 

정렬선은 말 그대로 컴포저블 내부에 숨겨진 '눈에 보이지 않는 기준선'입니다. 부모 레이아웃은 이 정렬선을 참고해 자식들을 정렬합니다.

 

예를 들어 막대 그래프인 BarChart를 만들고, 그래프의 최댓값 위치최솟값 위치를 다른 컴포저블들과 정렬하고 싶다면, 그 위치에 정렬선을 만들어야 합니다. 그래야 Text("MAX"), Text("MIN") 같은 텍스트를 막대그래프의 위쪽과 아래쪽에 딱 맞게 배치할 수 있습니다.

이런 경우에는 Compose에서 제공하는 HorizontalAlignmentLine을 사용해서 정렬선을 만들 수 있습니다. 이름은 자유롭게 정할 수 있고, 예제에서는 아래와 같이 정의합니다:

 * 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 타입입니다. 이 정렬선은 자식들을 수직 방향으로 정렬할 때 사용되기 때문입니다.

여러 레이아웃이 같은 정렬선에 값을 제공하는 경우를 대비해, 병합 정책(merge policy)을 파라미터로 전달합니다.

Compose의 레이아웃 좌표계와 Canvas 좌표계는 [0, 0]이 왼쪽 위 모서리이며, x축과 y축은 아래 방향으로 양의 값을 가지므로, MaxChartValue의 값은 항상 MinChartValue보다 작게 됩니다.

 

따라서 병합 정책은 다음과 같습니다:

  • 최대값(MaxChartValue) 정렬선: min (값들 중 가장 위쪽)
  • 최소값(MinChartValue) 정렬선: max (값들 중 가장 아래쪽)

실제로 아래 예제와 같이 커스텀 Layout 또는 LayoutModifier를 만들 때는 MeasureScope.layout 메서드에서 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()
                        )
                    ) {}
                }
            }
        }
    }
}

 

이 컴포저블의 부모는 해당 컴포저블이 제공하는 정렬선(Alignment Line)을 사용할 수 있습니다.


아래 예시는 두 개의 Text 슬롯(maxText, minText)과 dataPoints를 인자로 받아,  막대 그래프의 최대값과 최소값 위치에 두 텍스트를 정렬하는 커스텀 레이아웃을 만드는 코드입니다. 

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

 

위의 코드를 하나 하나 분석해보겠습니다.

@Composable
private fun BarChartMinMax(
    dataPoints: List<Int>,
    maxText: @Composable () -> Unit,
    minText: @Composable () -> Unit,
    modifier: Modifier = Modifier,
)


이 함수는 maxText, minText 컴포저블 슬롯과 데이터 리스트를 받아, maxText()와 minText()를 각각 최대/최소값 정렬선에 정렬시킵니다. 내부에 BarChart도 포함되며, 이 BarChart가 정렬선을 제공합니다.

 

        Layout(
            content = {
                maxText()
                minText()
                BarChart(dataPoints, Modifier.size(200.dp))
            },


고정된 크기의 BarChart를 넣어 예제를 단순하게 만들었고, 총 3개의 컴포저블이 들어간다고 가정합니다.

 

    val placeables = measurables.map {
            it.measure(constraints.copy(minWidth = 0, minHeight = 0))
        }


각각의 요소들을 측정할 때 최소 너비/높이를 0으로 지정해 자유롭게 배치할 수 있도록 합니다.

 

        val minValueBaseline = barChartPlaceable[MinChartValue]
        val maxValueBaseline = barChartPlaceable[MaxChartValue]


BarChart가 제공한 정렬선 값을 여기서 꺼내옵니다.
이 값은 y 좌표이며, Text("Max")와 Text("Min")을 이 위치에 배치하기 위한 기준이 됩니다.

 

        layout(constraints.maxWidth, constraints.maxHeight) {
            maxTextPlaceable.placeRelative(
                x = 0,
                y = maxValueBaseline - (maxTextPlaceable.height / 2)
            )
            minTextPlaceable.placeRelative(
                x = 0,
                y = minValueBaseline - (minTextPlaceable.height / 2)
            )


Text("Max")는 MaxChartValue 기준선의 중앙에 오도록 배치되고, Text("Min")은 MinChartValue 기준선의 중앙에 오도록 배치됩니다.

 

       barChartPlaceable.placeRelative(
                x = max(maxTextPlaceable.width, minTextPlaceable.width) + 20,
                y = 0
            )


BarChart는 텍스트 옆에 충분한 간격(20dp)을 두고 오른쪽에 배치됩니다.

 

정렬 기준이 아직 정해지지 않았는데, 어떻게 먼저 호출되는 Text가 거기에 정렬될 수 있나요?

    Layout(
        content = {
            maxText()
            minText()
            // Set a fixed size to make the example easier to follow
            BarChart(dataPoints, Modifier.size(200.dp))
        },
        modifier = modifier
    ){ .. }

 

그 이유는 Compose는 “그리기 전에 모든 컴포저블을 먼저 측정”해서 정렬 기준을 계산한 뒤, 다시 실제 위치를 배치하기 때문이에요.

  1. 측정 (Measure)
    각 컴포저블이 "나는 어느 정도 크기야", "내 정렬선은 여기야"라고 정보를 제공함
    👉 이때 BarChart는 MaxChartValue, MinChartValue 정렬선을 Map에 담아서 보고함
  2. 정렬선 수집 (Alignment Line Propagation)
    부모(Column, Row)는 모든 자식들이 제공한 정렬선 정보를 수집
  3. 배치 (Placement)정렬선을 기준으로 자식들(Text("Max"), BarChart, Text("Min"))을 배치함
    👉 alignBy(MaxChartValue)에 따라 Text("Max")를 정렬선 위치에 맞게 배치

자식 컴포저블을 measure할 때 minWidth = 0, minHeight = 0을 설정해서 왜 측정하나요?

결론부터 말하면, 자식의 내용만큼만 공간을 차지하도록 하고 싶기 때문입니다.

그래서 강제로 minWidth/minHeight를 0으로 설정하는 거예요.

 

설정 여부에 따라 아래와 같은 차이가 발생합니다.

 

minWidth = 0 없이 그냥 부모의 constraints를 사용하여 측정한 경우

val placeables = measurables.map {
    it.measure(constraints) // 부모로부터 받은 제약 그대로 사용
}
  • 부모가 minWidth = 100dp라는 제약을 줬다고 가정하면, "Max" 텍스트도 최소 너비 100dp 이상으로 측정됨.
  • 그럼 실제로 텍스트 내용이 작아도 공간을 크게 차지함불필요한 여백 발생
  • 배치 예시: [   Max   ][           BarChart           ]
      ↑ 실제 글자보다 여백이 많음

반면, minWidth = 0으로 제약을 풀어준 경우

val placeables = measurables.map {
    it.measure(constraints.copy(minWidth = 0, minHeight = 0))
}
  • "Max" 텍스트는 글자 크기만큼만 딱 측정됨.
  • BarChart는 텍스트가 차지한 만큼만 오른쪽으로 밀려 배치됨.
  • 배치 예시: [Max][           BarChart           ]
     ↑ 딱 글자 크기만큼만 공간 차지

 

 

끝.


참고자료

https://developer.android.com/develop/ui/compose/layouts/alignment-lines?_gl=1*asq7fs*_up*MQ..*_ga*OTkzNjI4NTA4LjE3NDg4MjkyNDA.*_ga_6HH9YJMN9M*czE3NDg4MjkyNDAkbzEkZzAkdDE3NDg4MjkyNDAkajYwJGwwJGgxMjgxMjk2MDkz

 

Jetpack Compose의 정렬 선  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Jetpack Compose의 정렬 선 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose 레이아웃 모델을 사용하

developer.android.com