Compose의 레이아웃

Jetpack Compose를 사용하면 앱의 UI를 훨씬 쉽게 디자인하고 빌드할 수 있습니다. 이 문서에서는 개발자가 UI 요소를 배치하는 데 도움이 되도록 Compose에서 제공하는 몇 가지 구성요소를 설명하며 필요시 더 전문적인 레이아웃을 빌드하는 방법을 보여줍니다.

Compose의 레이아웃 목표

Jetpack Compose 레이아웃 시스템의 구현에는 두 가지 주요 목표가 있습니다. 고성능을 발휘하는 것과 맞춤 레이아웃을 쉽게 작성하도록 하는 것입니다. 고성능은 Compose에서 레이아웃 하위 요소를 두 번 이상 측정하는 것을 금지하여 달성할 수 있습니다. 여러 측정값이 필요하면 Compose는 내장 기능 측정이라는 특수한 시스템을 사용합니다. 이 기능에 관한 자세한 내용은 내장 기능 측정 섹션을 참고하세요.

구성 가능한 함수 기본사항

구성 가능한 함수는 Compose의 기본 구성요소입니다. 구성 가능한 함수는 UI의 일부를 설명하는 Unit을 내보내는 함수입니다. 이 함수는 몇 가지 입력을 받아서 화면에 표시되는 내용을 생성합니다. 컴포저블에 관한 자세한 내용은 Compose 멘탈 모델 문서를 살펴보세요.

구성 가능한 함수는 여러 UI 요소를 내보낼 수 있습니다. 그러나 개발자가 UI 요소를 어떻게 정렬해야 하는지에 관한 가이드를 제공하지 않으면 Compose는 개발자가 원하지 않는 방식으로 요소를 정렬할 수 있습니다. 예를 들어 다음 코드는 텍스트 요소 두 개를 생성합니다.

@Composable
fun ArtistCard() {
    Text("Alfred Sisley")
    Text("3 minutes ago")
}

원하는 정렬 방식에 관한 가이드가 없으면 Compose는 텍스트 요소를 서로 겹치게 표시하므로 텍스트를 읽을 수 없게 됩니다.

두 개의 텍스트 요소가 서로 위에 그려져 텍스트를 읽을 수 없게 됨

Compose는 UI 요소를 정렬하는 데 도움이 되도록 즉시 사용 가능한 레이아웃 컬렉션을 제공하므로 이를 사용하면 더욱 전문적인 고유한 레이아웃을 쉽게 정의할 수 있습니다.

표준 레이아웃 구성요소

많은 경우에 Compose의 표준 레이아웃 요소를 사용할 수 있습니다.

Column을 사용하여 항목을 화면에 세로로 배치합니다.

@Composable
fun ArtistCard() {
    Column {
        Text("Alfred Sisley")
        Text("3 minutes ago")
    }
}

두 개의 텍스트 요소가 열 레이아웃으로 정렬되므로 텍스트를 읽을 수 있음

마찬가지로 Row를 사용하여 항목을 화면에 가로로 배치합니다. ColumnRow는 모두 포함된 요소의 정렬 구성을 지원합니다.

@Composable
fun ArtistCard(artist: Artist) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(/*...*/)
        Column {
            Text(artist.name)
            Text(artist.lastSeenOnline)
        }
    }
}

텍스트 요소의 열 옆에 작은 그래픽이 있는 더 복잡한 레이아웃 예시

Box을 사용하여 한 요소를 다른 요소 위에 배치합니다.

세 가지 간단한 레이아웃 컴포저블(열, 행, 상자) 비교

흔히 이러한 구성요소만 있으면 됩니다. 자체 구성 가능한 함수를 작성하여 이러한 여러 레이아웃을 앱에 적합한 더욱 정교한 레이아웃으로 결합할 수 있습니다.

Row 내에서 하위 요소의 위치를 설정하려면 horizontalArrangementverticalAlignment 인수를 설정하세요. Column의 경우 verticalArrangementhorizontalAlignment 인수를 설정합니다.

@Composable
fun ArtistCard(artist: Artist) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.End
    ) {
        Image(/*...*/)
        Column { /*...*/ }
    }
}

항목이 오른쪽에 정렬됩니다.

수정자

