|

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

Android ConstraintLayout 완전 정리 (11) – ConstraintLayout chain weight와 bias: 같은 폭, 다른 비율 배치 만들기
ConstraintLayout chain weight는 XML 코드와 실제 배치 결과를 같이 볼 때 이해가 빨라집니다.

ConstraintLayout chain weight는 10편에서 본 chain 기본을 실제 화면 비율 배치로 확장할 때 꼭 만나는 개념입니다. 버튼 두 개를 같은 폭으로 만들거나, 가운데 콘텐츠 영역만 더 넓게 만들고 싶을 때 단순히 margin을 조절하는 방식으로는 한계가 있습니다.

이번 글에서는 chain weight가 언제 동작하고, 왜 0dp와 함께 봐야 하는지부터 정리합니다. 그리고 1:1 버튼, 1:2:1 영역 분할, 같은 폭 탭 UI, packed bias와의 차이까지 XML 예시와 이미지로 비교하겠습니다.


먼저 한 줄로 정리

  • chain weight는 chain 안의 0dp View들이 남은 공간을 나눠 갖는 비율입니다.
  • wrap_content View에 weight만 붙여도 폭이 자동으로 비율 배치된다고 이해하면 안 됩니다.
  • 같은 폭이 필요하면 각 View를 0dp로 두고 같은 weight를 줍니다.
  • 1:2:1처럼 다른 비율이 필요하면 각 View의 weight 값을 다르게 둡니다.
  • packed bias는 weight와 다릅니다. packed bias는 묶음의 위치를 옮기고, weight는 0dp View의 크기를 나눕니다.
ConstraintLayout chain weight 0dp와 wrap_content 차이 이미지
chain weight는 wrap_content 간격 조절이 아니라 0dp View의 남은 공간 분배 규칙입니다.

ConstraintLayout chain weight가 동작하는 조건

공식 문서 기준으로 weighted chain은 spread 또는 spread_inside chain에서 MATCH_CONSTRAINT를 쓰는 요소들이 남은 공간을 나눠 쓰는 방식입니다. XML에서는 보통 android:layout_width="0dp" 또는 android:layout_height="0dp"로 표현합니다.

여기서 중요한 점은 weight가 View 사이 간격을 직접 조절하는 값이 아니라는 점입니다. chain 구조가 있고, 해당 방향 크기가 0dp인 View가 있을 때, 그 View들이 사용할 수 있는 공간을 어떤 비율로 나눌지 정하는 값입니다.

1:1 버튼 두 개 만들기

ConstraintLayout chain weight 1대1 버튼 폭 예시
같은 weight를 주면 사용 가능한 폭을 같은 비율로 나눌 수 있습니다.

가장 쉬운 예시는 하단 버튼 두 개입니다. 텍스트 길이가 달라도 두 버튼의 클릭 영역은 같은 폭이어야 하는 경우가 많습니다. 이때 두 버튼을 chain으로 묶고, 둘 다 width를 0dp로 둔 뒤 같은 weight를 주면 됩니다.

1:1 버튼 XML 예시

<Button
    android:id="@+id/primaryButton"
    android:layout_width="0dp"
    android:layout_height="48dp"
    android:text="확인"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toStartOf="@id/secondaryButton"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="spread"
    app:layout_constraintHorizontal_weight="1" />

<Button
    android:id="@+id/secondaryButton"
    android:layout_width="0dp"
    android:layout_height="48dp"
    android:text="취소"
    app:layout_constraintStart_toEndOf="@id/primaryButton"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="@id/primaryButton"
    app:layout_constraintHorizontal_weight="1" />

이 예시에서 두 버튼은 모두 가로 방향 크기가 0dp입니다. 그래서 내용의 글자 수가 아니라 chain 안에서 사용할 수 있는 공간을 기준으로 폭이 계산됩니다. 둘 다 weight가 1이므로 결과는 1:1에 가깝게 나뉩니다.


1:2:1 영역 분할 만들기

ConstraintLayout chain weight 1대2대1 영역 분할 예시
1:2:1 weight는 가운데 영역을 양쪽보다 두 배 넓게 만듭니다.

같은 폭이 아니라 가운데 영역을 더 넓게 만들고 싶다면 weight 값을 다르게 주면 됩니다. 예를 들어 왼쪽, 가운데, 오른쪽 영역을 1:2:1로 만들면 가운데 영역이 양쪽보다 더 넓은 공간을 차지합니다.

1:2:1 XML 예시

<TextView
    android:id="@+id/navArea"
    android:layout_width="0dp"
    android:layout_height="56dp"
    android:text="Nav"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toStartOf="@id/contentArea"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="spread"
    app:layout_constraintHorizontal_weight="1" />

