Dev/android

Sticky Header Recyclerview using ItemDecoration without library - 1 (Kotlin)

Lonnie.byeol 2020. 5. 26. 22:56

Sticky Header 란? 사용자가 페이지를 아래로 스크롤 할 때, 화면 상단에 고정된 뷰를 말한다.

 

검색해 보면 라이브러리 한 줄로 사용하기도 쉽게 해뒀지만, 각자의 상황에 맞게 커스텀 하려면 라이브러리는 불편하다

 

ItemDecoration을 커스텀 해서 고정할 헤더 뷰를 그리기로 했다. 아래는 예제 샘플이다.

 

바쁜 사람은 여기 를 눌러 소스를 보러 가시길...

 

 

아래 부턴 삽질 히스토리 

 

 

사실 맨 위의 상단을 고정하는 건 검색해보면 되게 많이 나온다.

1. 

https://github.com/paetztm/recycler_view_headers

 

paetztm/recycler_view_headers

Simple Recycler View Section Header implementation - paetztm/recycler_view_headers

github.com

나는 이 분 소스를 많이 참고 했다..

 

2. https://stackoverflow.com/questions/32949971/how-can-i-make-sticky-headers-in-recyclerview-without-external-lib

 

How can I make sticky headers in RecyclerView? (Without external lib)

I want to fix my header views in the top of the screen like in the image below and without using external libraries. In my case, I don't want to do it alphabetically. I have two different types of...

stackoverflow.com

하지만 내가 구현해야 할 상황은 고정 될 뷰 위에 함께 스크롤 되어야할 뷰가 하나 더 존재하는 상황이다.

즉, 전체 RecyclerView에 고정되어야 할 뷰 인덱스가 0이 아니라 1인 경우이다.

 

먼저 RecyclerView를 만든다.

 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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=".MainActivity">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"/>
</LinearLayout>

 

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var adapter: MainAdapter
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setupAdapter()
    }

    private val peopleInfoList = listOf(
        Data("apple", 17),
        Data("banana", 31),
        Data("car", 26),
        Data("dog", 26),
        Data("egg", 24),
        Data("fish", 33),
        Data("great", 40),
        Data("happy", 10),
        Data("ice", 20),
        Data("juice", 32),
        Data("key", 50),
        Data("lonnie", 30),
        Data("mom", 58),
        Data("notice", 11),
        Data("object", 36),
        Data("people", 38),
        Data("queen", 15),
        Data("right", 22),
        Data("sight", 27),
        Data("tiger", 13),
        Data("uv", 29),
        Data("virus", 44),
        Data("wow", 36),
        Data("xylitol", 24),
        Data("yes", 26),
        Data("zoo", 36)
    )

    private fun setupAdapter() {
        adapter = MainAdapter(peopleInfoList)
        val layoutManager = LinearLayoutManager(this)
        recycler.layoutManager = layoutManager
        recycler.adapter = adapter
    }
}

 

Data.kt

data class Data(var name: String = "", var age: Int = -1)

RecyclerView에 집어넣을 데이터 클래스를 만들고 RecyclerView와 Adapter를 연결 시킨다.

Adapter엔 Data를 모델로 한 리스트를 매개 변수로 넣어준다.

 

ItemViewType을 정해보자.

 

최 상단에 올라갈 뷰를 TYPE_TOP 으로 정한다.

그 다음 리스트에서 고정될 뷰는 TYPE_HOLDER 

리스트가 그려질 뷰는 TYPE_ITEM

리스트가 없으면 TYPE_EMPTY

 

    companion object {
        const val TYPE_TOP = 0
        const val TYPE_HOLDER = 1
        const val TYPE_EMPTY = 2
        const val TYPE_LIST = 3
    }

    override fun getItemViewType(position: Int): Int {
        return when (position) {
            0 -> TYPE_TOP
            1 -> TYPE_HOLDER
            2 -> TYPE_EMPTY
            else -> TYPE_LIST
        }
    }

여기서 ViewType이 TYPE_HOLDER일 경우에 스크롤 시 상단에 그려진다는 것을 유의하자 TYPE_TOP과 혼돈 주의

각 뷰에 해당하는 layout xml을 만들고 뷰홀더에 bind 해준다.

 

top_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="1dp"
        android:background="#F98740"
        android:gravity="center"
        android:minHeight="54dp"
        android:paddingLeft="16dp"
        android:paddingTop="20dp"
        android:paddingRight="16dp"
        android:paddingBottom="20dp"
        android:text="This is a top type view"
        android:textSize="16sp"
        android:textStyle="bold" />
</LinearLayout>

hold_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="1dp"
        android:background="#ff8181"
        android:gravity="center"
        android:minHeight="54dp"
        android:paddingLeft="16dp"
        android:paddingTop="20dp"
        android:paddingRight="16dp"
        android:paddingBottom="20dp"
        android:text="This is a hold type view"
        android:textSize="16sp"
        android:textStyle="bold" />
</LinearLayout>

list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:paddingTop="19dp"
        android:paddingBottom="19dp">

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="20dp"
            android:layout_weight="1"
            android:text="text"
            android:textSize="16sp" />

        <TextView
            android:id="@+id/tv_age"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="13dp"
            android:drawablePadding="10.3dp"
            android:text="30"
            android:textSize="14sp" />

    </LinearLayout>
</LinearLayout>

empty_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center|top"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="26dp"
            android:gravity="center"
            android:text="There are currently no registered Item."
            android:textSize="16sp"
            android:textStyle="bold" />
    </LinearLayout>
</LinearLayout>

Adapter 내에서 각 View Type 에 맞게 뷰 홀더를 만들어 바인드 해준다.

    override fun onCreateViewHolder(parent: ViewGroup, position: Int): RecyclerView.ViewHolder {
        val view: View
        return when (recyclerItemList[position].type) {
            TYPE_TOP -> {
                view = LayoutInflater.from(parent.context)
                    .inflate(R.layout.top_item, parent, false)
                TopViewHolder(view)
            }
            TYPE_HOLDER -> {
                view = LayoutInflater.from(parent.context)
                    .inflate(R.layout.hold_item, parent, false)
                HolderViewHolder(view)
            }
            TYPE_EMPTY -> {
                view = LayoutInflater.from(parent.context)
                    .inflate(R.layout.empty_item, parent, false)
                EmptyViewHolder(view)
            }
            else -> {
                view = LayoutInflater.from(parent.context)
                    .inflate(R.layout.list_item, parent, false)
                ItemViewHolder(view)
            }
        }

    }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is TopViewHolder -> {
                holder.bindView()
            }
            is HolderViewHolder -> {
                holder.bindView()
            }
            is EmptyViewHolder -> {
                holder.bindView()
            }
            is ItemViewHolder -> {
                holder.bindView()
            }
        }
    }
inner class TopViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bindView() {
        }
    }

    inner class HolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bindView() {
        }
    }

    inner class EmptyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bindView() {
        }
    }

    inner class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bindView() {
        }
    }

다음 페이지에서 계속.. NEXT