수정자를 사용하면 컴포저블을 장식하거나 강화할 수 있습니다. 수정자를 통해 다음과 같은 종류의 작업을 실행할 수 있습니다.

  • 컴포저블의 크기, 레이아웃, 동작 및 모양 변경
  • 접근성 라벨과 같은 정보 추가
  • 사용자 입력 처리
  • 요소를 클릭 가능, 스크롤 가능, 드래그 가능 또는 확대/축소 가능하게 만드는 것과 같은 높은 수준의 상호작용 추가

수정자는 표준 Kotlin 객체입니다. Modifier 클래스 함수 중 하나를 호출하여 수정자를 만듭니다. 다음과 같이 이러한 함수를 함께 연결하여 구성할 수 있습니다.

@Composable
fun ArtistCard(
    artist: Artist,
    onClick: () -> Unit
) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
        Spacer(Modifier.size(padding))
        Card(elevation = 4.dp) { /*...*/ }
    }
}

수정자를 사용하여 그래픽이 정렬되는 방식과 사용자 입력에 응답하는 영역을 변경하는 훨씬 더 복잡한 레이아웃

위의 코드에서 다양한 수정자 함수가 함께 사용된 것을 확인할 수 있습니다.

  • clickable: 컴포저블이 사용자 입력에 반응하도록 설정하고 물결 효과를 표시합니다.
  • padding: 요소 주위에 공간을 배치합니다.
  • fillMaxWidth: 컴포저블이 상위 요소로부터 부여받은 최대 너비를 채우도록 합니다.
  • size(): 요소의 기본 너비 및 높이를 지정합니다.

수정자의 순서가 중요

수정자 함수의 순서는 중요합니다. 각 함수는 이전 함수에서 반환한 Modifier를 변경하므로 순서는 최종 결과에 영향을 줍니다. 다음 예를 살펴보겠습니다.

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

가장자리 주변의 패딩을 포함한 영역 전체가 클릭에 반응함

위의 코드에서는 padding 수정자가 clickable 수정자 뒤에 적용되었기 때문에 주변 패딩을 포함하여 전체 영역을 클릭할 수 있습니다. 수정자 순서가 뒤집히면 다음과 같이 padding으로 추가된 공간은 사용자 입력에 반응하지 않습니다.

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .padding(padding)
            .clickable(onClick = onClick)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

레이아웃 가장자리 주변의 패딩이 더 이상 클릭에 반응하지 않음

내장 수정자

Jetpack Compose는 컴포저블을 장식하거나 강화하는 데 도움이 되는 내장 수정자 목록을 제공합니다. padding, clickable, fillMaxWidth와 같은 일부 수정자는 이미 도입되었습니다. 다른 일반적인 수정자 목록은 다음과 같습니다.

크기

기본적으로 Compose에 제공된 레이아웃은 하위 요소를 래핑합니다. 하지만 size 수정자를 사용하여 크기를 설정할 수 있습니다.

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(/*...*/)
        Column { /*...*/ }
    }
}

지정한 크기가 레이아웃의 상위 요소에서 수신된 제약 조건을 충족하지 않는 경우 적용되지 않을 수 있습니다. 수신된 제약 조건에 관계없이 컴포저블의 크기를 고정해야 하는 경우 requiredSize 수정자를 사용하세요.

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.requiredSize(150.dp)
        )
        Column { /*...*/ }
    }
}

하위 이미지는 상위 이미지의 제약 조건보다 큽니다.

이 예에서는 상위 height100.dp로 설정되더라도 Image 높이는 150.dp가 됩니다. requiredSize 수정자가 우선하기 때문입니다.

하위 레이아웃이 상위 요소에 의해 허용된 모든 가용 높이를 채우도록 하려면 fillMaxHeight 수정자를 추가합니다(Compose는 fillMaxSizefillMaxWidth도 제공함).

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.fillMaxHeight()
        )
        Column { /*...*/ }
    }
}

이미지 높이가 상위 요소만큼 큽니다.

레이아웃 상단에서 기준선까지 특정 거리가 유지되도록 텍스트 기준선 위에 패딩을 추가하려면 paddingFromBaseline 수정자를 사용합니다.

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(
                text = artist.name,
                modifier = Modifier.paddingFromBaseline(top = 50.dp)
            )
            Text(artist.lastSeenOnline)
        }
    }
}

위에 패딩이 있는 텍스트

오프셋