<TextView
    android:id="@+id/contentArea"
    android:layout_width="0dp"
    android:layout_height="56dp"
    android:text="Content"
    app:layout_constraintStart_toEndOf="@id/navArea"
    app:layout_constraintEnd_toStartOf="@id/sideArea"
    app:layout_constraintTop_toTopOf="@id/navArea"
    app:layout_constraintHorizontal_weight="2" />

<TextView
    android:id="@+id/sideArea"
    android:layout_width="0dp"
    android:layout_height="56dp"
    android:text="Side"
    app:layout_constraintStart_toEndOf="@id/contentArea"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="@id/navArea"
    app:layout_constraintHorizontal_weight="1" />

이 구조는 LinearLayout의 layout_weight와 비슷하게 느껴질 수 있습니다. 다만 ConstraintLayout에서는 먼저 chain이 성립해야 하고, weight가 적용될 View의 해당 방향 크기가 0dp여야 한다는 점을 같이 봐야 합니다.


같은 폭 탭 UI에 chain weight 쓰기

ConstraintLayout chain weight 같은 폭 탭 UI 예시
탭 UI에서는 텍스트 길이보다 클릭 영역의 균등성이 더 중요할 때가 많습니다.

탭 UI에서는 텍스트 길이가 서로 달라도 각 탭의 클릭 영역이 같아야 자연스러운 경우가 많습니다. , 검색, 내 정보처럼 글자 수가 달라도 세 탭을 모두 0dp와 weight 1로 두면 균등한 탭 영역을 만들 수 있습니다.

<TextView
    android:id="@+id/homeTab"
    android:layout_width="0dp"
    android:layout_height="48dp"
    android:gravity="center"
    android:text=""
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toStartOf="@id/searchTab"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_weight="1" />

<TextView
    android:id="@+id/searchTab"
    android:layout_width="0dp"
    android:layout_height="48dp"
    android:gravity="center"
    android:text="검색"
    app:layout_constraintStart_toEndOf="@id/homeTab"
    app:layout_constraintEnd_toStartOf="@id/profileTab"
    app:layout_constraintTop_toTopOf="@id/homeTab"
    app:layout_constraintHorizontal_weight="1" />

<TextView
    android:id="@+id/profileTab"
    android:layout_width="0dp"
    android:layout_height="48dp"
    android:gravity="center"
    android:text="내 정보"
    app:layout_constraintStart_toEndOf="@id/searchTab"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="@id/homeTab"
    app:layout_constraintHorizontal_weight="1" />

이때 중요한 것은 텍스트를 가운데 정렬하는 android:gravity="center"와 폭을 나누는 weight를 구분해서 보는 것입니다. gravity는 탭 안의 글자 위치를 정하고, chain weight는 탭 자체의 폭을 정합니다.


weight가 안 먹는 가장 흔한 실수

ConstraintLayout chain weight가 안 먹는 width 실수 이미지
weight가 기대대로 안 보이면 먼저 chain 안의 View width가 0dp인지 확인해야 합니다.

weight가 안 먹는다고 느껴질 때는 먼저 width 또는 height를 확인해야 합니다. 가로 chain weight라면 android:layout_width="0dp"가 필요하고, 세로 chain weight라면 android:layout_height="0dp"가 필요합니다.

실패 예시

<TextView
    android:id="@+id/left"
    android:layout_width="wrap_content"
    android:layout_height="48dp"
    android:text="A"
    app:layout_constraintHorizontal_weight="1"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toStartOf="@id/right" />

<TextView
    android:id="@+id/right"
    android:layout_width="wrap_content"
    android:layout_height="48dp"
    android:text="Long text"
    app:layout_constraintHorizontal_weight="1"
    app:layout_constraintStart_toEndOf="@id/left"
    app:layout_constraintEnd_toEndOf="parent" />

이 XML은 weight 속성이 보여도 두 View가 wrap_content이기 때문에 기대한 균등 폭 배치가 나오지 않을 수 있습니다. 글자 길이에 따라 View 폭이 먼저 잡히기 때문입니다.

수정 예시

<TextView
    android:id="@+id/left"
    android:layout_width="0dp"
    android:layout_height="48dp"
    android:gravity="center"
    android:text="A"
    app:layout_constraintHorizontal_weight="1"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toStartOf="@id/right" />

<TextView
    android:id="@+id/right"
    android:layout_width="0dp"
    android:layout_height="48dp"
    android:gravity="center"
    android:text="Long text"
    app:layout_constraintHorizontal_weight="1"
    app:layout_constraintStart_toEndOf="@id/left"
    app:layout_constraintEnd_toEndOf="parent" />

수정된 예시는 두 View 모두 width가 0dp입니다. 이제 텍스트 길이보다 chain 안의 사용 가능한 공간과 weight 값이 폭 계산의 기준이 됩니다.


