읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.

Jetpack Compose의 레이아웃 코드랩에서 실습한 내용들을 정리합니다. 해당 챕터에서 다루는 ConstraintLayout은 XML 기반 뷰 정의할 때 많이 쓰던 ConstraintLayout을 Compose 버전으로 바꿔서 도입된 라이브러리입니다. 따라서, 기존 뷰 정의 시 사용했던 레이아웃과 유사한 점이 있습니다. 컴포저블 간 Constraint를 적용하고 가이드라인 등 Helper 클래스를 사용하여 컴포저블 배치를 다른 컴포저블에 의존(연결)하게끔 정의합니다. 코드랩 실습에 요구되는 시간은 대략 1시간 정도로 학습 목적을 고려하면 대략 2 - 3시간 정도를 투자해야 합니다. 분량이 너무 많아 챕터 별로 분할하여 업로드합니다.

ConstraintLayout

Compose 도입으로 UI 트리를 사용해 화면을 구성함에 따라 공식적으로 ConstraintLayout은 가급적 권장하고 있지 않다. 오히려 복잡한 정렬이 필요한 경우 하위 요소들을 제어할 수 있는 커스텀 레이아웃 구성함이 바람직하다.

내장 레이아웃이 아니므로 별도의 라이브러리 import가 필요하다.

// build.gradle
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc01"

ConstraintLayout 사용을 위해 기억해야 할 점들이 있다.

  • ConstraintLayout의 각 컴포저블에는 참조가 연결되어야 하며 참조는 createRefs(), createRef()를 사용하여 생성한다.
  • Constraint는 constraintAs Modifier를 사용하여 연결한다. 해당 Modifier는 참조를 매개변수로 사용하며 본문 람다에 제약조건을 지정할 수 있다.
  • 제약 조건은 linkTo 혹은 다른 함수 등을 사용하여 지정한다.
  • parent는 ConstraintLayout 컴포저블 자체를 의미한다.
@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {

        // 컴포저블에 대해 레퍼런스를 생성한다.
        val (button, text) = createRefs()

        Button(
            onClick = { /* Do something */ },
            // Button 컴포저블에 "button" 참조를 할당한다.
            // ConstraintLayout의 top 부분에 연결한다.
            modifier = Modifier.constrainAs(button) {
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button")
        }

        // Text 컴포저블에 text 참조를 할당한다.
        // Button 컴포저블 botton에 연결한다.
        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
        })
    }
}

@Preview
@Composable
fun ConstraintLayoutContentPreview() {
    ConstraintLayoutContent()
}

Button과 Text 컴포저블을 선언하고 참조를 생성한 뒤 할당하였다. 각 컴포저블에 대해 어디에 연결되어야 하는지 linkTo 함수를 사용하여 연결하였다. margin을 부여하여 간격도 띄울 수 있다.

Codelab_Jetpack_Compose_layout_029

만약 버튼 하단의 텍스트를 레이아웃의 정중앙에 오도록 하려면 start와 end를 parent의 가장자리로 설정하는 centerHorizontallyTo 함수를 사용하면 된다.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        ... // 참조 생성과 버튼은 이전과 동일

        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
            // ConstraintLayout의 수평 중앙으로 오게끔 설정
            centerHorizontallyTo(parent)
        })
    }
}

마치 뷰 기반 xml에서 ConstraitnLayout을 사용했을 때와 활용법이 비슷하다. 다만, UI 요소끼리 연결하는 방식이 좀 더 programmatic하다.

Codelab_Jetpack_Compose_layout_030

Helper

ConstraintLayout의 더 나은 활용을 위해 사용하는 가이드라인, 배리어, 체인 등을 지원한다.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        val (button1, button2, text) = createRefs()

        Button(
            onClick = { /* Do something */ },
            modifier = Modifier.constrainAs(button1) {
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button 1")
        }

        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button1.bottom, margin = 16.dp)
            centerAround(button1.end)
        })
        // 배리어를 생성함
        val barrier = createEndBarrier(button1, text)
        Button(
            onClick = { /* Do something */ },
            modifier = Modifier.constrainAs(button2) {
                top.linkTo(parent.top, margin = 16.dp)
                start.linkTo(barrier)
            }
        ) {
            Text("Button 2")
        }
    }
}

