본문 바로가기
Android

[Android] Sticky Header 만들기

by 너츠너츠 2023. 4. 9.

정리 배경

프로젝트를 진행하며 특정 부분이 스크롤 됨에 따라 상단에 붙어 있어야 하는 기능들이 존재합니다. 이 때 간편하게 적용할 수 있도록 단계별로 정리해보고자 합니다.

 

저의 경우 아래 그림과 같이 배너 (가로 스크롤), 카테고리 (스크롤 시 상단 고정), 컨텐츠(ViewPagers 가로 스크롤 with 내부 RecyclerView)를 가진 형태를 기준으로 테스트해보겠습니다

 

Sticky Header 만드는 방법

1. 오픈소스가 존재합니다

구글에 StickyScrollView Github 라고 검색하게 되면 amarjain07님께서 만들어주신 StickyScrollView가 있습니다. 또한 사용법 역시 간단해서 빠르게 구현할 수 있습니다.

 

https://github.com/amarjain07/StickyScrollView

 

만약 구현하고 싶다면 오픈 소스 내에 StickyScrollView 코드를 참고하여 CustomView를 구현하실 수 있습니다.

https://github.com/amarjain07/StickyScrollView/blob/master/library/src/main/java/com/amar/library/ui/StickyScrollView.kt

 

 

오픈 소스 코드를 참고해보면 CustomView를 만드실 때 NestedScrollView를 사용하신 것을 확인할 수 있습니다. 제 생각엔 이 부분이 내부에 RecyclerView를 도입해도 스크롤이 잘 동작하도록 설정하신 것 같습니다. 

class StickyScrollView @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0
) : NestedScrollView(context, attributeSet, defStyleAttr) {...}

 

 

만약 RecyclerView를 넣었을 때 각자 움직인다면 nestedScrollingEnabled를 false로 설정하시면 됩니다.

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:overScrollMode="never"
        android:nestedScrollingEnabled="false" />

 

하지만 이 방법은 생각보다 문제가 많습니다. NestedScollView를 사용하시는 대부분의 분들은 Scoll의 권한을 상위 ScollView가 가져가니까 편리하다는 생각으로 사용하실 수 있습니다만 ViewHolder가 재사용되지 않는 문제가 발생합니다.

 

NestedScrollView안에 RecyclerView를 사용할 경우, RecyclerView는 아이템을 전부 미리 생성하게 되고, 이에 따라 뷰를 재사용하여 메모리 효율을 높일 수 있는 RecyclerView의 장점이 사라진다.
따라서 아이템이 많은 경우에는 NestedScrollView안에 RecyclerView를 사용하는 것을 지양하는 것이 좋다고 한다.

https://velog.io/@kimbsu00/Android-7

 

제가 이걸 보여드리기 위해 컨텐츠영역을 ViewPager2를 지정한 것입니다. Contents영역에서 TAB0에서 Contents 100만큼 확인한다음 다음 카테고리로 넘어가면 TAB1의 contents는 스크롤 한 적이 없음에도 Contents 100을 바라보는 것을 확인할 수 있습니다.

 

이것이 바로 NestedScrollView가 아이템을 미리 생성하게 만들기 때문에 RecyclerView의 장점이 사라진다는 것입니다. 

그러면 이걸 바로 잡으려면 어떻게 해야할까요?

 

 

2. RecyclerView (Multi-ViewType) + ItemDecoration

많은 개발자 분들이 테크 블로그에 RecyclerView와 ItemDecoration을 통해 구현하는 것을 확인할 수 있었습니다. 하지만 전체 가로 스크롤이 가능한 뷰일 뿐, 여러 스크롤들이 적용된 화면은 보지 못했습니다. 그래서 한번 만들어보려고 합니다.

 

일단 ItemDecoration에 대해 구현하기 위해 여러 블로그들을 참고했습니다. 

https://dnight.tistory.com/entry/Android-Sticky-Header-RecyclerView

https://leveloper.tistory.com/198

 

ItemDecoration과 Adapter 구현은 넘어가도록 하겠습니다.

 

코드를 적용하면 아래 영상처럼 나오게 됩니다. 하지만 아직도 Scoll이 독립적이지 않고 영향을 주고 있음을 확인할 수 있었습니다.

 

 

현재 TabLayout을 Header로 설정하고 계속 그리기 때문에 ViewPager2가 넘어갔음에도 잘 적용되지 않는 것을 볼 수 있었습니다. 이를 해결하는 다른 대안으로는 Header에 TabLayout과 ViewPager2를 넣는 것인데 Tab은 잘 넘어갈지 몰라도 계속 그리면 성능상 문제가 발생합니다. 

반대로 ScrollView안에 넣는 방법은 1번의 방법과 다른 것이 없기 때문에 개선이 필요했습니다.

 

즉 Multi-ViewType 같은 경우 TabLayout을 같이 쓰는 ViewPager2는 별로 좋지 않다는 것을 알 수 있었습니다.

(제가 실력이 부족해서 구현이 모자랐던 부분이 있을 것 같아 다른 해결책을 떠올리면 포스팅 해보겠습니다!)

 

 

조금 더 좋은 UX를 주고자 방안을 모색하다 CoordinateLayout에 대해 알게 되었습니다.

 

3. CoordinateLayout를 활용하여 처리하기

 

저는 CoordinateLayout을 도입 함으로서 UX적인 측면을 개선할 수 있었습니다. 

해당 테크 블로그를 참고하여 구현하였습니다!

 

 

 

Layout은 다음과 같이 구현했습니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".case2_2.CoordinateActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:fitsSystemWindows="true"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:contentScrim="@color/white"
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:title="AppBar Title">

                <androidx.viewpager2.widget.ViewPager2
                    android:id="@+id/banner"
                    android:layout_width="match_parent"
                    android:layout_height="300dp" />

        </com.google.android.material.appbar.CollapsingToolbarLayout>

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="50dp"
            app:expandedTitleMarginBottom="50dp"
            app:expandedTitleMarginStart="32dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:title="AppBar Title">

            <com.google.android.material.tabs.TabLayout
                android:id="@+id/category_tab"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:background="@color/white"
                app:tabIndicatorColor="@null" />

        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/category_vp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        android:overScrollMode="never"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
        app:layout_constrainedHeight="true" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

 

사실 2-1에서 RecyclerView의 ItemDecoration을 이용해서 구현하는 것보다 훨씬 쉽습니다. 

이를 통해 구현 실력을 통해 특정 기능을 만들어 내는 것도 좋지만 지속적인 기술 변화를 체크하며 인지하고 있어야 한다는 생각을 하게 되는 결과물이었습니다.

 

<코드>

https://github.com/JGeun/Android_Study/tree/master/StickyHeader

 

GitHub - JGeun/Android_Study: This repository is an Android Study Collections

This repository is an Android Study Collections. Contribute to JGeun/Android_Study development by creating an account on GitHub.

github.com

 

반응형

댓글