Android 공부/Android UI

NestedScrollView 없이 RecyclerView를 사용해보자.

0. 요약

- 최근 고민은 리사이클러뷰의 재활용과 함께, NestedScrollView를 사용하지 않고, 뷰 구조를 잡기 위해서 고민을 했다.

- 고민 끝에, Coordinator Layout 내부에 각종 View를 셋팅하게 되면, 리사이클러뷰의 구조를 살린채, 레이아웃 구조를 잡을 수 있을 것이라고 생각을 했다.

- 하지만, Coordinator Layout에서 자유롭게 사용되어지는 뷰는 behavior를 받는 뷰 뿐만이 자유롭게 움직일 수 있다는 것을 알았다.

- 그래서, 리사이클러뷰를 만들고, 어댑터에 여러 개의 뷰홀더를 만들어야만 해결이 된다는 것을 알게 되었다.

1. NestedScrollView & RecyclerView

- 리사이클러뷰의 장점은 뷰홀더를 통해서 뷰를 재활용해서, 앱의 퍼포먼스를 향상시키는 것에 있다.

- 하지만, 리사이클러뷰가 전부가 아닌, 한 부분으로만 뷰구조를 잡아야 하는 경우가 발생하게 된다. 

- 그런 상황에서 가장 쉽게 생각할 수 있는 것은 네스티드 스크롤뷰를 리사이클러뷰에 감싸는 것을 많이 사용한다.

- 하지만, 네스티드 스크롤뷰를 사용하게 되면 뷰홀더는 재활용 되지 않고, 어플리케이션 성능을 크게 저하시키는 단점을 갖고 있다.

2. Coordinator Layout을 사용하면 해결할 수 있지 않을까?

 처음에는 Coordinator Layout을 사용하게 되면 해당 문제를 해결할 수 있을 것이라고 생각을 했다. 그래서 처음 생각한 뷰의 구조는 아래와 같다.


- CoordinatorLayout을 사용하여, 스크롤 시에 툴바 영역과 TextView, ConstraintLayout, RecyclerView 영역처럼 다양한 View들이 공존하는 영역으로 나눈다.

- 리스트를 스크롤 시에 CoordinatorLayout을 이용해서, 툴바의 애니메이션을 사용한다.

- NestedScrollView가 갖는 재활용이 되지 않는 리스트에서 벗어난다.

  

3. CoordinatorLayout + RecyclerView 구조

 2번에서 생각했던 뷰 구조를 만든다면, 아래와 같은 XML이 만들어질 것이다.
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
<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        
    </data>
    <androidx.coordinatorlayout.widget.CoordinatorLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:bind="http://schemas.android.com/tools"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        <!-- 툴바 관련 -->
        <include layout="@layout/layout_coordinator_toolbar"/>
        <LinearLayout 
                android:layout_width="match_parent" android:layout_height="320dp">
                <!-- 내용들 -->
        </LinearLayout>
        <!-- 리스트 -->
        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rv_contents"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingStart="@dimen/content_card_side_dimen"
                android:paddingEnd="@dimen/content_card_side_dimen"
                app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
        
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
 
cs

 우선, Coordinator가 XML의 최상단에 있다.

3-1. 평상시처럼 NestedScrollView처럼 생각하는 문제

 include_coordinator_toolbar 영역을 툴바 영역을 별도로 분리를 시켰고, LinearLayout에는 TextView나 ConstraintLayout등의 다양한 뷰가 사용되도록 했고, 리사이클러뷰를 추가적으로 사용하는 구조를 생각했다.

 이런 구조를 생각한 이유는 평상시에 NestedScrollView로 XML을 만들었기 때문에 이와 같은 구조를 생각한 것 같다. 하지만, 이것은 위에서 말한 것처럼 치명적인 문제가 있다.

- NestedScrollView은 리사이클러뷰가 오더라도, 해당 뷰를 모두 미리 그려서 재활용의 장점이 없이, 뷰를 표현한다.

- 그래서 NestedScrollView는 TextView, LinearLayout, ConstraintLayout, RecyclerView등 구분 없이 뷰를 모두 그린다.

- 이러한 뷰 구조는 성능상의 이슈를 가져올 가능성이 크다.