버튼 두 개를 선언하여 상단에 링크를 연결한다. 텍스트를 선언하여 좌측 버튼과 연결을 수행하되 중앙점을 좌측 버튼의 끝부분에 위치하도록 설정한다. 이후 텍스트와 버튼을 묶어 배리어를 생성한 뒤 우측 버튼의 시작점을 배리어로 설정하면 아래와 같이 출력된다.

Codelab_Jetpack_Compose_layout_031

참고할 점으로 배리어 등 다른 Helper들은 constraintAs 함수 내부에서 선언할 수 없으며 ConstraintLayout 본문에서만 선언할 수 있다. linkTo 함수를 사용하여 마치 레이아웃에 연결하는 것처럼 Helper와 컴포저블을 연결할 수 있다.

측정 기준 커스텀

컴포저블에 할당된 텍스트 길이가 지나치게 긴 경우 화면 밖으로 빠져나와 보이지 않는 경우가 있다. 이 문제를 해결하기 위해 콘텐츠 출력 범위를 래핑하는 데 필요한 크기를 조정하도록 기능을 제공한다.

@Composable
fun LargeConstraintLayout() {
    ConstraintLayout {
        val text = createRef()

        val guideline = createGuidelineFromStart(fraction = 0.5f)
        Text(
            "This is a very very very very very very very long text",
            Modifier.constrainAs(text) {
                linkTo(start = guideline, end = parent.end)
            }
        )
    }
}

@Preview
@Composable
fun LargeConstraintLayoutPreview() {
    LayoutsCodelabTheme {
        LargeConstraintLayout()
    }
}

Codelab_Jetpack_Compose_layout_032

위 그림처럼 화면의 절반을 분할하는 가이드라인을 생성한 뒤 가이드라인 우측부터 출력하도록 작성하였다. 그 결과, 텍스트가 너무 길어 화면 밖으로 나가버렸다. 화면 밖으로 출력될 텍스트들을 줄바꿈하여 화면 안으로 집어넣고자 한다. 해당 동작을 위해 Text 컴포저블의 Modifier 중 width 동작을 다음과 같이 변경하자.

@Composable
fun LargeConstraintLayout() {
    ConstraintLayout {
        val text = createRef()

        val guideline = createGuidelineFromStart(0.5f)
        Text(
            "This is a very very very very very very very long text",
            Modifier.constrainAs(text) {
                linkTo(guideline, parent.end)
                width = Dimension.preferredWrapContent
            }
        )
    }
}

Codelab_Jetpack_Compose_layout_033

Dimension을 사용하여 그림과 같이 줄바꿈을 수행하였다. Dimension 동작에 대해 정리해보자.

  • preferredWrapContent : 레이아웃이 해당 레이아웃 측정 기준의 constraint를 적용하는 wrap 컨텐츠라고 설명한다. 즉, 레이아웃을 내부 공간에 맞춰서 wrap content로 설정함을 의미한다.
  • wrapContent : 제약 조건(constriant)에서 허용하지 않더라도 내부 공간과는 무관하게 컨텐츠를 감쌀 수 있는만큼의 크기를 갖는다.
  • fillToConstraints : constraint에 선언된 값만큼을 채운다.
  • preferredValue : 레이아웃이 constraint 내부 공간에서 선언된 값(dp)만큼의 크기를 갖는다.
  • value : 레이아웃에 constraint와는 상관없이 고정 값(dp)을 적용한다.

특정 Dimension 동작은 강제 적용될 수 있다.

width = Dimension.preferredWrapContent.atLeast(100.dp)

분리된(Decoupled) API

앞에서는 ContraintLayout 내부의 Composable들에 대해 각기 다른 컴포저블과의 관계를 직접 자신들의 Modifier로 정의하였다. 좀 더 나아가서 컴포저블에 참조 정도만 입력하고 참조의 생성 및 constraint 설정은 다른 곳에서 이루어지게끔 작성할 수 있다.

@Composable
fun DecoupledConstraintLayout() {
    BoxWithConstraints {
        val constraints = if (maxWidth < maxHeight) {
            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)
        }
    }
}

DecoupledConstraintLayout 컴포저블 함수에서 decoupledConstraints함수를 사용하여 레퍼런스를 생성하고 각 레퍼런스에 할당될 constraint들을 정의한다. 이후 ConstraintLayout는 앞서 생성했던 constraint를 매개변수로 받고 내부에 정의한 컴포저블에 레퍼런스를 할당하여 컴포저블에 constraint가 적용될 수 있도록 한다.

참고자료

  1. Google Codelab - Jetpack Compose Layout #10 제약 조건 레이아웃

+ Recent posts