margin이 있으면 weight 결과도 달라진다

ConstraintLayout chain weight margin 계산 이미지
margin이 있으면 weight가 나눌 수 있는 남은 공간도 줄어듭니다.

10편에서 본 것처럼 chain은 margin을 무시하지 않습니다. weight를 사용할 때도 마찬가지입니다. View 사이에 margin이 있으면 그 margin이 먼저 공간을 차지하고, 남은 공간 안에서 0dp View들이 weight 비율대로 나뉩니다.

그래서 1:1 weight를 줬는데도 화면에서 꽉 찬 두 덩어리처럼 보이지 않는다면 margin을 확인해야 합니다. 이건 오류라기보다, margin까지 포함한 실제 배치 결과입니다.


packed bias와 chain weight는 무엇이 다를까

ConstraintLayout packed bias와 chain weight 차이 이미지
packed bias는 그룹 위치, weight는 MATCH_CONSTRAINT View의 크기 분배 문제입니다.

10편에서 packed chain은 요소들을 묶은 뒤 bias로 묶음 위치를 조절할 수 있다고 설명했습니다. 하지만 packed bias와 weight는 목적이 다릅니다. packed bias는 View들의 묶음이 parent 안에서 어느 쪽에 가까운지를 바꾸고, weight는 0dp View들이 차지할 크기 비율을 바꿉니다.

  • packed bias: View 묶음 전체의 위치를 조절한다.
  • chain weight: 0dp View들의 폭 또는 높이를 비율로 나눈다.
  • margin: weight가 나눌 수 있는 남은 공간 자체를 줄인다.
  • gravity: View 안의 텍스트나 내용 위치를 조절한다.

세로 chain weight도 같은 원리다

본문에서는 가로 chain을 기준으로 설명했지만 세로 chain도 원리는 같습니다. 세로 방향에서는 android:layout_height="0dp"app:layout_constraintVertical_weight를 사용합니다.

<View
    android:id="@+id/topPanel"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toTopOf="@id/bottomPanel"
    app:layout_constraintVertical_weight="1" />

<View
    android:id="@+id/bottomPanel"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:layout_constraintTop_toBottomOf="@id/topPanel"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintVertical_weight="2" />

가로에서는 width 0dp와 horizontal weight를 보고, 세로에서는 height 0dp와 vertical weight를 보면 됩니다. 축만 다르고 읽는 순서는 같습니다.


문제가 생겼을 때 확인 순서

ConstraintLayout chain weight 확인 순서 이미지
weight가 이상할 때는 chain 구조부터 bias까지 순서대로 좁혀 가는 편이 빠릅니다.

chain weight 문제는 속성 하나만 보고 해결하기 어렵습니다. 실제로는 chain 구조, 0dp, weight, margin, bias가 함께 결과를 만듭니다. 그래서 아래 순서대로 확인하면 원인을 훨씬 빨리 좁힐 수 있습니다.

  • 양 끝 View가 parent 또는 같은 축의 기준에 연결되어 있는가
  • 중간 View들이 서로 양방향으로 연결되어 chain이 성립하는가
  • weight를 줄 View의 해당 방향 크기가 0dp인가
  • 모든 0dp View에 의도한 weight 값이 들어갔는가
  • margin이 기대보다 큰 공간을 먼저 차지하고 있지는 않은가
  • packed chain을 쓰면서 weight처럼 기대하고 있지는 않은가

한 번에 정리

  • ConstraintLayout chain weight는 chain 안의 0dp View 크기 분배 규칙이다.
  • 가로 비율 배치는 width 0dp와 layout_constraintHorizontal_weight를 함께 본다.
  • 세로 비율 배치는 height 0dp와 layout_constraintVertical_weight를 함께 본다.
  • 같은 폭은 같은 weight, 다른 비율은 다른 weight로 만든다.
  • packed bias는 묶음 위치 조절이고, weight는 크기 분배다.

즉, ConstraintLayout chain weight를 이해할 때는 weight 속성만 외우지 말고 chain 구조와 0dp가 먼저 준비되어 있는지를 확인해야 합니다.


마무리

ConstraintLayout chain weight는 실무에서 같은 폭 버튼, 균등 탭, 비율이 다른 콘텐츠 영역을 만들 때 유용합니다. 하지만 weight는 단독으로 동작하는 마법 속성이 아니라 chain, 0dp, margin과 함께 읽어야 하는 크기 계산 규칙입니다.

공식 기준은 Android Developers ConstraintLayout 가이드AndroidX ConstraintLayout 레퍼런스를 확인했습니다. 다음 12편에서는 보이지 않는 기준선인 Guideline으로 화면을 percent 기준으로 나누는 방법을 이어서 보겠습니다.

함께보면 좋은 글