개발 공부/안드로이드

[Compose] 컴포즈 공부하기5 - ConstraintLayout 1

yong_DD 2023. 9. 6. 16:35

xml을 사용해서 화면을 만들 때 가장 많이 사용하던 것이 LinearLayout, ConstraintLayout 이였다.

LinearLayout은 Row, Column 을 사용하면 되지만 ConstraintLayout 은?

아래와 같이 추가하면 사용할 수 있다!

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

최신 버전 확인

 

 

시작하기 전에 알아야 할게 있다.

ConstraintLayout은 상대 위치에 따라 위치와 크기를 지정하기 때문에 xml에서는 View의 id 값을 이용해 해당 뷰의 위치를 지정해서 사용했는데 id값이 없는 compose에서는 어떻게 처리할까?

 

바로 createRefs()createRefFor()를 사용해서 처리할 수 있다.

 

 

createRefs()

Modifier.constraintAs의 일부로 ConstraintLayout 내 레이아웃에 할당해야 하는 여러 개의 ConstraintLayoutReference를 만드는 편리한 방법입니다. 하나의 참조만 만들려면 createRef를 참조하십시오.

ConstrainLayout.kt를 보면 위와 같이 설명이 되어 있다.

하나의 참조라는 말을 집중해 볼 수 있다.

@Stable
fun createRefs() =
        referencesObject ?: ConstrainedLayoutReferences().also { referencesObject = it }
private var referencesObject: ConstrainedLayoutReferences? = null

코드를 보면 실제로 여러 개의 ConstrainedLayoutReference를 반환하고 있다.

[코드1]

 ConstraintLayout(modifier = Modifier.size(60.dp)) {
 
        val (box1, box2, box3, box4)  = createRefs()

        Box(
            modifier = Modifier
                .size(10.dp)
                .background(Color.Red)
                .constrainAs(box1) {
                    start.linkTo(parent.start)
                    top.linkTo(parent.top)
                }
        )
        Box(
            modifier = Modifier
                .size(10.dp)
                .background(Color.Yellow)
                .constrainAs(box2) {
                    top.linkTo(parent.top)
                    end.linkTo(parent.end)
                }
        )
        Box(
            modifier = Modifier
                .size(10.dp)
                .background(Color.Blue)
                .constrainAs(box3) {
                    bottom.linkTo(parent.bottom)
                    start.linkTo(parent.start)
                }
        )
        Box(
            modifier = Modifier
                .size(10.dp)
                .background(Color.Green)
                .constrainAs(box4) {
                    bottom.linkTo(parent.bottom)
                    end.linkTo(parent.end)
                }
        )
}

결과 사진

60dp의 사이즈를 만들어서 각 모퉁이에 box1, box2, box3, bo4를 배치하게 해두었다.

constrainAs는 ConstrainedLayoutReference가 들어가게 되어있고 이것을 createRef를 통해 한 줄로 만들 수가 있다.

  val (box1, box2, box3, box4)  = createRefs()
@Stable
fun Modifier.constrainAs(
    ref: ConstrainedLayoutReference,
    constrainBlock: ConstrainScope.() -> Unit
) = this.then(ConstrainAsModifier(ref, constrainBlock))

constrainAs는 이 ConstrainedLayoutReference를 통해 위치(anchor)를 지정할 수 있게 된다.

* ConstrainedLayoutReference는 ConstrainLayoutBaseScope.kt 내에 있다.

@Stable
class ConstrainedLayoutReference(val id: Any) {
    /**
     * The start anchor of this layout. Represents left in LTR layout direction, or right in RTL.
     */
    @Stable
    val start = ConstraintLayoutBaseScope.VerticalAnchor(id, -2)

    /**
     * The left anchor of this layout.
     */
    @Stable
    val absoluteLeft = ConstraintLayoutBaseScope.VerticalAnchor(id, 0)

    /**
     * The top anchor of this layout.
     */
    @Stable
    val top = ConstraintLayoutBaseScope.HorizontalAnchor(id, 0)

    /**
     * The end anchor of this layout. Represents right in LTR layout direction, or left in RTL.
     */
    @Stable
    val end = ConstraintLayoutBaseScope.VerticalAnchor(id, -1)

    /**
     * The right anchor of this layout.
     */
    @Stable
    val absoluteRight = ConstraintLayoutBaseScope.VerticalAnchor(id, 1)

    /**
     * The bottom anchor of this layout.
     */
    @Stable
    val bottom = ConstraintLayoutBaseScope.HorizontalAnchor(id, 1)

