|

Android ConstraintLayout 완전 정리 (17) – ConstraintSet 사용법: 코드로 ConstraintLayout 제약 바꾸기

Android ConstraintLayout 완전 정리 (17) – ConstraintSet 사용법: 코드로 ConstraintLayout 제약 바꾸기
ConstraintSet은 ConstraintLayout의 제약 상태를 코드에서 바꾸는 도구입니다.

ConstraintSet 사용법은 ConstraintLayout을 XML로만 쓰다가 한 단계 더 들어갈 때 자주 만나는 주제입니다. 화면 상태가 바뀔 때 View 위치나 연결 기준도 함께 바꿔야 한다면, 그때 ConstraintSet을 검토할 수 있습니다.

이번 글에서는 ConstraintSet을 어렵게 API 목록으로 외우지 않고, clone으로 현재 제약을 가져오고, clear/connect로 바꾸고, applyTo로 반영하는 흐름으로 정리하겠습니다.


ConstraintSet 사용법 핵심 정리

  • ConstraintSet은 ConstraintLayout의 제약 상태를 코드에서 다루기 위한 객체다.
  • 기본 흐름은 clone -> clear/connect -> applyTo다.
  • connect의 margin 인자는 px 값이므로 예시에서는 dp(16)처럼 변환해서 넘긴다.
  • 기본 화면 구조는 XML에 두고, 상태별 차이만 Kotlin에서 바꾸는 편이 읽기 쉽다.
  • 기존 제약을 바꿀 때는 필요한 축을 clear한 뒤 새 연결을 connect한다.
  • 복잡한 전환 애니메이션까지 설계해야 한다면 MotionLayout을 별도로 검토한다.
ConstraintSet 사용법 clone clear connect applyTo 흐름 이미지
ConstraintSet의 기본 흐름은 clone -> clear/connect -> applyTo입니다.

여기서 중요한 점은 clear/connect 단계가 단순한 코드 장식이 아니라는 것입니다. 기존 연결을 그대로 둔 채 새 연결만 추가하면 의도가 흐려질 수 있으므로, 상태가 바뀌는 축을 먼저 정리하고 새 제약을 연결하는 순서가 읽기 쉽습니다.


ConstraintSet은 왜 필요한가

ConstraintLayout은 XML만으로도 대부분의 정적 배치를 만들 수 있습니다. 하지만 화면이 한 상태에만 머무르지 않는다면 이야기가 달라집니다. 버튼이 아래쪽에 있다가 제목 아래로 올라오거나, 숨겨진 패널이 열릴 때 본문 영역 기준이 바뀌는 식의 요구사항이 생깁니다.

이때 레이아웃 XML을 여러 개로 나누는 방법도 있지만, 변화가 비교적 단순하다면 같은 ConstraintLayout 안에서 제약만 바꾸는 편이 더 직접적일 수 있습니다. ConstraintSet은 바로 이 상황에서 쓰는 도구입니다.

ConstraintSet으로 버튼 위치를 상태 A에서 상태 B로 바꾸는 이미지
상태가 바뀌면 View를 새로 만들기보다 제약 연결을 바꿔야 할 때가 있습니다.

기본 상태는 XML에 둔다

ConstraintSet을 쓴다고 해서 XML을 버리는 것은 아닙니다. 오히려 기본 상태는 XML에 두고, 상태가 바뀔 때 달라지는 연결만 Kotlin 코드에서 바꾸는 구성이 읽기 쉽습니다.

ConstraintLayout XML 기본 상태와 Kotlin ConstraintSet 변경 상태 역할 비교 이미지
기본 상태는 XML에 두고, 상태별 차이만 ConstraintSet으로 바꾸면 구조가 읽기 쉬워집니다.
<androidx.constraintlayout.widget.ConstraintLayout
    android:id="@+id/rootLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/titleText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/actionButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

이 XML은 기본 상태입니다. 화면이 처음 열렸을 때 버튼이 어디에 있는지, 제목이 어디에 붙는지 같은 기준은 XML에서 명확하게 볼 수 있어야 합니다.


clone으로 현재 제약을 가져온다

