Jetpack Compose ConstraintLayout

Compose에 대한 내용을 언급하던 중에 ConstraintLayout 이란 항목이 갑자기 튀어나옵니다. 

한껏 Column, Row, Box란 view group 역할을 하는 composable을 한껏 제공해 주고 나서 말이죠. 

게다가 ConstraintLayout은 기존 View group 방식에서 view group의 중첩, 다시 말해 layout의 depth가 깊어지는 걸 막기 위해 2017년에 새롭게 등장한 layout입니다.

CustomLayout에서 이미 봤듯이 Compose에서는 measure가 한 번만 가능하기 때문에 View의 depth가 깊어지더라도 문제가 발생하지 않습니다. 

다만 Compose에서의 ConstraintLayout은 뷰의 구조가 복잡할 때, 여러 view들 간의 상호관계에 따른 배치를 좀 더 쉽게 가능하도록 하는 목적으로 사용됩니다.

추후 설명할 Compose의 constraint의 사용법은 기존 view system에서 사용하던 ConstraintLayout과 거의 동일합니다. (코드로 표현하는 방법이 다를 뿐) 다만 목적은 "기존의 view system에서 사용된 것과는 다르다" 정도만 이해하고 있으면 이번 포스트팅은 어렵지 않게 클리어 가능할 것으로 생각됩니다.

이 글은 Android developer 공식 사이트에서 제공하는 문서를 기반으로 의역, 번역하였습니다.



Gradle dependency

Constraint layout을 compose에서 사용하기 위해서는  따로 dependency가 필요합니다. 여기서 주의해야 할 점은 기본 compose의 버전을 따라가지 않는다는 겁니다.

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


현재 최신 compose 버전은 1.6.7이며, 이 constraintlayout (1.0.1)을 쓰려면 kotlin은 2.0.0을 써야 합니다

정리하면 기본 compose 버전과는 다르게 진행된다는 것과 constraintlayout을 사용하기 위해서 kotlin 버전도 호환성 있는 버전으로 맞춰서 사용해야 한다는 점을 유념해 두어야 합니다.


ConstraintLayout의 기본적인 사용

Constraint layout에서 view들 간의 관계는 ID로 표현합니다. 

Compose에서는 ID를 createRef(), createRefs()로 생성합니다.

- createRefs() 또는 createRef()를 사용하여, ConstraintLayout 내에서 각 컴포저블에 대한 참조를 선언

- 이 과정은 xml에서 ConstraintLayout내에서의 참조를 위해 각 레이아웃에 id를 부여하는 작업과 유사


생성된 ID를 각 Compose에 부여하기 위 해어 constrainAs() modifier를 제공하며, lambda block안에서 다른 compose와의 관계를 정의합니다.

lambda block에서 관계를 정의할 때는 linkTo()를 이용합니다.

Compose에서도 parent를 사용할 수 있으며 ConstraintLayout을 나타냅니다.


이제 버튼을 하나 만들고, 그 아래 text를 배치하는 간단한 형태를 구현해 보겠습니다.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        // Creates references for the two composables
        // in the ConstraintLayout's body
        val (button1, 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)
        })

    }
}


먼저 각 compose에서 사용할 reference값 (기존 view system에서는 ID)를 생성합니다.

constrainAs()로 reference를 부여한 후에 해당 block안에서 linkTo로 관계를 정의합니다. 

이미 기존 view system의 constraint layout에 익숙한 개발자라면 표현방법이 다를 뿐 동일한 형태로 사용한다는 걸 알 수 있습니다. 따라서 이해하기가 다른 compose 섹션보다는 쉽습니다.


여기서 Text버튼이 쏠려 있으니 가운데 정렬을 위해서 centerHorizontallyTo()를 이용해 보겠습니다. 

이 function은 start와 end를 각각 parent의 양쪽 끝 edge로 선언하는 것과 동일한 효과를 갖습니다.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        ...

        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button1.bottom, margin = 16.dp)
            // Centers Text horizontally in the ConstraintLayout
            centerHorizontallyTo(parent)
        })

    }
}






parent의 중간에 위치하도록 Text를 설정했지만 결국 button1 기준으로 중간에 위치한것처럽 보입니다. 

이는 constratinlayout 자체가 기본적으로 본인이 가진 contents를 wrap 한 사이즈를 사용하기 때문입니다. 

다르게 말해 자기가 가진 자식 composables을 표현할 수 있는 최소한의 크기만을 사용합니다.

물론 전체 화면으로 확장해서 사용하기 위해 이미 다른 포스팅에서 여러 번 사용했던 modifier의 fillMaxSize 또는 size를 이용하면 됩니다.



GuideLine

가이드라인은 컴포저블을 위치하게 할 수 있는 보이지 않는 뷰입니다.

가이드라인은 float, offset, dp를 사용하여 크기/위치를 설정할 수 있습니다.