    /**
     * The baseline anchor of this layout.
     */
    @Stable
    val baseline = ConstraintLayoutBaseScope.BaselineAnchor(id)
}

 

이 위치 값은 또 linkTo를 통해 값을 지정해 줄 수 있다.

* linkTo는 VerticalAnchorable, HorizontalAnchorable, BaseLineAnchorable의 메서드이다.

/**
 * Represents a vertical side of a layout (i.e start and end) that can be anchored using
 * [linkTo] in their `Modifier.constrainAs` blocks.
 */
interface VerticalAnchorable {
    /**
     * Adds a link towards a [ConstraintLayoutBaseScope.VerticalAnchor].
     */
    fun linkTo(
        anchor: ConstraintLayoutBaseScope.VerticalAnchor,
        margin: Dp = 0.dp,
        goneMargin: Dp = 0.dp
    )
}

/**
 * Represents a horizontal side of a layout (i.e top and bottom) that can be anchored using
 * [linkTo] in their `Modifier.constrainAs` blocks.
 */
interface HorizontalAnchorable {
    /**
     * Adds a link towards a [ConstraintLayoutBaseScope.HorizontalAnchor].
     */
    fun linkTo(
        anchor: ConstraintLayoutBaseScope.HorizontalAnchor,
        margin: Dp = 0.dp,
        goneMargin: Dp = 0.dp
    )
}

/**
 * Represents the [FirstBaseline] of a layout that can be anchored
 * using [linkTo] in their `Modifier.constrainAs` blocks.
 */
interface BaselineAnchorable {
    /**
     * Adds a link towards a [ConstraintLayoutBaseScope.BaselineAnchor].
     */
    fun linkTo(
        anchor: ConstraintLayoutBaseScope.BaselineAnchor,
        margin: Dp = 0.dp,
        goneMargin: Dp = 0.dp
    )
}

 

그래서 코드 1번 처럼 사용할 수 있다.


+ 추가로 알아보기

그런데, linkTo를 보면 anchor외 에도 margin과 goneMargin을 넣을 수 있다.

이 2개의 값은 어떻게 다른걸까?

 

1. margin을 넣었을 때 

Box(
    modifier = Modifier
        .size(10.dp)
        .background(Color.Red)
        .constrainAs(box1) {
            start.linkTo(
                anchor = parent.start,
                margin = 10.dp)
            top.linkTo(parent.top)
        }
)

 

 

코드 1에서 box1의 start에 margin을 10dp 주었다.

생각한 대로 잘나오게 된다.

 

 

 

2. goneMargin을 넣었을 때 

Box(
    modifier = Modifier
        .size(10.dp)
        .background(Color.Red)
        .constrainAs(box1) {
            start.linkTo(
                anchor = parent.start,
                goneMargin = 10.dp)
            top.linkTo(parent.top)
        }
)

goneMargin으로 같은 값을 주었는데,

start가 10dp만큼 움직이지 않는다.

둘의 차이가 뭘까?

 

 

goneMargin은 말 그대로 gone일 때의 margin이다.

아래의 예시를 봐보자.

Box(
    modifier = Modifier
        .size(10.dp)
        .background(Color.Yellow)
        .constrainAs(box2) {
            start.linkTo(
                anchor = box1.end,
                margin = 10.dp
            )
        }
)

코드 1에서 box2를 box1 옆으로 붙여보았다.

그러면 좌측의 그림과 같이 나오게 된다.

 

 

 

 

그런데 빨간색이 gone으로 바뀐다면 어떻게 될까?

val box1Visible by remember { mutableStateOf(false)}

Box(
    modifier = Modifier
        .size(10.dp)
        .background(Color.Red)
        .constrainAs(box1) {
            start.linkTo(anchor = parent.start)
            top.linkTo(parent.top)
            visibility = if (box1Visible) Visibility.Visible else Visibility.Gone
        }
)

Box(
    modifier = Modifier
        .size(10.dp)
        .background(Color.Yellow)
        .constrainAs(box2) {
            start.linkTo(
                anchor = box1.end,
                margin = 10.dp,
                goneMargin = 20.dp
            )
            top.linkTo(parent.top)
        }
)

box1Visible이라는 값을 임의로 주고 box1에 visibility를 주었다.

box2에는 goneMargin을 20dp를 주었다.

box1이 gone이 되었기 때문에 margin이 아닌 goneMargin의 값을 start에 주게 되어 아까와 똑같은 위치가 된 것을 볼 수 있다!

 