원래 위치를 기준으로 레이아웃을 배치하려면 offset 수정자를 추가하고 x축 및 y축에 오프셋을 설정합니다. 오프셋은 양수일 수도 있고 양수가 아닐 수도 있습니다. paddingoffset의 차이점은 컴포저블에 offset을 추가해도 측정값이 변경되지 않는다는 것입니다.

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(artist.name)
            Text(
                text = artist.lastSeenOnline,
                modifier = Modifier.offset(x = 4.dp)
            )
        }
    }
}

텍스트가 상위 컨테이너의 오른쪽으로 이동함

offset 수정자는 레이아웃 방향에 따라 가로로 적용됩니다. 왼쪽에서 오른쪽으로 컨텍스트에서 양수 offset은 요소를 오른쪽으로 이동하고 오른쪽에서 왼쪽으로 컨텍스트에서는 요소를 왼쪽으로 이동합니다. 레이아웃 방향을 고려하지 않고 오프셋을 설정해야 하는 경우 양의 오프셋 값이 항상 요소를 오른쪽으로 이동시키는 absoluteOffset 수정자를 확인하세요.

Compose의 유형 안전성

Compose에는 특정 컴포저블의 하위 요소에 적용될 때만 작동하는 수정자가 있습니다. 예를 들어 하위 요소를 Box 크기에 영향을 미치지 않고 상위 Box만큼 크게 만들려면 matchParentSize 수정자를 사용합니다.

Compose는 맞춤 범위를 통해 이 유형 안전성을 적용합니다. 예를 들어 matchParentSizeBoxScope에서만 사용할 수 있습니다. 따라서 하위 요소가 Box 내에서 사용될 때만 사용할 수 있습니다.

범위 지정 수정자는 상위 요소가 하위 요소에 관해 알아야 하는 정보를 상위 요소에 알립니다. 일반적으로 상위 데이터 수정자라고도 합니다. 내부 요소는 범용 수정자와 다르지만 사용 관점에서 이러한 차이는 중요하지 않습니다.

상자의 matchParentSize

위에서 언급했듯이 하위 레이아웃이 Box 크기에 영향을 미치지 않고 상위 Box와 크기가 같이지도록 하려면 matchParentSize 수정자를 사용하세요.

matchParentSizeBox 범위 내에서만 사용할 수 있습니다. 즉 Box 컴포저블의 직접 하위 요소에만 적용됩니다.

아래 예에서 하위 Spacer는 상위 Box에서 크기를 가져오고 결과적으로 가장 큰 하위 요소(이 경우에는 ArtistCard)에서 크기를 가져옵니다.

@Composable
fun MatchParentSizeComposable() {
    Box {
        Spacer(Modifier.matchParentSize().background(Color.LightGray))
        ArtistCard()
    }
}

컨테이너를 채우는 회색 배경

matchParentSize 대신 fillMaxSize가 사용된 경우 Spacer는 허용된 모든 가용 공간을 상위 요소로 가져온 다음 상위 요소에서 모든 가용 공간을 확장하고 채웁니다.

화면을 채우는 회색 배경

행 및 열의 가중치

이전 패딩 및 크기 섹션에서 확인했듯이 기본적으로 컴포저블 크기는 컴포저블이 래핑하는 콘텐츠로 정의됩니다. RowScopeColumnScope에서만 사용할 수 있는 weight 수정자를 사용하여 컴포저블 크기를 상위 요소 내에서 유연하게 설정할 수 있습니다.

두 개의 Box 컴포저블이 포함된 Row를 사용하겠습니다. 첫 번째 상자의 weight가 두 번째 상자의 두 배로 지정되므로 너비가 두 배로 지정됩니다. Row의 너비가 210.dp이므로 첫 번째 Box의 너비는 140.dp이고 두 번째는 70.dp입니다.

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.fillMaxWidth()
    ) {
        Image(
            /*...*/
            modifier = Modifier.weight(2f)
        )
        Column(
            modifier = Modifier.weight(1f)
        ) {
            /*...*/
        }
    }
}

이미지 너비가 텍스트 너비의 두 배입니다.

스크롤 가능한 레이아웃

Compose 동작 문서에서 스크롤 가능한 레이아웃에 관해 자세히 알아보세요.

목록과 지연 목록은 Compose 목록 문서를 참고하세요.