clone은 현재 ConstraintLayout의 제약 상태를 ConstraintSet으로 가져오는 단계입니다. 처음부터 모든 제약을 코드로 다시 쓰는 것이 아니라, 이미 정의된 XML 상태를 복사해 필요한 부분만 수정한다고 이해하면 쉽습니다.

import androidx.constraintlayout.widget.ConstraintSet

val set = ConstraintSet()
set.clone(binding.rootLayout)

이렇게 하면 rootLayout 안에 있는 View들의 현재 제약 정보가 set 안으로 들어옵니다. 이후에는 이 set을 수정하고 다시 레이아웃에 적용합니다.


clear와 connect로 제약을 바꾼다

connect는 새 제약을 연결하는 메서드입니다. 예를 들어 버튼의 top을 제목의 bottom에 붙이고 싶다면, 버튼의 TOP을 titleText의 BOTTOM에 연결합니다.

다만 기존에 bottom 제약이 남아 있는 상태에서 top 제약만 추가하면 의도가 흐려질 수 있습니다. 상태를 분명히 바꾸는 코드라면 먼저 필요 없는 축을 clear하고 새 제약을 연결하는 편이 안전합니다.

ConstraintSet clear 후 connect하는 흐름을 설명하는 비교 이미지
상태 전환에서는 기존 제약을 명시적으로 지운 뒤 새 제약을 연결하는 편이 읽기 쉽습니다.
val set = ConstraintSet()
set.clone(binding.rootLayout)

set.clear(R.id.actionButton, ConstraintSet.BOTTOM)
set.connect(
    R.id.actionButton,
    ConstraintSet.TOP,
    R.id.titleText,
    ConstraintSet.BOTTOM,
    dp(16)
)

set.applyTo(binding.rootLayout)

이 코드는 버튼의 기존 bottom 연결을 지운 뒤, 버튼의 top을 제목의 bottom에 다시 연결합니다. 마지막 applyTo를 호출해야 실제 ConstraintLayout에 변경 내용이 반영됩니다.

또 하나 조심할 점은 margin 값입니다. XML에서는 16dp처럼 쓰지만, connect의 margin 인자는 정수 px 값입니다. 그래서 예시처럼 dp(16) 형태로 변환해서 넘기는 편이 안전합니다.


applyTo는 마지막 적용 단계다

applyTo는 ConstraintSet에 담아둔 변경 사항을 실제 ConstraintLayout에 반영하는 단계입니다. clone과 connect만 해서는 화면이 바뀌지 않습니다. 변경을 끝낸 뒤 명확히 applyTo를 호출해야 합니다.

import androidx.constraintlayout.widget.ConstraintSet

private fun moveButtonBelowTitle() {
    val set = ConstraintSet()
    set.clone(binding.rootLayout)

    set.clear(R.id.actionButton, ConstraintSet.BOTTOM)
    set.connect(
        R.id.actionButton,
        ConstraintSet.TOP,
        R.id.titleText,
        ConstraintSet.BOTTOM,
        dp(16)
    )

    set.applyTo(binding.rootLayout)
}

private fun dp(value: Int): Int {
    return (value * resources.displayMetrics.density).toInt()
}

예시에서는 margin 값을 px로 넘기기 위해 간단한 dp() 함수를 함께 넣었습니다. 실제 프로젝트에서는 이미 쓰고 있는 dimension helper나 resources 값을 사용하면 됩니다.


예시 1: 버튼 위치를 상태에 따라 바꾸기

가장 단순한 예시는 버튼 위치 변경입니다. 처음에는 버튼이 parent bottom에 붙어 있다가, 특정 상태가 되면 titleText 아래로 이동해야 한다고 해보겠습니다.

ConstraintSet으로 버튼 위치를 상태 A에서 상태 B로 바꾸는 이미지
상태가 바뀌면 View를 새로 만들기보다 제약 연결을 바꿔야 할 때가 있습니다.

이때 핵심은 버튼 View를 새로 만들거나 parent를 바꾸는 것이 아닙니다. 버튼의 기존 bottom 연결을 지우고, top 연결을 새 기준으로 바꾸는 것입니다.


예시 2: 카드 펼침 상태 만들기

카드 UI에서는 접힌 상태와 펼친 상태가 달라질 수 있습니다. 펼친 상태에서는 description 영역이 보이고, summary 아래에 붙어야 합니다.

