최근에 만든 프로젝트에서 필터 화면을 만들어야 했다. 하지만, 어떻게 구현해야할지 난감하기도 했다. 실제 화면을 보여주기는 그렇고, 대략 아래와 같은 화면이다. 즉, 크기가 서로 다른 필터 아이템을 정해진 넓이에 따라 자동으로 행을 바꿔 배치하려고 하는 경우, 안드로이드에서는 기본 레이아웃으로는 구현이 불가능하다.

위의 사진과 같은 화면을 어떻게 구현할 수 있을지 먼저 생각해봤다.

  1. ContraintLayout? -> 불가능
    • 개수가 정해진 경우에는 자식 뷰들간의 제약 조건을 구성하여 배치가 쉽지만, 새로운 항목이 추가되거나 제거되는 동적인 환경에서는 사용하기 어렵다.
    • 또한, 목록이 많아지는 경우 스크롤되지 않으며 자식 뷰의 크기가 줄어들어 적합하지 않다.
  2. RelativeLayout -> 불가능
    • ContraintLayout과 동일한 구조적인 문제로 적합하지 않다.
  3. GridLayout -> 불가능
    • RecyclerView를 사용하여 행에 표시될 기본 갯수를 지정해야 하기 때문에 자식 뷰의 넓이에 따라 행의 갯수를 가져야 하는 것과 동떨어지기 때문에 적합하지 않다.
  4. LinearLayout -> 최선은 아니지만, 구현 가능
    • ScrollView와 조합하여 수동으로 구현할 수 있다.
    • 부모 레이아웃의 넓이를 가져와 자식 뷰를 하나씩 추가하면서 행을 언제 바꿔야 할지 수동으로 구현할 수 있다.
    • 이미 TagView 라이브러리가 존재한다.

여러 방법을 생각해보다가 FlexBoxLayout이라는 걸 알게 되었다. Github을 살펴보고 여러 자료를 참고한 결과, 사용하기 적절할 것 같았다.

FlexBoxLayout

  • 기본 개념은 웹에서 사용되던 CSS Flexible Box Layout Module을 안드로이드에 접목하여 개발한 라이브러리이다.
  • 위의 4번에서 LinearLayout으로 구현하기 위해 사용한 중첩 구조를 완벽하게 피할 수 있으면서 원하는 구조를 만들 수 있다.

1. dependency

1
2
3
dependencies {
implementation "com.google.android:flexbox:2.0.1"
}

2. usage

  • FlexboxLayout을 정의하고, flexwrap 속성을 사용하면 ViewGroup 내의 자식 뷰들이 자동으로 다음 행으로 이동한다.
  • GirdLayout의 행에 고정된 항목이 아닌 행 별로 다른 넓이를 가질 수 있도록 항목이 가변적일 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<com.google.android.flexbox.FlexboxLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:flexWrap="wrap"
app:alignItems="stretch"
app:alignContent="stretch" >

<TextView
android:id="@+id/textview1"
android:layout_width="120dp"
android:layout_height="80dp"
app:layout_flexBasisPercent="50%"
/>

<TextView
android:id="@+id/textview2"
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_alignSelf="center"
/>

<TextView
android:id="@+id/textview3"
android:layout_width="160dp"
android:layout_height="80dp"
app:layout_alignSelf="flex_end"
/>
</com.google.android.flexbox.FlexboxLayout>
  • 또한, FlexboxLayout은 RecyclerView와 함께 사용하여 스크롤을 자동으로 처리할 수 있다.
  • 화면에 모두 표시된다면 문제가 없지만, 아이템이 많아 스크롤되어야 한다면 ScrollView에서 FlexboxLayout을 구현하는 것보다 RecyclerView를 이용하는 것이 성능이나 메모리면에서 훨씬 더 많은 이점이 있다.
  • FlexboxLayoutManager를 사용하면 된다.
1
2
3
4
5
6
7
8
9
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/ageList"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_8"
app:layoutManager="com.google.android.flexbox.FlexboxLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/titleAges" />
  • 이 경우에는 flexWrap 속성을 xml에서 설정할 수 없으므로 코드 상에서 설정해야 한다.
  • flexGlow : 이 속성은 LinearLayout의 weight와 같은 속성으로 동작한다. 각 행의 나머지 공간을 균등한 공간으로 채울 수 있다. 하지만, itemView에 margin을 추가해서 이를 대체할 수 있다.(필자는 그렇게 했다…ㅎ) -> 이게 아닌 divider를 사용하는 방법도 있는데, 이는 Github을 참고하자!
  • flexDirection : 총 4가지 속성으로 구성되어 있다.
    • ROW : 아이템 항목이 배치되는 방향은 행(가로) 방향으로 배치된다.(왼쪽부터 시작한다.) 행을 다 채웠다면 자동으로 다음 행으로 바뀐다.
    • ROW_REVERSE : 반대 방향으로 진행되며 행 방향으로 배치된다.
    • COLUMN : 아이템 항목이 배치되는 방향은 열(세로) 방향으로 배치된다.(위쪽부터 시작한다.) 아래로 길어진다.
    • COLUMN_REVERSE : 반대 방향으로 진행되면 열 방향으로 배치된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
with(binding) {
styleList.run {
adapter = styleAdapter
layoutManager = FlexboxLayoutManager(activity).apply {
flexWrap = FlexWrap.WRAP
flexDirection = FlexDirection.ROW
}
setHasFixedSize(true)
}

ageList.run {
adapter = ageAdapter
layoutManager = FlexboxLayoutManager(activity).apply {
flexWrap = FlexWrap.WRAP
flexDirection = FlexDirection.ROW
}
setHasFixedSize(true)
}
}

3. Supported attributes

  • justifyContent : 설정된 방향에 따라 배치된 정렬 상태를 변경할 수 있으며, 왼쪽으로 정렬하거나 가운데 또는 뒤쪽으로 정렬할 수 있다.
  • flexWrap : wrap으로 설정하면, 현재 라인에 충분한 공간이 없는 경우 다음 라인에 view를 배치한다.
  • 반응형 레이아웃을 구성할 수 있다. landscape 모드로 전환했을 때, 레이아웃이 자동으로 landscape 모드에 맞게 변경된다. 만약, 같은 기능을 LinearLayout을 이용해 구성하려고 했다면 수많은 레이아웃 파일들이 필요했을 것이다.

FlexboxLayout을 통해 필자가 원하는 기능을 구현할 수 있었다. 사용법도 어렵지 않고, Github에 잘 설명되어 있다. 간단하게 적용해봤지만, 조금 더 공부한다면 더 멋지게 활용할 수 있을 것 같다. 추가 사항은 Github을 참고하길 바란다!

Thanks Google :-)

Reference