반응형 레이아웃

레이아웃은 여러 화면 방향과 폼 팩터 크기를 고려하여 디자인해야 합니다. Compose에서 제공하는 즉시 사용 가능한 몇 가지 메커니즘으로 컴포저블 레이아웃을 다양한 화면 구성에 따라 쉽게 조정할 수 있습니다.

제약 조건

상위 요소의 제약 조건을 파악하고 그에 따라 레이아웃을 디자인하려면 BoxWithConstraints를 사용하면 됩니다. 측정 제약 조건은 콘텐츠 람다의 범위에서 확인할 수 있습니다. 이 측정 제약 조건을 사용하여 다양한 화면 구성에 따라 다양한 레이아웃을 구성할 수 있습니다.

@Composable
fun WithConstraintsComposable() {
    BoxWithConstraints {
        Text("My minHeight is $minHeight while my maxWidth is $maxWidth")
    }
}

슬롯 기반 레이아웃

Compose는 UI를 쉽게 빌드할 수 있도록 머티리얼 디자인 및 Android 스튜디오에서 Compose 프로젝트를 만들 때 포함되는 androidx.compose.material:material 종속 항목을 기반으로 한 다양한 컴포저블을 제공합니다. Drawer, FloatingActionButtonTopAppBar와 같은 요소가 모두 제공됩니다.

머티리얼 구성요소는 Compose가 컴포저블 위에 맞춤설정 레이어를 배치하기 위해 도입한 패턴인 슬롯 API를 많이 사용합니다. 이 접근 방식을 사용하면 하위 요소의 모든 구성 매개변수를 노출하지 않고 자체적으로 하위 요소를 구성할 수 있으므로 구성요소의 유연성이 향상됩니다. 슬롯은 개발자가 원하는 대로 채울 수 있도록 UI에 빈 공간을 남겨둡니다. 예를 들어 다음은 개발자가 TopAppBar에서 맞춤설정할 수 있는 슬롯입니다.

머티리얼 구성요소 앱 바에 사용 가능한 슬롯을 보여주는 다이어그램

컴포저블은 일반적으로 content 컴포저블 람다(content: @Composable () -> Unit)를 사용합니다. 슬롯 API는 특정 용도를 위해 여러 content 매개변수를 노출합니다. 예를 들어 TopAppBar를 사용하면 title, navigationIconactions의 콘텐츠를 제공할 수 있습니다.

예를 들어 Scaffold를 사용하면 기본 머티리얼 디자인 레이아웃 구조로 UI를 구현할 수 있습니다. ScaffoldTopAppBar, BottomAppBar, FloatingActionButton, Drawer 등 가장 일반적인 상위 머티리얼 구성요소용 슬롯을 제공합니다. Scaffold를 사용하면 이러한 구성요소가 적절하게 배치되어 함께 올바르게 작동하는지 쉽게 확인할 수 있습니다.

Scaffold를 사용하여 여러 요소를 배치하는 JetNews 샘플 앱

@Composable
fun HomeScreen(/*...*/) {
    Scaffold(
        drawerContent = { /*...*/ },
        topBar = { /*...*/ },
        content = { /*...*/ }
    )
}

ConstraintLayout

ConstraintLayout은 화면에 다른 요소를 기준으로 컴포저블을 배치하는 데 도움이 될 수 있으며 중첩된 여러 Row, Column, Box 및 맞춤 레이아웃 요소 대신 사용할 수 있습니다. ConstraintLayout은 더 복잡한 정렬 요구사항이 있는 더 큰 레이아웃을 구현할 때 유용합니다.

Compose에서 ConstraintLayout을 사용하려면 build.gradle에 이 종속 항목을 추가해야 합니다.

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha08"

다음과 같이 Compose의 ConstraintLayoutDSL과 함께 작동합니다.

  • 참조는 createRefs() 또는 createRefFor()를 사용하여 생성되며 ConstraintLayout의 각 컴포저블에는 연결된 참조가 있어야 합니다.
  • 제약 조건은 constrainAs() 수정자를 사용하여 제공됩니다. 이 수정자는 참조를 매개변수로 사용하고 본문 람다에 제약 조건을 지정할 수 있게 합니다.
  • 제약 조건은 linkTo() 또는 다른 유용한 메서드를 사용하여 지정됩니다.
  • parentConstraintLayout 컴포저블 자체에 대한 제약 조건을 지정하는 데 사용할 수 있는 기존 참조입니다.

