본문 바로가기
안드로이드

[안드로이드] 반응형을 고려한 xml 마크업 (ConstraintLayout)

by algosketch 2022. 2. 26.

이 글은 특정 키워드를 검색해서 필요한 내용만 확인하는 글보다는, 가볍게 전체적으로 읽기를 바라며 작성했다. 마크업부터 잘못 작성되는 경우가 있는데, 참고할 만한 글이 되었으면 좋겠다. 특정 코드를 작성하는 방법은 이미 많은 글이 있으므로 코드보다는 의미 위주로 보면 된다.

 

마크업은 반응형 뿐만 아니라 추가로 나중에 수정될 것까지 고려해야 한다. 추가로 이 글은 다양한 레퍼런스를 참고하기보다는 내 개인적인 의견으로 작성된 글이니 무작정 받아들이기보다 자신의 생각과 비교하며 읽어주길 바란다. 마크업은 일반적으로 성능(View 객체가 가장 적게 생성 됨.)이 가장 좋은 ConstraintLayout 을 기준으로 작성하였다.

 

왜 마크업을 주의해서 작성해야 할까?

휴대폰마다 해상도, pixel ratio (pixel 을 계산하기 위해 dp 에 곱하는 값)가 모두 다르다. 안드로이드에서는 가로길이 360 dp 를 많이 사용했었고 최근에는 412 dp 가 더 많아지는 추세이다. 참고로 아이폰은 375 dp 가 많다. 세로길이의 경우 스크롤을 넣으면 해결되므로 크게 중요하지 않다. 디자인은 그 중 작은 화면인 360 dp 기준으로 많이 작성된다. (1배수 디자인) 핵심은 해상도가 달라져도 크게 다르지 않은 UI 를 제공해야 한다는 것이다.

디자인에 4, 8의 배수가 많이 들어가는 이유는 pixel ratio 때문이다. dp 값에 pixel ratio 를 곱하면 실제 픽셀수가 나온다. pixel ratio 를 찾아보면 0.75, 1.5, 2.625 와 같은 값을 확인할 수 있을 것이다. 즉, dp * (pixel ratio) 값이 정수가 되는 수가 8의 배수이다. 다만 크기가 큰 경우 별로 티가 나지 않고, 이 사실을 알고 디자인하는 경우도 그리 많지 않은 것 같으니 너무 4의 배수에 고집하지 말고 적당히 타협하기도 했다. 크기가 작을 경우에만 디자이너에게 전달해 주자.

 

디자인이 잘못된 경우

가로길이 412 dp

디자인을 처음하는 경우 잘못된 디자인이 나오는 경우도 흔히 볼 수 있다. 대표적인 예로는 고정된 가로 길이를 사용하는 경우이다. 위 디자인에서 디자이너가 가로길이 160 dp 를 의도했다고 가정해 보자. 위 화면을 보는 사람의 휴대폰은 412 dp 이므로 문제가 없다.

 

만약 360 dp 화면에서 본다면 위처럼 문제가 생긴다. 고정된 가로길이를 무조건 사용하면 안 되는 것은 아니지만 위처럼 문제가 발생할 가능성이 있는 디자인은 잘못된 경우다. 위 디자인에서는 일반적으로 양옆 마진과 가운데 마진만 정해두고 View 는 자신이 사용할 수 있는 최대 가로길이를 사용한다.

 

위처럼 고정된 가로길이를 사용해도 기기마다 별 차이가 없는 경우 고정된 길이를 사용하기도 한다.

그 밖에도 디자인에 소수점이 있거나, 통일되지 않은 디자인, 정사각형으로 래핑되지 않은 아이콘 등 디자인에서 잘못된 경우도 많다. 디자인 asset 을 다운받는 방법은 아이콘은 24 x 24 svg, 이미지일 경우 jpg, 이미지인데 배경이 투명인 경우 png 형식으로 다운로드 받으면 된다. svg 의 경우 벡터이기 때문에 크기가 상관 없지만, 이미지 형태로 다운 받을 경우에는 반드시 x4 과 같이 큰 이미지로 받아야 고해상도 휴대폰에서 뭉개지지 않는다.

