ConstraintLayout은 컴포저블을 다른 컴포저블들에 관련지어 화면상에 배치 할 수 있게 하고, 여러개의 Row, Column 및 Box를 사용하는 것에 대한 대안이 된다. ConstraintLayout은 더 복잡한 정렬이 필요하거나, 더 큰 레이아웃들을 구현할 때 유용하다.

Note: View시스템에서 ConstraintLayout은 flat한 View계층을 갖는것이 성능상으로 더 좋기 때문에, 크고 복잡한 레이아웃을 만들 때 추천했었다. 하지만 Compose에서는 효율적으로 깊은 레이아웃 계층을 다룰수 있기 때문에, 이런 부분이 걱정거리는 아니다.

컴포즈 Constraint Layout 의존성은 다음과 같이 build.gradle에 추가 할 수 있다. 최신버전은 공식문서에서 확인할 수 있다.

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

컴포즈의 ConstraintLayout는 DSL로 동작한다.

  • createRefs()(또는 createRef())을 사용하여 참조를 할 수 있고 ConstraintLayout내에 있는 각 컴포저블은 연관된 참조를 갖는 것을 필요로 한다.
  • Constraint(제약조건)는 constrainAs라는 modifier를 사용하여 제공된다. 이는 매개변수로 참조를 취하고, 람다식의 몸체에서 제약조건을 명시 하도록 한다.
  • Constraints는 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 컴포저블 bottom에 제약조건을 설정한다.
        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
        })
    }
}

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

위 코드는 Button의 top 제약조건은 parent에 설정하고, margin을 16.dp 적용한다. 그리고 TextButton의 bottom부분에 제약조건을 설정하고, margin을 16.dp 설정한다.

만약 텍스트를 수평적으로 중심에 두기를 원한다면, centerHorizontallyTo 함수를 사용하여 Textstartend를 parent의 가장자리 부분에 설정하면 된다.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        ... // Same as before

        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
            // 텍스트를 ConstraintLayout 중앙에 정렬한다.
            centerHorizontallyTo(parent)
        })
    }
}

미리보기로 보면 다음과 같다.

ConstraintLayout의 사이즈는 컨텐츠를 감쌀만큼 가능한 작은 사이즈가 된다. 그렇기 때문에 Text가 parent 대신 Button의 중앙에 있는 것처럼 보인다. 만약 다른 크기 조절이 필요한 경우, 사이즈를 조절할 수 있는 modifiers(예를들면 fillMaxSize, size)를 Compose의 다른 레이아웃과 마찬가지로 ConstraintLayout 컴포저블에 적용할 수 있다.

Helper

DSL은 또한 guidline, barrier 및 chain을 생성을 지원한다. 예시를 보자.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        // ConstraintLayout내에 3가지 컴포저블들을 위한 레퍼런스들을 생성한다.
        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") 
        }
    }

button1 및 text 참조를 매개변수로 하여 barrier를 생성했다. 미리보기를 통해 확인하자.

  • barrier(및 다른 모든 helper들)는 ConstraintLayout 함수 몸체에서 생성될 수 있다. 하지만 constrainAs는 아니다.
  • linkTo는 guideline 및 barrier와 함께 같은 방식으로 레이아웃의 가장자리 부분에 제약조건을 설정할 수 있다.

Dimension 커스텀하기

기본적으로 ConstraintLayout 하위 요소들은 그들의 컨텐츠 사이즈를 감싸는데 필요한 정도의 사이즈를 선택하는 것을 허용하도록 되어있다. 예를 들면, 이것은 Text의 내용이 너무 길 때, 화면 경계 밖으로 나갈 수 있음을 의미한다.

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

분명히 사용가능한 공간내에서 텍스트를 개행하고 싶을 것이다. 이를 위해서는 text의 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
            }
        )
    }
}

미리보기로 보면 다음과 같다.

사용 가능한 Dimension 동작은 다음과 같다.

  • wrapContent – 컨텐츠를 감싸면서 동적으로 사이즈를 변화시킨다. 심지어 주어진 제약 조건을 위반하면서 사이즈를 변경할 수 있다.(화면밖으로 벗어날 수 있음)
  • preferredWrapContent – 기본적으로 wrapContent와 같지만, 주어진 제약조건내에서 컨텐츠를 감싼다. (화면 밖을 벗어나지 않음)
  • fillToConstraints – 레이아웃을 확장시켜 해당 dimension의 제약조건에 의해 규정된 공간을 채운다.
  • value -주어진 제약조건이 있음에도 레이아웃은 고정된 dp 값을 갖는다. (화면 밖을 벗어날 수 있음)
  • preferredValue – 주어진 제약조건을 잘 지키며, 고정된 dp값을 갖는다. (화면 밖을 벗어나지 않음)

또한 어떤 Dimension들은 강제될 수 있다.

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

Decoupled API

지금까진 예제에서 제약조건들은 컴포저블내에 적용된 modifier와 함께 inline으로 명시되어져 왔다. 하지만 레이아웃에 적용된 제약조건들을 분리시킬 때 몇가지 쓸만한 내용들이 있다. 일반적인 예는 화면 구성을 기반으로 제약 조건을 쉽게 변경하거나, 2개의 제약 조건 세트 간에 애니메이션을 적용하는 것이다.

이러한 경우에 ConstraintLayout을 다른 방법으로 사용할 수 있다.

  1. ConstraintSet을 매개변수로 ConstraintLayout에 전달한다.
  2. ConstraintSet에서 Modifier의 layoutId를 사용하여 생성된 참조를 컴포저블에 배정한다.

위의 첫 번째 ConstraintLayout 예제에 적용된 이 API 모양은 화면 너비에 최적화되어 다음과 같다.

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

미리보기로 보면 다음과 같다.

Note: Modifier.layoutId 함수는 tag가 없는 id만 단일 매개변수로 갖는 메서드를 사용해야 한다.

카테고리: Compose

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.