[Android] RecyclerViewを用いてSticky Headerを実装する

##はじめに
RecyclerViewとItemDecorationクラスを用いて、Sticky Headerを実装します。

[Epoxy](https://github.com/airbnb/epoxy)などのライブラリを利用することで、Sticky Headerの実装は可能ですが、今回は使い慣れてるRecyclerViewを用いて実装します。
ライブラリを使うより、描画のロジックが分かりやすいため、できればこちらの方法で実装することをお勧めします。

理解して頂くためにも、ソースコードにはなるべくコメント残していますので、最後まで見て頂ける嬉しいです。



ソースコードは[GitHub](https://github.com/kosuke-nakazono/Sticky_Header)に上げております。

##そもそもSticky Headerとは?
Sticky Headerとはこのような動きをするリストです。
![2021-08-01_22_24_09.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1830512/d34c3b02-43b6-b5c5-2971-2154cda28294.gif)

##　全体の流れ

**RecyclerViewとRecyclerViewのItemDecorationクラスを用いて実装します。**


1. 各リストを管理するItemクラスを作成
1. StickyHeader描画用のインターフェースの定義
* RecyclerViewのAdapterクラスに上記インターフェースを継承させる
* Adapterクラスのインスタンスから、RecyclerViewのItemDecorationを作成し、ItemDecorationのonDrawOverメソッド内で、Sticky Headerの描画ロジックを作成
* RecyclerViewに上記のItemDecorationをセット

以上が全体の流れになります。

#　実装
まずは各リストを管理するItemクラスを作成します。
このItemクラスでは日付を管理するString型の**date**と、Headerかどうかを判断するBoolean型の**isHeader**の二つの変数を持たせます。

```Item.kt
data class Item(var date: String, var isHeader: Boolean)
```
### 通常のRecyclerViewの実装から始める
一度に書いてしまうと混乱するため、まずは通常のRecyclerViewの実装します。
#### MainActivity
MainActivityではAdapterのセットと、日付を格納したリストを作成します。

```MainActivity.kt
class MainActivity : AppCompatActivity() {

    companion object {
    const val STICKY_HEADER_SAMPLE = "STICKY_HEADER_SAMPLE"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)

        // adapterのセット
        val adapter =  MyAdapter(makeItems())
        recyclerView.adapter = adapter
    }


    // 2021年5月1日から今日までの日付を格納したリストを作成
    // 10の倍数をStickyHeaderとする
    private fun makeItems(): MutableList<Item>{
        val list = mutableListOf<Item>()

        // 現在の日付を元にカレンダーのインスタンスを取得
        val calender = Calendar.getInstance()

        // 2021年5月1日
        val pastCalender = Calendar.getInstance().apply {
            set(2021,5,1)
        }

        while (calender.after(pastCalender)){
            val day = calender.get(Calendar.DATE)
            val month = calender.get(Calendar.MONTH).plus(1)
            val date = "${month}月${day}日"

            // 10の倍数であればSticky Headerとする
            val isHeader = (day % 10) == 0

            val item = Item(date = date, isHeader = isHeader)
            list.add(item)

            calender.add(Calendar.DATE,-1)
        }

        return list
    }
}
```
```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

</androidx.constraintlayout.widget.ConstraintLayout>
```

#### Adapter
Headerタイプと通常タイプの2種類のレイアウトを使用しています。

```MyAdapter.kt
class MyAdapter(private val mItemList: List<Item>) : RecyclerView.Adapter<MyAdapter.ParentViewHolder>(){

    enum class ListStyle(val type: Int){
        HeaderType(0),
        NormalType(1)
    }

    // ViewHolder機構（親クラス）
    open class ParentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    // ViewHolder機構 Header Type（子クラス）
    class HeaderViewHolder(view: View) : MyAdapter.ParentViewHolder(view) {
         val headerDate: TextView = view.findViewById(R.id.headerDate)
    }

    // ViewHolder機構 Normal type（子クラス）
    class NormalViewHolder(view: View) : MyAdapter.ParentViewHolder(view) {
         val date: TextView = view.findViewById(R.id.date)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParentViewHolder {
         return when(viewType){
            // HeaderType
            ListStyle.HeaderType.type -> {
                val view =  LayoutInflater.from(parent.context).inflate(R.layout.header_layout, parent,false)
                HeaderViewHolder(view)
            }
            // NormalType
            else -> {
                val view =  LayoutInflater.from(parent.context).inflate(R.layout.normal_layout, parent,false)
                NormalViewHolder(view)
            }
        }
    }

    override fun onBindViewHolder(holder: ParentViewHolder, position: Int) {
        val item = mItemList[position]
        when(holder){
            // HeaderType
            is HeaderViewHolder -> {
                holder.headerDate.text =  item.date
            }
            // NormalType
            is  NormalViewHolder -> {
                holder.date.text = item.date
            }
        }
    }

    override fun getItemCount(): Int = mItemList.size

    override fun getItemViewType(position: Int): Int {
        val item = mItemList[position]
        return if(item.isHeader){
            ListStyle.HeaderType.type
        }else{
            ListStyle.NormalType.type
        }
    }
}
```
```header_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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="wrap_content"
    tools:context=".MainActivity"
    android:background="#eee"
    android:paddingTop="25dp"
    android:paddingBottom="25dp">

    <TextView
        android:id="@+id/headerDate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/black"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

```normal_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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="wrap_content"
    android:background="@drawable/normal_layout_bg"
    tools:context=".MainActivity"
    android:paddingTop="25dp"
    android:paddingBottom="25dp">

    <TextView
        android:id="@+id/date"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/black"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```


以上が通常のRecyclerViewの実装です。
10の倍数の日付のセルに着色させた、至って普通のリストですね。

こんな感じでできました。
![2021-08-01_23_00_45.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1830512/1e237233-a5d0-6f82-f16b-85d62d337b2b.gif)

では次にこのヘッダー用のセルをSticky Headerへと変えて行きます。

まずRecycleraViewのItemDecorationクラスの作成とSticky Headerの作成に必要なメソッドを定義したンターフェースを作成します。

### StickyHeaderHandlerインターフェースの作成

```StickyHeaderHandler.kt
interface StickyHeaderHandler {
    companion object {
        const val HEADER_POSITION_NOT_FOUND = -1
    }

    /** StickyHeaderのポジションを返す */
    fun getHeaderPosition(itemPosition: Int): Int
    /** StickyHeaderのレイアウトIDを返す */
    fun getHeaderLayout(headerPosition: Int): Int
    /** StickyHeaderにデーターを渡す */
    fun bindHeaderData(header: View?, headerPosition: Int)
    /** リストがヘッダーかどうかを判定する */
    fun isHeader(itemPosition: Int): Boolean
}
```

実装先である、Adapterクラスにて具体的な処理内容を記述します。

```MyAdapter.kt
class MyAdapter(private val mItemList: List<Item>) : RecyclerView.Adapter<MyAdapter.ParentViewHolder>(),
StickyHeaderHandler  // 追加{ 

    enum class ListStyle(val type: Int){
        HeaderType(0),
        NormalType(1)
    }

    // ViewHolder機構（親クラス）
    open class ParentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    // ViewHolder機構 Header Type（子クラス）
    class HeaderViewHolder(view: View) : MyAdapter.ParentViewHolder(view) {
         val headerDate: TextView = view.findViewById(R.id.headerDate)
    }

    // ViewHolder機構 Normal type（子クラス）
    class NormalViewHolder(view: View) : MyAdapter.ParentViewHolder(view) {
         val date: TextView = view.findViewById(R.id.date)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParentViewHolder {
         return when(viewType){
            // HeaderType
            ListStyle.HeaderType.type -> {
                val view =  LayoutInflater.from(parent.context).inflate(R.layout.header_layout, parent,false)
                HeaderViewHolder(view)
            }
            // NormalType
            else -> {
                val view =  LayoutInflater.from(parent.context).inflate(R.layout.normal_layout, parent,false)
                NormalViewHolder(view)
            }
        }
    }


    override fun onBindViewHolder(holder: ParentViewHolder, position: Int) {
        val item = mItemList[position]
        when(holder){
            // HeaderType
            is HeaderViewHolder -> {
                holder.headerDate.text =  item.date
            }
            // NormalType
            is  NormalViewHolder -> {
                holder.date.text = item.date
            }
        }
    }

    override fun getItemCount(): Int = mItemList.size

    override fun getItemViewType(position: Int): Int {
        val item = mItemList[position]
        return if(item.isHeader){
            ListStyle.HeaderType.type
        }else{
            ListStyle.NormalType.type
        }
    }

     // ここから下を追加 ---------

    // 表示中の一番上のセルからリストの先頭(一番上)まで遡り、ヘッダーがあればそのインデックスを、なければ -1を返す
    override fun getHeaderPosition(itemPosition: Int): Int {
        var headerPosition = StickyHeaderHandler.HEADER_POSITION_NOT_FOUND

        var targetItemPosition = itemPosition
        do {
            if (isHeader(targetItemPosition)) {
                headerPosition = targetItemPosition
                break
            }
            targetItemPosition -= 1
        } while (targetItemPosition >= 0)
        return headerPosition
    }

    // Header用のレイアウトを返す
    override fun getHeaderLayout(headerPosition: Int): Int {
        return R.layout.header_layout
    }

    // Headerセルにてデータをbindする
    override fun bindHeaderData(header: View?, headerPosition: Int) {
        header?:return

        val headerItem = mItemList[headerPosition]
        if (headerItem.isHeader) {
            val headerDate = header.findViewById(R.id.headerDate) as TextView
            headerDate.text = headerItem.date
        }
    }

    override fun isHeader(itemPosition: Int): Boolean {
        val item = mItemList[itemPosition]
        return item.isHeader
    }
}
```
### ItemDecorationクラスの実装
このItemDecorationクラス内で先ほど作成したインターフェースのメソッドを使い、Sticky Headerの描画を行います。

### 何をしてるかざっくり解説

<dl>
  <dt>・画面上にSticky Headerが既に描画されている場合</dt>
  <dd>そのSticky Headerの次のセル(下のセル)がSticky Headerタイプのセルかどうかを判定し、Sticky Headerタイプであれば既存のSticky Headeを押し上げ、新たなSticky Headerを描画。Sticky Headerタイプでなければ、既存のSticky Headerをそのまま同じ位置で最描画。</dd>
  <dt>・画面上にSticky Headerが描画されていない場合</dt>
  <dd>表示中の一番上のセルがSticky Headerタイプであれば、そのセルを新たにSticky Headerとして描画。通常のセルであれば何もしない。</dd>
</dl>


```MyItemDecoration.kt
class MyItemDecoration(private val mStickyHeaderListener: StickyHeaderHandler) : RecyclerView.ItemDecoration() {

    // Header View
    private var mCurrentHeaderView: View? = null

    // RecyclerViewのセルが表示される度に呼ばれる
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)

        // 表示中のリスト内のトップViewを取得
        val topChildView: View = parent.getChildAt(0)?:return

        // topChildViewのインデックス
        val topChildViewPosition = parent.getChildAdapterPosition(topChildView)

        // topChildViewPosition取得失敗時はこれ以降の処理を行わない
        if (topChildViewPosition == RecyclerView.NO_POSITION) return

        // 直近のHeaderPositionのインデックスを取得
        val prevHeaderPosition = mStickyHeaderListener.getHeaderPosition(topChildViewPosition)

        // 直近にHeaderが存在しない場合はこれ以降の処理を行わない
        if(prevHeaderPosition == StickyHeaderHandler.HEADER_POSITION_NOT_FOUND) return

        // 直近にHeaderが存在する → topChildViewPositionにHeaderが存在する
        mCurrentHeaderView = getHeaderView(topChildViewPosition, parent)

        // 現在のHeaderレイアウトのセット
        fixLayoutSize(parent, mCurrentHeaderView)

        // 現在のHeaderのBottom Positionを取得 (親Viewからの相対距離)
        val contactPoint = mCurrentHeaderView!!.bottom

        // Headerの次のセルを取得
        val nextCell = getNextCellToHeader(parent, contactPoint) ?: return  // 次のセルがない
        // nextCellのインデックスを取得
        val nextCellPosition = parent.getChildAdapterPosition(nextCell)
        if(nextCellPosition == StickyHeaderHandler.HEADER_POSITION_NOT_FOUND) return

        // nextCellがHeaderかどうかの判定
        if (mStickyHeaderListener.isHeader(nextCellPosition)) {
            // 既存のStickyヘッダーを押し上げる
            moveHeader(c, mCurrentHeaderView, nextCell)
            return
        }

        // Stickyヘッダーの描画
        drawHeader(c, mCurrentHeaderView)
    }

    // HeaderのViewを取得
    private fun getHeaderView(itemPosition: Int, parent: RecyclerView): View? {
        val headerPosition = mStickyHeaderListener.getHeaderPosition(itemPosition)
        val layoutResId = mStickyHeaderListener.getHeaderLayout(headerPosition)
        // Headerレイアウトをinflate
        val headerView = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
        // データバインディング
        mStickyHeaderListener.bindHeaderData(headerView, headerPosition)
        return headerView
    }

    // Headerレイアウトのセット
    private fun fixLayoutSize(parent: ViewGroup, headerView: View?) {
        headerView?:return

        // RecyclerViewのSpecを取得
        val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
        val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)

        // Header ViewのSpecを取得
        val headerWidthSpec = ViewGroup.getChildMeasureSpec(
                widthSpec,
                parent.paddingLeft + parent.paddingRight,
                headerView.layoutParams.width
        )
        val headerHeightSpec = ViewGroup.getChildMeasureSpec(
                heightSpec,
                parent.paddingTop + parent.paddingBottom,
                headerView.layoutParams.height
        )
        headerView.measure(headerWidthSpec, headerHeightSpec)
        headerView.layout(0, 0, headerView.measuredWidth, headerView.measuredHeight)
    }


    // Headerの次のセルを取得
    private fun getNextCellToHeader(parent: RecyclerView, contactPoint: Int): View? {
        var nextView: View? = null
        for (index in 0 until parent.childCount) {
            val child = parent.getChildAt(index)
            if (child.bottom > contactPoint) {
                if (child.top <= contactPoint) {
                    nextView = child
                    break
                }
            }
        }
        return nextView
    }

    // Stickyヘッダーを動かす
    private fun moveHeader(c: Canvas, currentHeader: View?, nextCell: View) {
        currentHeader?:return

        c.save()
        c.translate(0F, (nextCell.top - currentHeader.height).toFloat())
        currentHeader.draw(c)
        c.restore()
    }

    // Stickyヘッダーを描画する
    private fun drawHeader(c: Canvas, header: View?) {
        c.save()
        c.translate(0F, 0F)
        header!!.draw(c)
        c.restore()
    }
}
```
あとは上記のMyItemDecorationクラスをRecyclerViewにセットしてやれば終わりです。

![2021-08-01_22_24_09.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1830512/d34c3b02-43b6-b5c5-2971-2154cda28294.gif)
#### 全てのソースコード

```Item.kt
data class Item(var date: String, var isHeader: Boolean)
```

```MainActivity.kt
class MainActivity : AppCompatActivity() {

    companion object {
    const val STICKY_HEADER_SAMPLE = "STICKY_HEADER_SAMPLE"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)

        // adapterのセット
        val adapter =  MyAdapter(makeItems())
        recyclerView.adapter = adapter

        // itemDecorationのセット
        val itemDecoration = MyItemDecoration(adapter)
        recyclerView.addItemDecoration(itemDecoration)
    }


    // 2021年5月1日から今日までの日付を格納したリストを作成
    // 10の倍数をStickyHeaderとする
    private fun makeItems(): MutableList<Item>{
        val list = mutableListOf<Item>()

        // 現在の日付を元にカレンダーのインスタンスを取得
        val calender = Calendar.getInstance()

        // 2021年5月1日
        val pastCalender = Calendar.getInstance().apply {
            set(2021,5,1)
        }

        while (calender.after(pastCalender)){
            val day = calender.get(Calendar.DATE)
            val month = calender.get(Calendar.MONTH).plus(1)
            val date = "${month}月${day}日"

            // 10の倍数であればSticky Headerとする
            val isHeader = (day % 10) == 0

            val item = Item(date = date, isHeader = isHeader)
            list.add(item)

            calender.add(Calendar.DATE,-1)
        }

        return list
    }
}
```

```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

</androidx.constraintlayout.widget.ConstraintLayout>
```

```MyAdapter.kt
class MyAdapter(private val mItemList: List<Item>) : RecyclerView.Adapter<MyAdapter.ParentViewHolder>(),StickyHeaderHandler {

    enum class ListStyle(val type: Int){
        HeaderType(0),
        NormalType(1)
    }

    // ViewHolder機構（親クラス）
    open class ParentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    // ViewHolder機構 Header Type（子クラス）
    class HeaderViewHolder(view: View) : MyAdapter.ParentViewHolder(view) {
         val headerDate: TextView = view.findViewById(R.id.headerDate)
    }

    // ViewHolder機構 Normal type（子クラス）
    class NormalViewHolder(view: View) : MyAdapter.ParentViewHolder(view) {
         val date: TextView = view.findViewById(R.id.date)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParentViewHolder {
         return when(viewType){
            // HeaderType
            ListStyle.HeaderType.type -> {
                val view =  LayoutInflater.from(parent.context).inflate(R.layout.header_layout, parent,false)
                HeaderViewHolder(view)
            }
            // NormalType
            else -> {
                val view =  LayoutInflater.from(parent.context).inflate(R.layout.normal_layout, parent,false)
                NormalViewHolder(view)
            }
        }
    }

    override fun onBindViewHolder(holder: ParentViewHolder, position: Int) {
        val item = mItemList[position]
        when(holder){
            // HeaderType
            is HeaderViewHolder -> {
                holder.headerDate.text =  item.date
            }
            // NormalType
            is  NormalViewHolder -> {
                holder.date.text = item.date
            }
        }
    }

    override fun getItemCount(): Int = mItemList.size

    override fun getItemViewType(position: Int): Int {
        val item = mItemList[position]
        return if(item.isHeader){
            ListStyle.HeaderType.type
        }else{
            ListStyle.NormalType.type
        }
    }

    // 表示中の一番上のセルからリストの先頭(一番上)まで遡り、ヘッダーがあればそのインデックスを、なければ -1を返す
    override fun getHeaderPosition(itemPosition: Int): Int {
        var headerPosition = StickyHeaderHandler.HEADER_POSITION_NOT_FOUND

        var targetItemPosition = itemPosition
        do {
            if (isHeader(targetItemPosition)) {
                headerPosition = targetItemPosition
                break
            }
            targetItemPosition -= 1
        } while (targetItemPosition >= 0)
        return headerPosition
    }

    // Header用のレイアウトを返す
    override fun getHeaderLayout(headerPosition: Int): Int {
        return R.layout.header_layout
    }

    // Headerセルにてデータをbindする
    override fun bindHeaderData(header: View?, headerPosition: Int) {
        header?:return

        val headerItem = mItemList[headerPosition]
        if (headerItem.isHeader) {
            val headerDate = header.findViewById(R.id.headerDate) as TextView
            headerDate.text = headerItem.date
        }
    }

    // Headerかどうかの判定
    override fun isHeader(itemPosition: Int): Boolean {
        val item = mItemList[itemPosition]
        return item.isHeader
    }
}
```


```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

</androidx.constraintlayout.widget.ConstraintLayout>
```

```header_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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="wrap_content"
    tools:context=".MainActivity"
    android:background="#eee"
    android:paddingTop="25dp"
    android:paddingBottom="25dp">

    <TextView
        android:id="@+id/headerDate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/black"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

```normal_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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="wrap_content"
    android:background="@drawable/normal_layout_bg"
    tools:context=".MainActivity"
    android:paddingTop="25dp"
    android:paddingBottom="25dp">

    <TextView
        android:id="@+id/date"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/black"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

```MyItemDecoration.kt
class MyItemDecoration(private val mStickyHeaderListener: StickyHeaderHandler) : RecyclerView.ItemDecoration() {

    // Header View
    private var mCurrentHeaderView: View? = null

    // RecyclerViewのセルが表示される度に呼ばれる
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)

        // 表示中のリスト内のトップViewを取得
        val topChildView: View = parent.getChildAt(0)?:return

        // topChildViewのインデックス
        val topChildViewPosition = parent.getChildAdapterPosition(topChildView)

        // topChildViewPosition取得失敗時はこれ以降の処理を行わない
        if (topChildViewPosition == RecyclerView.NO_POSITION) return

        // 直近のHeaderPositionのインデックスを取得
        val prevHeaderPosition = mStickyHeaderListener.getHeaderPosition(topChildViewPosition)

        // 直近にHeaderが存在しない場合はこれ以降の処理を行わない
        if(prevHeaderPosition == StickyHeaderHandler.HEADER_POSITION_NOT_FOUND) return

        // 直近にHeaderが存在する → topChildViewPositionにHeaderが存在する
        mCurrentHeaderView = getHeaderView(topChildViewPosition, parent)

        // 現在のHeaderレイアウトのセット
        fixLayoutSize(parent, mCurrentHeaderView)

        // 現在のHeaderのBottom Positionを取得 (親Viewからの相対距離)
        val contactPoint = mCurrentHeaderView!!.bottom

        // Headerの次のセルを取得
        val nextCell = getNextCellToHeader(parent, contactPoint) ?: return  // 次のセルがない
        // nextCellのインデックスを取得
        val nextCellPosition = parent.getChildAdapterPosition(nextCell)
        if(nextCellPosition == StickyHeaderHandler.HEADER_POSITION_NOT_FOUND) return

        // nextCellがHeaderかどうかの判定
        if (mStickyHeaderListener.isHeader(nextCellPosition)) {
            // 既存のStickyヘッダーを押し上げる
            moveHeader(c, mCurrentHeaderView, nextCell)
            return
        }

        // Stickyヘッダーの描画
        drawHeader(c, mCurrentHeaderView)
    }

    // HeaderのViewを取得
    private fun getHeaderView(itemPosition: Int, parent: RecyclerView): View? {
        val headerPosition = mStickyHeaderListener.getHeaderPosition(itemPosition)
        val layoutResId = mStickyHeaderListener.getHeaderLayout(headerPosition)
        // Headerレイアウトをinflate
        val headerView = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
        // データバインディング
        mStickyHeaderListener.bindHeaderData(headerView, headerPosition)
        return headerView
    }

    // Headerレイアウトのセット
    private fun fixLayoutSize(parent: ViewGroup, headerView: View?) {
        headerView?:return

        // RecyclerViewのSpecを取得
        val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
        val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)

        // Header ViewのSpecを取得
        val headerWidthSpec = ViewGroup.getChildMeasureSpec(
                widthSpec,
                parent.paddingLeft + parent.paddingRight,
                headerView.layoutParams.width
        )
        val headerHeightSpec = ViewGroup.getChildMeasureSpec(
                heightSpec,
                parent.paddingTop + parent.paddingBottom,
                headerView.layoutParams.height
        )
        headerView.measure(headerWidthSpec, headerHeightSpec)
        headerView.layout(0, 0, headerView.measuredWidth, headerView.measuredHeight)
    }


    // Headerの次のセルを取得
    private fun getNextCellToHeader(parent: RecyclerView, contactPoint: Int): View? {
        var nextView: View? = null
        for (index in 0 until parent.childCount) {
            val child = parent.getChildAt(index)
            if (child.bottom > contactPoint) {
                if (child.top <= contactPoint) {
                    nextView = child
                    break
                }
            }
        }
        return nextView
    }

    // Stickyヘッダーを動かす
    private fun moveHeader(c: Canvas, currentHeader: View?, nextCell: View) {
        currentHeader?:return

        c.save()
        c.translate(0F, (nextCell.top - currentHeader.height).toFloat())
        currentHeader.draw(c)
        c.restore()
    }

    // Stickyヘッダーを描画する
    private fun drawHeader(c: Canvas, header: View?) {
        c.save()
        c.translate(0F, 0F)
        header!!.draw(c)
        c.restore()
    }
}
```


```StickyHeaderHandler.kt
interface StickyHeaderHandler {
    companion object {
        const val HEADER_POSITION_NOT_FOUND = -1
    }

    /** StickyHeaderのポジションを返す */
    fun getHeaderPosition(itemPosition: Int): Int
    /** StickyHeaderのレイアウトIDを返す */
    fun getHeaderLayout(headerPosition: Int): Int
    /** StickyHeaderにデーターを渡す */
    fun bindHeaderData(header: View?, headerPosition: Int)
    /** リストがヘッダーかどうかを判定する */
    fun isHeader(itemPosition: Int): Boolean
}
```

## 終わりに
ライブラリを使うより、描画のロジックが分かりやすいはずです。

では!^^