만약 아이콘을 확대했을 때 뭉개진다면 원본 아이콘이 벡터가 아닌 경우이니 디자이너에게 벡터 아이콘을 사용해달라고 하자.

 

1. 가로길이 마크업

1-1. 가장 기본적인 가로 길이

이런 디자인이 일반적인 버튼의 형태다. 위 디자인에서 가로길이는 몇일까? 정답은 312 dp 가 아니라 match_parent 이다.

<androidx.appcompat.widget.AppCompatButton
    android:layout_width="match_parent"
    android:layout_height="48dp"
    app:layout_constraintBottom_toBottomOf="parent"
    android:layout_marginHorizontal="16dp"
    ...
    />

유사한 디자인을 마크업한 코드를 가져온 것이다. 위처럼 match_parent 에 marginHorizontal 을 사용하여 마크업해주면 된다. 위처럼 가이드라인이 있는 경우 상위 View (ConstraintLayout) 에 paddingHorizontal 을 주는 방식으로 마크업하고 싶은 충동에 빠질 수 있다. 이 편이 전체 코드가 줄어드니까. 하지만 디자인은 바뀌기 쉽기 때문에 margin 을 사용하기를 권장한다.

위 디자인은 실제 출시된 버전은 아니고, 여러 디자인을 테스트해본 흔적 중 하나이다. 처음에 왼쪽 디자인이어서 상위 View의 padding 으로 마크업을 했다가 오른쪽 디자인으로 바뀔 경우 구분선이 추가되어 padding 을 다시 지우고 다른 모든 View 에 margin 을 추가해야 한다.

 

1-2. 가로길이 히든 케이스? 찾아내기

위 디자인은 꼼꼼함이 약간 부족한 디자인이다. (사실 위 케이스에서는 괜찮다. 비슷한 케이스를 마주쳤을 때 고려해 보라. 내가 만든 거니까 내 맘대로 까도 된다고 생각한다.) 무엇이 잘못되었을까? 가로길이는 몇일까? wrap_content?

이 경우도 마찬가지로 가로 길이는 match_parent 이다. 이런 디자인에서 wrap_content 로 마크업하는 건 디자인이 한 줄 케이스만 주어졌을 때 흔히 하는 실수이다. 글자 길이가 길어지면 TextView의 오른쪽은 margin 가이드라인 밖으로 나갈 것이다. 이런 디테일까지 고려하여 디자이너가 있다면 디자이너에게 감사하자.

 

1-3. 글자수에 따라 가로 길이가 유동적으로 변하는 경우

참고로 이 태그 형태의 UI 는 RecyclerView 로 구현되었다. 이 태그의 사이즈를 확인하니 숫자가 이상하다.

 

이럴 때는 패딩을 확인해 보자. 말풍선과 같은 UI 는 View 의 사이즈를 wrap_content 와 padding 을 이용해서 준다.

<TextView
    android:id="@+id/item_tag"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textColor="@color/primary"
    android:textSize="12sp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    tools:text="정문"
    android:paddingVertical="4dp"
    android:paddingHorizontal="8dp"
    android:background="@drawable/tag_background"/>

실제 마크업 된 코드이다. 가로, 세로 길이를 wrap_content 그리고 padding 을 통해 사이즈를 조절하고 있다.

 

1-4. 일부러 고정된 가로 길이를 사용하는 경우

이 경우 회색글씨로 작성된 글자수가 달라도 정렬을 맞추기 위해 의도적으로 wrap_content 가 아닌 80 dp 로 마크업할 수 있다.

 

 

 

2. 위치 설정

2-1. 세로 위치

위 디자인에서 TextView 의 세로 위치는 어떻게 설정할까? 구분선에서 16 dp? 나는 왼쪽 8의 top 위치에 일치시킨다.