3-2. 위의 XML의 문제

 CoordinatorLayout은 CollapsingToolbarLayout과 layout_behavior의 조화로 움직이게 된다. 그래서 크게 움직이는 레이아웃은 CollapgsingToolbarLayout 안에 입혀진 뷰와 layout_behavior의 속성을 갖는 레이아웃 2가지 뿐이다. 그래서, 여러개의 위젯이 사용되어진다면, 해당 구조는 리니어레이아웃이 따로 움직이고, RecyclerView가 따로 움직이는 구조가 되버린다.

3-3. 문제 해결 방법

 뷰의 구조는 CoordinatorLayout과 RecyclerView, 2가지만 있거나, 오직 RecyclerView만 존재해야 한다.,
결국, RecyclerView의 다양한 뷰홀더 패턴을 활용해야만 해당 문제를 해결할 수 있을 것이라고 생각한다.

4. RecyclerView에 여러 개의 ViewHolder를 만들어서 해결하자.

   NestedScrollView를 사용하지 않고, 자유로운 뷰구조를 만드는 것이 힘들다는 것을 알았다.
RecyclerView를 심도 있게 사용하는 방법 밖에 없었고, 최종적인 XML은 아래의 구조로 만들어지게 되었다.
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable name="subtitle" type="String"/>
    </data>
    <androidx.coordinatorlayout.widget.CoordinatorLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:bind="http://schemas.android.com/tools"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/view_content_background">
        <!-- 툴바 관련 -->
        <com.google.android.material.appbar.AppBarLayout
                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="194dp"
                android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
                android:fitsSystemWindows="true">
            <com.google.android.material.appbar.CollapsingToolbarLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    app:layout_scrollFlags="scroll|enterAlwaysCollapsed"
                    android:fitsSystemWindows="true"
                    app:contentScrim="?attr/colorPrimary"
                    app:title="안녕.">
                <ImageView
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:background="@color/colorPrimary"
                        android:fitsSystemWindows="true"
                        app:layout_collapseMode="parallax"/>
                <androidx.appcompat.widget.Toolbar
                        android:id="@+id/toolbar"
                        android:layout_width="match_parent"
                        android:layout_height="?attr/actionBarSize"
                        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                        app:layout_collapseMode="pin" />
            </com.google.android.material.appbar.CollapsingToolbarLayout>
        </com.google.android.material.appbar.AppBarLayout>
        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rv_contents"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
 
cs

4-1. Adapter

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class ContentAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    enum class ContentViewType(val num: Int) {
        HEADER(0), CONTENT(1), HASHTAG(2), EMPTY(5)
    }
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            ContentViewType.HEADER.num -> {
                TitleViewHolder(makeInflater(parent, R.layout.item_content_title))
            }
            ContentViewType.CONTENT.num -> {
                ContentViewHolder(makeInflater(parent, R.layout.item_content_content))
            }
            ContentViewType.HASHTAG.num -> {
                SubTitleViewHolder(makeInflater(parent, R.layout.item_content_subtitle))
            }
            else -> {
                assert(false) { "절대 안 옴" }
                ContentViewHolder(makeInflater(parent, R.layout.item_content_subtitle))
            }
        }
    }
 
    override fun getItemCount(): Int {
        return 5
    }
 
    override fun getItemViewType(position: Int): Int {
        return when (position) {
            0 -> ContentViewType.HEADER.num
            123 -> ContentViewType.CONTENT.num
            4 -> ContentViewType.HASHTAG.num
            else -> ContentViewType.EMPTY.num
        }
    }
 
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        
    }
 
    inner class TitleViewHolder(view: View) : RecyclerView.ViewHolder(view)
    inner class ContentViewHolder(view: View) : RecyclerView.ViewHolder(view)
    inner class SubTitleViewHolder(view: View) : RecyclerView.ViewHolder(view)
 
 
    private fun makeInflater(parent: ViewGroup, layout: Int): View =
        LayoutInflater.from(parent.context).inflate(layout, parent, false)
}
cs


4-2. 설명

 어댑터에서는 헤더, 콘텐츠, 다른 부가적인 뷰, 바텀 뷰 등을 각각의 뷰홀더에서 관리를 해줘야 한다. 그러면 결과적으로 봤을 때는 리사이클러 뷰 이하에, 다양한 뷰가 움직이는 구조가 된다.

 그럼으로, 다양한 뷰를 그릴 수 있고, 리사이클러 뷰의 성능상 이점을 그대로 사용할 수 있게 되었다.