ConstraintSet으로 카드 접힘 펼침 상태의 제약을 바꾸는 이미지
카드가 펼쳐질 때 새 View를 추가하는 것이 아니라 필요한 제약을 다시 연결할 수 있습니다.
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible

fun expandCard() {
    val set = ConstraintSet()
    set.clone(binding.rootLayout)

    binding.descriptionText.isVisible = true

    set.clear(R.id.descriptionText, ConstraintSet.TOP)
    set.connect(
        R.id.descriptionText,
        ConstraintSet.TOP,
        R.id.summaryText,
        ConstraintSet.BOTTOM,
        dp(12)
    )

    set.applyTo(binding.rootLayout)
}

이 예시에서도 ConstraintSet이 하는 일은 상태별 제약을 다시 연결하는 것입니다. isVisible은 AndroidX Core KTX의 확장 프로퍼티이므로, 예시처럼 androidx.core.view.isVisible import가 필요합니다.

View의 표시 여부와 제약 연결을 함께 다루면, 접힘/펼침 상태를 더 명확하게 만들 수 있습니다.


예시 3: hidden panel이 열릴 때 content 기준 바꾸기

검색 필터나 옵션 패널처럼 평소에는 숨겨져 있다가 필요할 때 열리는 영역도 자주 나옵니다. 이때 panel만 보이게 하고 content 기준을 그대로 두면 영역이 겹치거나 여백이 어색해질 수 있습니다.

ConstraintSet hidden panel 표시 상태에서 content 제약 기준을 바꾸는 이미지
숨겨진 패널을 보이게 할 때는 주변 View의 연결 기준도 함께 확인해야 합니다.
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible

fun showFilterPanel() {
    val set = ConstraintSet()
    set.clone(binding.rootLayout)

    binding.filterPanel.isVisible = true

    set.clear(R.id.contentList, ConstraintSet.TOP)
    set.connect(
        R.id.contentList,
        ConstraintSet.TOP,
        R.id.filterPanel,
        ConstraintSet.BOTTOM,
        dp(16)
    )

    set.applyTo(binding.rootLayout)
}

상태 변경 UI에서는 어떤 View를 보이게 할지만 볼 것이 아니라, 주변 View의 기준선이 어디로 이동해야 하는지도 함께 확인해야 합니다. 패널이 보이면 content가 어디 아래에 붙어야 하는지까지 같은 흐름에서 확인해야 합니다.


0dp와 match constraint도 같이 조심해야 한다

ConstraintSet을 쓴다고 해서 ConstraintLayout의 기본 규칙이 사라지지는 않습니다. 공식 ConstraintLayout 문서는 0dp를 match constraints와 같은 의미로 설명합니다. 따라서 width나 height가 0dp인 View를 코드로 다룰 때도 양쪽 제약이 제대로 남아 있는지 확인해야 합니다.

예를 들어 content 영역의 height가 0dp라면 top과 bottom 기준이 모두 필요합니다. ConstraintSet으로 top만 바꾸다가 bottom 기준을 실수로 지우면, 크기 계산이 의도와 달라질 수 있습니다.


MotionLayout과는 어떻게 구분할까

ConstraintSet은 상태별 제약을 코드에서 바꾸는 데 초점을 둡니다. 반면 MotionLayout은 상태 전환과 애니메이션 자체를 설계하는 도구입니다. 버튼 위치를 조건에 따라 바꾸는 정도라면 ConstraintSet으로 충분할 수 있지만, 전환 과정의 움직임이 UI 경험의 핵심이라면 MotionLayout을 별도로 검토하는 편이 좋습니다.

이 시리즈에서는 MotionLayout을 다루지 않으므로, 이번 글은 애니메이션 설계가 아니라 상태별 제약 변경에만 집중합니다.


자주 하는 실수

ConstraintSet 사용법 디버깅 체크리스트 이미지
ConstraintSet 문제가 생기면 clone 대상, View id, clear 축, applyTo 순서로 확인합니다.
  • clone 대상이 ConstraintLayout이 아닌데도 코드를 그대로 작성한다.
  • View id를 잘못 넣어 connect가 기대한 View에 적용되지 않는다.
  • 기존 제약을 clear하지 않아 old/new 연결이 섞인다.
  • applyTo를 호출하지 않고 화면이 안 바뀐다고 판단한다.
  • connect margin에 dp 값을 px 변환 없이 그대로 넘긴다.
  • 0dp View의 반대쪽 제약까지 지워서 크기 계산이 깨진다.
  • 단순 상태 변경과 전환 애니메이션 설계를 구분하지 않는다.