다음은 ConstraintLayout을 사용하는 컴포저블의 예입니다.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        // Create references for the composables to constrain
        val (button, text) = createRefs()

        Button(
            onClick = { /* Do something */ },
            // Assign reference "button" to the Button composable
            // and constrain it to the top of the ConstraintLayout
            modifier = Modifier.constrainAs(button) {
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button")
        }

        // Assign reference "text" to the Text composable
        // and constrain it to the bottom of the Button composable
        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
        })
    }
}

이 코드는 Button의 상단을 여백이 16.dp인 상위 요소로 제한하고 Text를 여백이 16.dpButton의 하단으로 제한합니다.

ConstraintLayout에 정렬된 버튼 및 텍스트 요소 예

ConstraintLayout을 사용하는 방법에 관한 더 많은 예를 보려면 레이아웃 Codelab을 사용해 보세요.

분리된 API

ConstraintLayout에서 제약 조건은 적용되는 컴포저블의 수정자와 함께 인라인으로 지정됩니다. 그러나 제약 조건이 적용되는 레이아웃에서 제약 조건을 분리하는 것이 더 좋은 상황이 있습니다. 예를 들어 화면 구성을 기반으로 제약 조건을 변경하거나 두 제약 조건 세트 사이에 애니메이션을 적용할 수 있습니다.

이 같은 경우에는 ConstraintLayout을 서로 다른 방식으로 사용할 수 있습니다.

  1. ConstraintSet을 매개변수로 ConstraintLayout에 전달합니다.
  2. layoutId 수정자를 사용하여 ConstraintSet에 생성된 참조를 컴포저블에 할당합니다.
@Composable
fun DecoupledConstraintLayout() {
    BoxWithConstraints {
        val constraints = if (minWidth < 600.dp) {
            decoupledConstraints(margin = 16.dp) // Portrait constraints
        } else {
            decoupledConstraints(margin = 32.dp) // Landscape constraints
        }

        ConstraintLayout(constraints) {
            Button(
                onClick = { /* Do something */ },
                modifier = Modifier.layoutId("button")
            ) {
                Text("Button")
            }

            Text("Text", Modifier.layoutId("text"))
        }
    }
}

private fun decoupledConstraints(margin: Dp): ConstraintSet {
    return ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        constrain(button) {
            top.linkTo(parent.top, margin = margin)
        }
        constrain(text) {
            top.linkTo(button.bottom, margin)
        }
    }
}

그러면 제약 조건을 변경해야 할 때 다른 ConstraintSet을 전달하기만 하면 됩니다.

자세히 알아보기

Jetpack Compose 레이아웃 Codelab의 제약 조건 레이아웃 섹션에서 Compose의 ConstraintLayout에 관해 자세히 알아보고 ConstraintLayout을 사용하는 Compose 샘플에서 작동 중인 API를 확인하세요.

맞춤 레이아웃

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,
        children = 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!")
    }
}

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

맞춤 레이아웃 실제 사례

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

레이아웃 방향

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

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

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

내장 측정

Compose 규칙 중 하나는 하위 요소를 한 번만 측정해야 한다는 것입니다. 하위 요소를 두 번 측정하면 런타임 예외가 발생합니다. 하지만 하위 요소를 측정하기 전에 하위 요소에 관한 정보가 필요한 경우도 있습니다.

내장 기능을 사용하면 하위 요소가 실제로 측정되기 전에 하위 요소를 쿼리할 수 있습니다.

컴포저블에 intrinsicWidth 또는 intrinsicHeight를 요청할 수 있습니다.

  • (min|max)IntrinsicWidth: 이 높이에서 콘텐츠를 적절하게 그릴 수 있는 최소/최대 너비는 무엇인가요?
  • (min|max)IntrinsicHeight: 이 너비에서 콘텐츠를 적절하게 그릴 수 있는 최소/최대 높이는 무엇인가요?

예를 들어 width가 무한대인 TextminIntrinsicHeight를 요청하면 텍스트가 한 줄에 그려진 것처럼 Textheight가 반환됩니다.

내장 기능 실제 사례

다음과 같이 화면에 구분선으로 구분된 두 텍스트를 표시하는 컴포저블을 만든다고 가정해 보겠습니다.

