언제부턴가 Android Studio에서 ConstraintLayout 를 강력하게 밀고 있는 듯하다. New project를 만들면 기본적으로 생성되는 “Hello World!” 텍스트가 있는 액티비티도 ConstraintLayout으로 생성된다.

ConstraintLayout란

ConstraintLayout(컨스트레인트 레이아웃)은 위치와 크기 조절을 유연하게 할 수 있는 ViewGroup이다. 다양한 해상도를 지원하는 앱 작업을 할 때 상당히 편리하다.

아래 예제는 같은 코드로 작성한 xml 파일을 다른 기기(해상도)에서 실행한 것이다. 1) TextView, 2) ImageView, 3) Button의 세 가지 뷰로 구성되어 있다. 각 View마다 크기 및 위치 조건은 다음과 같이 주었다.

  • 이미지 : 원본 크기 맞춤, Parent의 수직 및 수평 중앙.
  • 텍스트 : 텍스트 크기 맞춤, 이미지의 왼쪽에 맞춰 정렬, 이미지의 8dp 위에 위치.
  • 버튼 : 이미지의 좌우 크기에 맞춰 크기 확장, 이미지ㅡParent의 20% 되는 높이에 위치

▲ Nexus 4 (768 x 1280)


▲ Pixel 2 XL (1440 x 2880)


▲ Nexus 10 (2560 x 1600)


뷰와 뷰 간의 관계가 절대적인 수치 값이 아닌 상대적인 비율로 지정되어 있기 때문에, 위처럼 다양한 해상도에서 원하는 레이아웃 모양대로 정렬하여 나타낼 수 있다.



ConstraintLayout의 주요 속성

이제 하나씩 살펴보자. app-res-layout-activity_main.xml 에서 xml 파일로 된 레이아웃 코드를 볼 수 있다. Design 탭에서 직접 뷰를 보면서 조정해도 된다. Design탭과 xml 탭에서 내용을 변경할 시 다른 한 쪽에서도 자동으로 변경된다.

<!-- activity_main.xml -->

<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:text="Hello Layout!"
        app:layout_constraintBottom_toTopOf="@+id/imageView"
        app:layout_constraintStart_toStartOf="@+id/imageView" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/simpsonize_nachoi" />

    <Button
        android:id="@+id/button"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="@+id/imageView"
        app:layout_constraintStart_toStartOf="@+id/imageView"
        app:layout_constraintTop_toBottomOf="@+id/imageView"
        app:layout_constraintVertical_bias="0.2" />

</android.support.constraint.ConstraintLayout>



특징(1) - width 와 height

각 뷰는 필수적으로 가로와 세로 높이를 나타내는 속성을 가지고 있어야 한다.

  • android:layout_width : 뷰의 가로 길이를 나타냄. (폭, 너비)
  • android:layout_height : 뷰의 세로 길이를 나타냄. (높이)


그리고 이 가로, 세로 속성은 3종류의 값을 가질 수 있다.

  • none : 화면의 비율에 상관 없이 고정 값을 갖는다. dp 단위로 표기한다.
  • wrap_content : 뷰가 차지하는 크기 만큼의 길이. Image의 경우, 특별히 dp값을 지정하지 않으면 원본 크기가 되며, TextView의 경우는 글자를 감싸는 크기가 된다.
  • match_constraint : 실제로 xml 파일에서는 "0dp"로 표기한다. 뷰의 가로 혹은 세로를 다른 곳에 고정시킨 후, 해당하는 영역을 모두 차지한다.

예제의 TextView나 ImageView의 width와 height 값 모두 wrap_content이다.

android:layout_width="wrap_content"
android:layout_height="wrap_content"


반면 Button의 width값은 match_constraint 이다. 0dp로 표시되며, 왼쪽과 오른쪽이 연결된 곳에 길이가 자동으로 맞춰진다. 만약 왼쪽과 오른쪽을 TextView에 연결했으면 TextView가 차지하는 영역 만큼이 버튼 크기가 되고, 레이아웃(Parent)에 연결했으면 화면을 꽉 채우는 크기가 된다.

android:layout_width="0dp"
android:layout_height="wrap_content"


특징(2) - constraint(A)_to(B)Of

xml 파일을 살펴보면 공통적으로 여러 개의 constraint(A)_to(B)Of 옵션을 가지고 있다. 말 그대로 상/하/좌/우를 다른 뷰의 어느 곳에 연결할 것인지 정해주는 옵션이다. constraint 를 정해주지 않으면 경고 표시가 발생하며, View를 다른 곳에 연결하지 않은 채 앱을 실행하면 View가 좌측 상단으로 이동해버린다. 그렇기 때문에 반드시 다른 View 혹은 Parent에 연결해야 한다.

ImageView는 상/하/좌/우가 각각 parent에 연결되어 있다.

app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

길이 값이 wrap_content이면서 4방향 모두 parent에 연결되어 있을 때 View는 예제와 같이 화면 중앙에 위치하게 된다. (만약 길이 값이 match_constraint인 경우에는 parent의 크기에 맞춰 이미지가 크기가 늘어난다.)