한 번에 정리

  • ConstraintSet 사용법의 중심은 clone -> clear/connect -> applyTo다.
  • 기본 상태는 XML에 두고, 상태별 차이만 Kotlin에서 바꾼다.
  • connect는 새 제약을 연결하고, clear는 기존 제약을 비운다.
  • applyTo가 실제 ConstraintLayout에 변경을 반영한다.
  • connect의 margin은 px 값이므로 dp 변환을 거쳐 넘긴다.
  • 상태 변경에서는 View 표시 여부와 주변 View의 연결 기준을 함께 본다.
  • 복잡한 애니메이션 전환이 목적이면 MotionLayout을 따로 검토한다.

ConstraintSet은 XML을 대체하는 도구가 아니라, XML 기본 상태 위에서 상태별 제약 차이를 코드로 적용하는 도구입니다.


마무리

ConstraintSet은 처음에는 메서드 이름이 많아 보이지만, 실전에서 먼저 잡아야 할 흐름은 단순합니다. 현재 상태를 clone하고, 바꿀 제약을 clear/connect로 정리하고, 마지막에 applyTo로 반영합니다.

공식 기준은 Android Developers ConstraintLayout 가이드, AndroidX ConstraintSet API reference, AndroidX ConstraintLayout API reference를 확인했습니다. 다음 18편에서는 loading, empty, error처럼 상태별 레이아웃을 ConstraintLayout 안에서 안정적으로 설계하는 방법을 이어서 정리하겠습니다.

Android ConstraintLayout 완전 정리 이전 글 모음

Android ConstraintLayout 완전 정리 (1) – ConstraintLayout 기본 사용법: LinearLayout과 다른 제약 기반 배치 이해하기

Android ConstraintLayout 완전 정리 (2) – ConstraintLayout start end top bottom 제약 정리: View를 원하는 위치에 붙이는 법

Android ConstraintLayout 완전 정리 (3) – ConstraintLayout goneMargin과 margin 차이: View.GONE일 때 간격이 바뀌는 이유

Android ConstraintLayout 완전 정리 (4) – ConstraintLayout width height 정리: wrap_content, 0dp, match_constraint 차이

Android ConstraintLayout 완전 정리 (5) – ConstraintLayout match_constraint 완전 정리: 0dp가 남은 공간을 채우는 원리

Android ConstraintLayout 완전 정리 (6) – ConstraintLayout bias 사용법: horizontalBias와 verticalBias로 위치 조절하기

Android ConstraintLayout 완전 정리 (7) – ConstraintLayout baseline 정렬: TextView 글자 기준선을 맞추는 방법

Android ConstraintLayout 완전 정리 (8) – ConstraintLayout dimensionRatio 사용법: 1:1, 16:9 이미지 비율 맞추기

Android ConstraintLayout 완전 정리 (9) – ConstraintLayout percent min max 크기 정리: 화면 크기에 맞게 View 조절하기

Android ConstraintLayout 완전 정리 (10) – ConstraintLayout chain 사용법: spread, spread_inside, packed 차이

Android ConstraintLayout 완전 정리 (11) – ConstraintLayout chain weight와 bias: 같은 폭, 다른 비율 배치 만들기

Android ConstraintLayout 완전 정리 (12) – ConstraintLayout Guideline 사용법: percent 기준선으로 화면 나누기

Android ConstraintLayout 완전 정리 (13) – ConstraintLayout Barrier 사용법: 텍스트 길이에 따라 동적 기준선 만들기

Android ConstraintLayout 완전 정리 (14) – ConstraintLayout Group과 Placeholder: 여러 View 숨기기와 위치 바꾸기

Android ConstraintLayout 완전 정리 (15) – ConstraintLayout Flow 사용법: Chip과 태그를 자동 줄바꿈 배치하기

Android ConstraintLayout 완전 정리 (16) – ConstraintLayout Layer 사용법: 여러 View를 함께 이동, 회전, 확대하기

함께보면 좋은 글