<TextView
    ...
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toTopOf="@id/bus_number"
    app:layout_constraintStart_toEndOf="@id/bus_number"
    ...
    />

코드를 살펴보자. constraintTop_toTopOf 를 통해 세로 위치를 맞춘 것을 확인할 수 있다. 이렇게 해야하는 이유는 뭘까? 마찬가지로 디자인은 변경되기 쉽기 때문에, 세로 마진 값이 바뀌었을 때 왼쪽 View 만 바꿔주면 TextView 는 자동으로 위치가 바뀌기 때문에 손 댈 필요가 없다.

 

2-2. 가로 위치

가로 위치도 같은 방법으로 마크업 할 수 있다. 위 디자인의 경우 위에 있는 TextView 의 start 위치와 일치시켜주면 된다. 아까 설명했던 match_parent 에 margin 으로 크기를 잡는 게 아닌 경우에 적용할 수 있다. 추가로 선택된 TextView 의 세로 위치는 위 TextView 와의 margin 이 아닌 왼쪽 View 의 bottom 와 자신의 bottom 을 일치시키는 방법으로 마크업 할 수 있다.

 

2-3. 가운데 정렬

이 경우에는 어떻게 해야할까? 마크업 할 때에는 단순히 똑같이 생긴 화면을 만드는 게 아니라, 디자인할 때와 같은 의도를 가지고 마크업 해야 한다. 이 경우에는 위 구분선으로부터 34 dp 떨어진 Text 가 아니라 왼쪽 Image 의 가운데에 위치하라는 의미일 것이다.

app:layout_constraintTop_toTopOf="@id/image_view"
app:layout_constraintBottom_toBottomOf="@id/image_view"

따라서 마크업도 위 코드처럼 ImageView 의 위아래를 기준으로 가운데로 맞춰야 한다.

 

3. ConstraintLayout 사용 방법

3-1. weight 적용

LinearLayout 에서 유용하게 사용하는 weight 속성은 Consraint 에서도 사용할 수 있다. 

보통의 위와 같이 Constraint 를 설정한다. 하지만 weight 를 설정하려면 아래와 같이 Constraint 를 설정해야 한다.

 

3-2. 사용할 수 있는 최대 크기로 늘리기

match_parent 를 사용하는 경우 영역을 초과하더라도 parent 크기로 렌더링 되다. 이것을 margin 이나 padding 으로 바로잡는 것을 적절해 보이지 않는다. 위와 같은 UI 에서 이런 문제를 겪는다.

<EditText
    android:id="@+id/commentEditText"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    ...
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toStartOf="@id/btn_finish"
    android:layout_marginStart="16dp"
    android:layout_marginEnd="4dp"/>

width 를 0dp 로 설정하고 좌우 constraint 를 설정해 주면 좌우 View 들의 위치와 margin 값을 고려하여, 자신이 사용할 수 있는 최대 크기로 늘어난다.

 

미리 내용을 정리하고 작성한 게 아니라 글을 작성하면서 생각난 내용 위주로 작성한 글이라 빠뜨린 내용이 있을 지 모르겠다. 세부적으로 들어가면 훨씬 더 많은 스킬이나 디자인할 때의 의미들을 찾을 수 있다. 이 글에서 각각을 어떻게 마크업해야하는지 힌트를 얻는 것도 좋지만, 그보다 더 핵심적인 것은 디자인에 접근하는 방법이라 생각한다. 무조건 이 상황엔 이렇게 마크업해야 한다고 익히는 것보다 이 UI 가 어떤 것을 기준으로 크기나 위치를 잡았을지 생각하며 마크업하기를 바란다.

안드로이드 마크업에 대해 더 공부하고 싶다면 스타일 가이드에 대해 찾아보고, 이런 스타일 가이드를 어떻게 안드로이드 resource 로 다룰 수 있을지 고민해보기를 추천한다.