맞춤 레이아웃

Compose에서 UI 요소는 호출될 때 UI 요소를 내보내는 구성 가능한 함수로 표시됩니다. 그런 다음 화면에 렌더링되는 UI 트리에 추가됩니다. 각 UI 요소에는 하나의 상위 요소와 여러 개의 하위 요소가 있을 수 있습니다. 또한 각 요소는 (x, y) 위치로 지정된 상위 요소 내에 배치되며 widthheight로 크기가 지정됩니다.

상위 요소는 하위 요소의 제약 조건을 정의합니다. 이러한 제약 조건 내에서 요소의 크기를 정의해야 합니다. 제약 조건은 요소의 최소/최대 widthheight를 제한합니다. 요소에 하위 요소가 있으면 각 하위 요소를 측정하여 요소의 크기를 결정할 수 있습니다. 요소가 자체 크기를 결정하고 보고하면 맞춤 레이아웃 만들기에 자세히 설명된 대로 그 요소를 기준으로 하위 요소를 배치하는 방법을 정의할 수 있습니다.

단일 패스 측정은 성능 측면에서 효율적이므로 Compose가 깊은 UI 트리를 효율적으로 처리할 수 있습니다. 요소가 하위 요소를 두 번 측정한 후 이 하위 요소가 자체 하위 요소들 중 하나를 두 번 측정하는 방식은 전체 UI를 배치하려는 한 번의 시도에서 많은 작업을 실행해야 하므로 앱의 성능을 유지하기가 어렵습니다. 그러나 단일 하위 요소 측정 결과에 따른 정보 외에 추가 정보가 실제로 필요할 때가 있습니다. 이러한 상황에 효율적으로 대처할 수 있는 접근 방식이 있으며 이 접근 방식은 내장 기능 측정에서 설명합니다.

범위 사용은 하위 요소를 측정하고 배치할 수 있는 시점을 정의합니다. 레이아웃 측정은 측정 및 레이아웃 패스 중에만 실행할 수 있고 하위 요소는 레이아웃 패스 중에만 그리고 미리 측정된 후에만 배치할 수 있습니다. MeasureScope, PlacementScope와 같은 Compose 범위로 인해 컴파일 시간에 적용됩니다.

레이아웃 수정자 사용

layout 수정자를 사용하여 요소가 측정되고 배치되는 방식을 수정할 수 있습니다. Layout은 람다입니다. 매개변수에는 측정할 수 있는 요소(measurable로 전달됨) 및 이 컴포저블의 수신된 제약 조건(constraints로 전달됨)이 포함됩니다. 맞춤 레이아웃 수정자는 다음과 같습니다.

fun Modifier.customLayoutModifier(...) =
    this.layout { measurable, constraints ->
        ...
    })

화면에 Text를 표시하고 텍스트 첫 줄의 상단에서 기준선까지의 거리를 제어해 보겠습니다. 이는 정확히 paddingFromBaseline 수정자의 기능이며 여기에 이 기능을 예로 구현합니다. 그렇게 하려면 layout 수정자를 사용하여 컴포저블을 화면에 수동으로 배치해야 합니다. 원하는 동작은 다음과 같으며 여기서 Text 상단 패딩은 24.dp로 설정됩니다.

요소 사이의 공간을 설정하는 일반 UI 패딩과 한 기준선에서 다음 기준선까지의 공간을 설정하는 텍스트 패딩의 차이를 보여주는 예시

다음은 해당 간격을 생성하는 코드입니다.

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

이 코드에서 이루어지는 작업은 다음과 같습니다.

  1. measurable 람다 매개변수에서 measurable.measure(constraints)를 호출하여 측정 가능한 매개변수로 표시되는 Text를 측정합니다.
  2. layout(width, height) 메서드를 호출하여 컴포저블의 크기를 지정합니다. 이 메서드는 래핑된 요소 배치에 사용되는 람다도 제공합니다. 이 경우 크기는 마지막 기준선과 추가된 상단 패딩 사이의 높이입니다.
  3. placeable.place(x, y)를 호출하여 화면에 래핑된 요소를 배치합니다. 래핑된 요소를 배치하지 않으면 래핑된 요소가 표시되지 않습니다. y 위치는 텍스트의 첫 번째 기준선 위치인 상단 패딩에 상응합니다.

예상대로 작동하는지 확인하려면 Text에서 다음 수정자를 사용합니다.

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
    MyApplicationTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

@Preview
@Composable
fun TextWithNormalPaddingPreview() {
    MyApplicationTheme {
        Text("Hi there!", Modifier.padding(top = 32.dp))
    }
}

텍스트 요소의 여러 미리보기: 하나는 요소 사이의 일반적인 패딩을 보여주고 다른 하나는 한 기준선에서 다음 기준선까지의 패딩을 보여줌

맞춤 레이아웃 만들기

layout 수정자는 호출하는 컴포저블만 변경합니다. 여러 컴포저블을 측정하고 배치하려면 Layout 컴포저블을 대신 사용하세요. 이 컴포저블을 사용하면 하위 요소를 수동으로 측정하고 배치할 수 있습니다. ColumnRow와 같은 모든 상위 수준 레이아웃은 Layout 컴포저블을 사용하여 빌드됩니다.

Column의 매우 기본적인 버전을 빌드해 보겠습니다. 대부분의 맞춤 레이아웃은 다음 패턴을 따릅니다.

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

layout 수정자와 마찬가지로 measurables는 측정해야 하는 하위 요소 목록이며 constraints는 상위 요소의 제약 조건입니다. 앞서와 동일한 로직에 따라 MyBasicColumn을 다음과 같이 구현할 수 있습니다.

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.map { measurable ->
            // Measure each children
            measurable.measure(constraints)
        }

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Track the y co-ord we have placed children up to
            var yPosition = 0

            // Place children in the parent layout
            placeables.forEach { placeable ->
                // Position item on the screen
                placeable.placeRelative(x = 0, y = yPosition)

                // Record the y co-ord placed up to
                yPosition += placeable.height
            }
        }
    }
}

하위 컴포저블은 (minHeight 제약 조건 없이) Layout 제약 조건에 의해 제한되며 이전 컴포저블의 yPosition을 기반으로 배치됩니다.

맞춤 컴포저블을 사용하는 방법은 다음과 같습니다.

@Composable
fun CallingComposable(modifier: Modifier = Modifier) {
    MyBasicColumn(modifier.padding(8.dp)) {
        Text("MyBasicColumn")
        Text("places items")
        Text("vertically.")
        Text("We've done it by hand!")
    }
}

열의 다음 텍스트 요소 위에 겹쳐진 여러 텍스트 요소

레이아웃 방향

LocalLayoutDirection 컴포지션 로컬을 변경하여 컴포저블의 레이아웃 방향을 변경합니다.

화면에 컴포저블을 수동으로 배치하는 경우 LayoutDirectionlayout 수정자 또는 Layout 컴포저블의 LayoutScope에 포함되어 있습니다.

layoutDirection을 사용할 때는 place를 사용하여 컴포저블을 배치합니다. placeRelative 메서드와 달리 place는 레이아웃 방향(왼쪽에서 오른쪽 또는 오른쪽에서 왼쪽)에 따라 변경되지 않습니다.

맞춤 레이아웃 실제 사례

Jetpack Compose 레이아웃 Codelab에서 맞춤 레이아웃 및 수정자에 관해 자세히 알아보고 맞춤 레이아웃을 만드는 Compose 샘플에서 작동 중인 API를 확인하세요.