Compose의 ConstraintLayout

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

다음 시나리오에서는 ConstraintLayout을 사용하는 것이 좋습니다.

  • 코드 가독성 개선을 위해 화면에 요소를 배치하는 여러 ColumnRow를 중첩하지 않습니다.
  • 다른 컴포저블을 기준으로 컴포저블을 배치하거나 가이드라인, 배리어, 체인을 기반으로 컴포저블을 배치합니다.

뷰 시스템에서 ConstraintLayout은 크고 복잡한 레이아웃을 만드는 데 권장되는 방법이었습니다. 플랫 뷰 계층 구조가 중첩된 뷰보다 성능 면에서 더 좋았기 때문입니다. 그러나 이는 깊은 레이아웃 계층 구조를 효율적으로 처리할 수 있는 Compose에서는 문제가 되지 않습니다.

ConstraintLayout 시작하기

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

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"

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에 정렬된 버튼 및 텍스트 요소 예시

분리된 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을 전달하기만 하면 됩니다.

ConstraintLayout 개념

ConstraintLayout에는 컴포저블 내에서 요소를 배치하는 데 도움이 될 수 있는 가이드라인, 배리어, 체인과 같은 개념이 포함되어 있습니다.

가이드라인

가이드라인은 레이아웃을 디자인할 때 사용할 수 있는 작은 시각적 도우미입니다. 컴포저블은 가이드라인으로 제한할 수 있습니다. 가이드라인은 상위 컴포저블 내에서 특정 dp 또는 percentage에 요소를 배치하는 데 유용합니다.

가이드라인에는 세로, 가로 두 종류가 있습니다. 가로 가이드라인 두 개는 topbottom이고 세로 두 개는 startend입니다.

ConstraintLayout {
    // Create guideline from the start of the parent at 10% the width of the Composable
    val startGuideline = createGuidelineFromStart(0.1f)
    // Create guideline from the end of the parent at 10% the width of the Composable
    val endGuideline = createGuidelineFromEnd(0.1f)
    //  Create guideline from 16 dp from the top of the parent
    val topGuideline = createGuidelineFromTop(16.dp)
    //  Create guideline from 16 dp from the bottom of the parent
    val bottomGuideline = createGuidelineFromBottom(16.dp)
}

가이드라인을 만들려면 필요한 가이드라인 유형과 함께 createGuidelineFrom*을 사용합니다. 이렇게 하면 Modifier.constrainAs() 블록에서 사용할 수 있는 참조가 만들어집니다.

배리어

배리어는 여러 컴포저블을 참조하여, 지정된 쪽에서 가장 극단적인 위젯을 기반으로 가상 가이드라인을 만듭니다.

배리어를 만들려면 createTopBarrier()(또는 createBottomBarrier(), createEndBarrier(), createStartBarrier())를 사용하고, 배리어를 구성해야 하는 참조를 제공합니다.

ConstraintLayout {
    val constraintSet = ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        val topBarrier = createTopBarrier(button, text)
    }
}

그러면 배리어를 Modifier.constrainAs() 블록에서 사용할 수 있습니다.

체인

체인은 단일 축(가로 또는 세로)에서 그룹과 같은 동작을 제공합니다. 다른 축은 독립적으로 제한할 수 있습니다.

체인을 만들려면 createVerticalChain 또는 createHorizontalChain을 사용합니다.

ConstraintLayout {
    val constraintSet = ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        val verticalChain = createVerticalChain(button, text, chainStyle = ChainStyle.Spread)
        val horizontalChain = createHorizontalChain(button, text)
    }
}

그러면 Modifier.constrainAs() 블록에서 체인을 사용할 수 있습니다.

체인은 여러 ChainStyles로 구성하여 컴포저블을 둘러싼 공간을 처리하는 방법을 결정합니다. 예를 들면 다음과 같습니다.

  • ChainStyle.Spread: 공간이 첫 번째 컴포저블 앞과 마지막 컴포저블 뒤의 여유 공간을 포함하여 모든 컴포저블에서 균등하게 분산됩니다.
  • ChainStyle.SpreadInside: 공간이 첫 번째 컴포저블 앞이나 마지막 컴포저블 뒤의 여유 공간 없이 모든 컴포저블에서 균등하게 분산됩니다.
  • ChainStyle.Packed: 공간이 첫 번째 컴포저블 앞과 마지막 컴포저블 뒤에 분산되고 컴포저블은 서로 간에 공간 없이 패킹됩니다.

자세히 알아보기

ConstraintLayout를 사용하는 Compose 샘플의 작동 중인 API에서 Compose의 ConstraintLayout에 관해 자세히 알아보세요.