아래와 같은 방식으로 가이드라인을 선언 할 수 있습니다.


val guideLineFromTop5f = createGuidelineFromTop(0.5f)

val guideLineFromTop20dp = createGuidelineFromTop(20.dp)

val guideLineFromEnd20dp = createGuidelineFromEnd(20.dp)

각각, 부모의 Top에서 50%, 20dp, End에서 20dp 만큼 떨어진 부분에 가이드라인이 생성됩니다.


선언 된 가이드라인은 다른 컴포저블들을 ConstraintLayout 내에서 위치시키는데 이용될 수 있습니다.

@Composable
fun GuideConstraintLayoutContent() {
    ConstraintLayout (modifier = Modifier.size(200.dp)) {
        val (text1, text2) = createRefs()

        val guideLineFromTop5f = createGuidelineFromTop(0.5f)
        val guideLineFromTop20dp = createGuidelineFromTop(20.dp)
        val guideLineFromEnd20dp = createGuidelineFromEnd(20.dp)

        Text(
            "50% from top",
            modifier = Modifier.constrainAs(text1) {
                top.linkTo(guideLineFromTop5f)
            }
        )

        Text(
            "Top 20dp End 20dp",
            modifier = Modifier.constrainAs(text2) {
                top.linkTo(guideLineFromTop20dp)
                end.linkTo(guideLineFromEnd20dp)
            }
        )

    }
}





Barrier

Barrier는 가이드라인과 같이 보이지 않는 뷰로서, 다른 컴포저블을 위치시키는데 도움을 줍니다.


하지만 가이드라인이 부모를 기준으로 위치했다면 Barrier는 다른 컴포넌트를 기준으로 유동적으로 위치하게 할 수 있습니다.


아래는 text1, text2중 더 end가 긴 쪽에 가이드라인을 선언하는 코드입니다.


val endBarrier = createEndBarrier(text1, text2)

End 대신 Top, Start, Bottom등을 넣어서 원하는 위치에 배리어를 생성할 수 있습니다.


이를 통해 아래와 같은 뷰를 만들 수 있습니다.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        // Creates references for the three composables
        // in the ConstraintLayout's body
        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")
        }
    }
}



이번에는 reference를 세 개 만들고 버튼 두 개와 Text를 하나 추가 했습니다.

Button1의 위치를 기준으로 Text는 button1의 아래에 위치시키고, Text의 중간이 button1의 끝이 오는 부분에 위치시켰습니다. 추가적으로 button1과 text의 끝 부분에 barrier를 만들고 button2의 시작은 barrier부터 하도록 연결합니다.




barrier 또는 그 이외의 다른 helper들은 constraintAs 영역 안에서 생성할 수 없습니다. 

따라서 위 샘플 코드처럼 constraintAs의 lambda 영역 밖에서 선언해야 합니다.



Chain

기존 xml에서의 ConstraintLayout 안에서 여러 레이아웃을 app:layout_constraintVertical_chainStyle 등의 코드를 통해 체이닝하여 사용하였습니다.

이는 Jetpack Compose에서도 마찬가지로 사용 가능합니다.

createHorizontalChain(button1, button2, chainStyle = ChainStyle.Packed)

우선, createHorizontalChain()을 사용해 체인을 선언해야합니다. createVerticalChain()을 통해 세로로 체이닝 할 수도 있습니다.

chainStyle는 각 컴포저블을 어떤 형태로 체이닝할지 결정하는 요소입니다.

특히, ChainStyle.Packed()는 bias 설정을 통해 어느 한 쪽으로 쏠리게 할 수 있습니다.

아래는 전체 코드이며, ChainStyle에 따라 위의 결과 중 선택한 style에 따른 결과를 얻을 수 있습니다.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout (modifier = Modifier
        .fillMaxWidth()
        .height(200.dp)) {
        val (button1, button2, text) = createRefs()

        createHorizontalChain(button1, button2, chainStyle = ChainStyle.Packed) // SpreadInside, Spread, Packed

        Button(
            onClick = { },
            modifier = Modifier.padding(8.dp).constrainAs(button1) {
                top.linkTo(parent.top, margin = 8.dp)
            }
        ) {
            Text("Button 1")
        }

        Button(
            onClick = { },
            modifier = Modifier.padding(8.dp).constrainAs(button2) {
                top.linkTo(parent.top, margin = 8.dp)
            }
        ) {
            Text("Button 2")
        }

        Text(
            "Text",
            modifier = Modifier.constrainAs(text) {
                top.linkTo(button1.bottom, margin = 16.dp)
                centerHorizontallyTo(parent)
            }
        )
    }
}





댓글

이 블로그의 인기 게시물

Jetpack Compose Navigation 정리

아이랑스토리 어플리케이션 개인정보처리방침

Android WebView WebViewClient (웹뷰에서 일어나는 요청, 상태, 에러 등 다양한 상황) 재정의 사용