다음으로, TextView는 좌측이 ImageView의 좌측에, 하단이 ImageView의 상단에 각각 연결되어 있다.

app:layout_constraintBottom_toTopOf="@+id/imageView"
app:layout_constraintStart_toStartOf="@+id/imageView"

View의 id값을 통해 View를 찾고, 이를 기준으로 constraint 옵션이 생긴다. 하단에는 layout_marginBottom 값이 있기 때문에 8dp의 여백이 생긴다. 이 값이 없다면 TextView는 ImageView 바로 위에 위치하게 된다.

android:layout_marginBottom="8dp"


Button 에 적용된 constraint는 다음과 같다.

app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/imageView"
app:layout_constraintStart_toStartOf="@+id/imageView"
app:layout_constraintTop_toBottomOf="@+id/imageView"

좌우는 ImageView에, 상하는 각각 ImageView와 parent에 연결되었다. width가 0dp이기 때문에 ImageView의 width 길이와 같아진다. 만약 다른 곳에 연결한다면 Button의 크기도 달라지게 된다.


Button의 Start와 End를 ImageView, TextView, parent 의 좌우에 연결했을 때 match_constraint, 즉 android:layout_width="0dp" 값을 가질 때 각각 연결된 View에 따라 크기가 바뀐다. 추가로 marginStart, marginEnd 값인 8dp만큼 좌우에 여백이 생긴다.
Button의 Top과 Bottom은 ImageView와 parent에 각각 연결되었기 때문에 그 사이에 위치하게 된다. 여기서는 20% 비율의 높이에 위치하도록 설정되어 있다.


특징(3) - 비율에 따른 View 조절

  • 정사각형 이미지

    ImageView의 옵션 중 다음과 같은 옵션이 있다.

    app:layout_constraintDimensionRatio="1:1"
    

    이미지가 정사각형이 아닐 때에 위 옵션을 적용해준다면 1:1 비율로 설정된다. 자세한 것은 Layout Tips 포스트를 참고할 수 있다.

  • Bias (비율 조정)

    상하나 좌우를 연결했을 때, 이미지는 가운데에 위치하게 된다. 이때, bias 옵션을 주면 0~1 사이 옵션에서 중앙 (0.5)이 아닌 비율로 정렬이 가능하다. Button은 상하가 ImageView와 parent에 연결되어 있고, bias 값을 주지 않으면 기본적으로 중앙에 위치한다. 예제와 같이 bias 값을 조정하면 0~1 (0%~100%) 비율로 위치하게 된다.

    app:layout_constraintVertical_bias="0.2"  
    


    이제 버튼은 이미지와 맨 밑 사이의 공간에서 20%에 해당하는 곳에 위치하게 된다. 여러 해상도에 같은 비율을 적용해야 할 때 유용한 옵션이다.


그 밖의 참고사항

  • dependency

    이전에는 build.gradle(Module:app)에서 ConstraintLayout의 dependency를 추가해야 했는데, 요즘에는 자동으로 implementation에 추가되는 것 같다. 만약 프로젝트에 dependency가 없다면 아래와 같은 형태로 추가 해야 ConstraintLayout을 사용할 수 있다.

      dependencies {
          ...
          implementation 'com.android.support.constraint:constraint-layout:1.1.0'
      }
    


  • TextView 미리보기 전용 텍스트

    android:text에 텍스트를 입력하면 앱을 실행했을 때에도 해당 텍스트가 보인다. 레이아웃 디자인 시에만 텍스트를 보이게 하려면 tools:text 로 텍스트를 입력할 수 있다. tools로 입력한 텍스트는 앱을 실행했을 때에는 보이지 않는다.

    android:text="실제 App에 표시되는 text들"
    tools:text="디자인을 위해서만 필요한 Fake text들"
    


  • marginLeft 와 marginStart 차이

    xml에서 marginLeft 값을 줄 수도 있고, marginStart 값을 줄 수도 있다. 그런데 Design 툴에서 마우스로 Constraint 연결 시에 marginStart로 표시되는 것을 볼 수 있다. 실제로 우리가 사용하는 대부분의 앱에서는 둘의 차이가 없다. 그 이유는 한글은 왼쪽 -> 오른쪽 방향으로 쓰기 때문에, 즉 왼쪽에서 시작하기 떄문이다. 하지만 오른쪽 -> 왼쪽으로 쓰는 언어의 경우 marginLeft를 사용하면 기대와는 다른 결과가 나올 것이다.

    android:layout_marginLeft="10dp"
    android:layout_marginStart="10dp"
    

    다시 말하면 marginLeft는 절대적으로 왼쪽에서 시작하는 것이고, marginStart는 설정된 언어가 시작되는 방향에서 시작되는 것이다. 한글이나 영어를 사용할 경우 실질적으로 결과에는 차이가 없다. 나의 경우 marginStart 를 사용하고 있지만, 앱 특징에 따라 절대적으로 왼쪽에 여백을 주어야 할 경우 marginLeft를 사용하면 되겠다.



References

  • https://developer.android.com/reference/android/support/constraint/ConstraintLayout.html