linkTo 말고도 centerTo, centerVerticallyTo, centerHorizontallyTo를 사용하여 한 번에  anchor를 지정할 수 있다.

centerTo                                                                  centerVerticallTo                                                      centerHorizontallyTo

Box(
    modifier = Modifier
        .size(10.dp)
        .background(Color.Red)
        .constrainAs(box1) {
            centerTo(parent)
            // centerVerticallyTo(parent)
            // centerHorizontallyTo(parent)
        }
)

 

 

createRefFor()

ID가 있는 ConstraintLayout 요소에 해당하는 하나의 ConstraintLayout 참조를 만듭니다.
fun createRefFor(id: Any) = ConstrainedLayoutReference(id)

하나의 ConstrainedLayoutReference를 만들기 때문에 createRefs처럼 여러 개를 만들 수 없다.

 

[코드 2]

val constraintSet = ConstraintSet {
        val box1 = createRefFor("box1")
        val box2 = createRefFor("box2")
        val box3 = createRefFor("box3")
        val box4 = createRefFor("box4")

        constrain(box1) {
            start.linkTo(parent.start)
            top.linkTo(parent.top)
        }

        constrain(box2) {
            end.linkTo(parent.end)
            top.linkTo(parent.top)
        }

        constrain(box3) {
            bottom.linkTo(parent.bottom)
            start.linkTo(parent.start)
        }

        constrain(box4) {
            bottom.linkTo(parent.bottom)
            end.linkTo(parent.end)
        }
    }
    
    ConstraintLayout(
        constraintSet = constraintSet,
        modifier = Modifier.size(60.dp)
    ) {
        Box(
            modifier = Modifier
                .size(10.dp)
                .background(Color.Red)
                .layoutId("box1")
        )

        Box(
            modifier = Modifier
                .size(10.dp)
                .background(Color.Yellow)
                .layoutId("box2")
        )
        Box(
            modifier = Modifier
                .size(10.dp)
                .background(Color.Blue)
                .layoutId("box3")
        )
        Box(
            modifier = Modifier
                .size(10.dp)
                .background(Color.Green)
                .layoutId("box4")
        )
 }

코드 1과 동일하게 동작하지만 constraintSet과 createRefFor을 사용한 코드이다.

코드 1과 다르게 constraintSet을 사용하여 box1~4를 각각 createRefFor을 사용하고, 

constrainAs 대신 constrainSet 내부에 constrain을 사용하여 constrainAs의 값을 넣어준다.

ConstrainLayout안에 서는 layoutId를 사용하여 id 값을 줘 구분하게 된다.

 

fun ConstraintSet(description: ConstraintSetScope.() -> Unit): ConstraintSet =
    DslConstraintSet(description)
    
internal class DslConstraintSet constructor(
    val description: ConstraintSetScope.() -> Unit,
    override val extendFrom: ConstraintSet? = null
) : DerivedConstraintSet {
    override fun applyToState(state: State) {
        val scope = ConstraintSetScope()
        scope.description()
        scope.applyTo(state)
    }

    override fun override(name: String, value: Float): ConstraintSet {
        // nothing yet
        return this
    }
}

ConstraintSet은 DslConstrainSet class를 통해 ConstrainSetScope를 주게 되고 

@LayoutScopeMarker
class ConstraintSetScope internal constructor() : ConstraintLayoutBaseScope() {
    /**
     * Creates one [ConstrainedLayoutReference] corresponding to the [ConstraintLayout] element
     * with [id].
     */
    fun createRefFor(id: Any) = ConstrainedLayoutReference(id)

    /**
     * Specifies the constraints associated to the layout identified with [ref].
     */
    fun constrain(
        ref: ConstrainedLayoutReference,
        constrainBlock: ConstrainScope.() -> Unit
    ) = ConstrainScope(ref.id).apply {
        constrainBlock()
        this@ConstraintSetScope.tasks.addAll(this.tasks)
    }
}

ConstrainSetScope의 constrain 메서드를 통해 값을 주게 된다.

 

 

 

때에 따라서 선택해서 쓰면 될 것 같다..!!


 

참고 및 출처

 

 

Compose의 ConstraintLayout  |  Jetpack Compose  |  Android Developers

Compose의 ConstraintLayout 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. ConstraintLayout은 화면에 다른 컴포저블을 기준으로 컴포저블을 배치할 수 있는 레이아웃

developer.android.com