나란히 표시된 두 텍스트 요소, 사이에 세로 구분선이 있음

이렇게 하려면 어떻게 해야 하나요? 안에 두 Text가 있고 최대한 확장할 수 있으며 중앙에 Divider가 있는 Row를 만들 수 있습니다. 구분선을 가장 높은 Text만큼 높고 가늘게(width = 1.dp) 만들려고 합니다.

@Composable
fun TwoTexts(
    text1: String,
    text2: String,
    modifier: Modifier = Modifier
) {
    Row(modifier = modifier) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )

        Divider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),
            text = text2
        )
    }
}

@Preview
@Composable
fun TwoTextsPreview() {
    MaterialTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

미리 보면 구분선이 전체 화면으로 확장되며 이는 원하는 결과가 아닙니다.

나란히 표시된 두 텍스트 요소, 사이에 구분선이 있지만 구분선이 텍스트 하단 아래로 확장됨

Row가 각 하위 요소를 개별적으로 측정하며 Text의 높이를 사용하여 Divider를 제약할 수 없기 때문에 이러한 결과가 발생합니다. Divider가 가용 공간을 지정된 높이로 채우도록 하려고 합니다. 이를 위해 height(IntrinsicSize.Min) 수정자를 사용할 수 있습니다.

height(IntrinsicSize.Min)는 하위 요소의 크기를 고유한 최소 높이로 강제 지정합니다. 이 기능은 반복적이므로 Row 및 하위 minIntrinsicHeight를 쿼리합니다.

코드에 적용하면 예상대로 작동합니다.

@Composable
fun TwoTexts(
    text1: String,
    text2: String,
    modifier: Modifier = Modifier
) {
    Row(modifier = modifier.height(IntrinsicSize.Min)) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        Divider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),
            text = text2
        )
    }
}

미리보기 포함:

나란히 표시된 두 텍스트 요소, 사이에 세로 구분선이 있음

Row 컴포저블의 minIntrinsicHeight가 하위 요소의 최대 minIntrinsicHeight입니다. 제약 조건이 지정되지 않은 경우 공간을 차지하지 않으므로 Divider element'sminIntrinsicHeight는 0이고 Text minIntrinsicHeight는 특정 width가 지정된 경우 텍스트의 높이입니다. 따라서 Row 요소의 height 제약 조건은 Text의 최대 minIntrinsicHeight입니다. 그런 다음 DividerheightRow가 지정한 height 제약 조건으로 확장합니다.

맞춤 레이아웃의 내장 기능

맞춤 Layout 또는 layout 수정자를 만들 때 내장 기능 측정은 근사값에 따라 자동으로 계산됩니다. 따라서 일부 레이아웃의 계산은 정확하지 않을 수 있습니다. 이러한 API는 이와 같은 기본값을 재정의하는 옵션을 제공합니다.

맞춤 Layout의 내장 기능 측정을 지정하려면 만들 때 MeasurePolicy 인터페이스의 minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, maxIntrinsicHeight를 재정의하세요.

@Composable
fun MyCustomComposable(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    return object : MeasurePolicy {
        override fun MeasureScope.measure(
            measurables: List<Measurable>,
            constraints: Constraints
        ): MeasureResult {
            // Measure and layout here
        }

        override fun IntrinsicMeasureScope.minIntrinsicWidth(
            measurables: List<IntrinsicMeasurable>,
            height: Int
        ) = {
            // Logic here
        }

        // Other intrinsics related methods have a default value,
        // you can override only the methods that you need.
    }
}

맞춤 layout 수정자를 만들 때 LayoutModifier 인터페이스에서 관련 메서드를 재정의합니다.

fun Modifier.myCustomModifier(/* ... */) = this.then(object : LayoutModifier {

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        // Measure and layout here
    }

    override fun IntrinsicMeasureScope.minIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ): Int = {
        // Logic here
    }

    // Other intrinsics related methods have a default value,
    // you can override only the methods that you need.
})

자세히 알아보기

내장 기능 측정에 관한 자세한 내용은 Jetpack Compose 레이아웃 Codelab의 내장 기능 섹션을 참고하세요.

자세히 알아보기

자세히 알아보려면 Jetpack Compose의 레이아웃 Codelab을 참조하세요.