Android RecyclerView StickyHeader without external library
Tags: android, recyclerview, kotlin, stickyheader
Published: 2019-12-24 13:52:55

There are several occasions when we need to implement sticky header for some list of data displayed in `RecyclerView`. And of course, Android doesn't have a native UI component to implement this easily. There are several third-party libraries which we can use to achieve this functionality. But using third party libraries always comes at a cost. There is always a doubt of whether the library we are using will be updated for future versions or not, apart from the additional overhead of LOC and app size that these libraries add. 
One simpler way of achieving this same behavior without having to use any third party library is to write a custom `RecyclerView ItemDecoration` and override `onDrawOver(canvas: Canvas, parent: RecyclerView, state: State)` function.
Below is the description on how to do that. Please feel free to customize this implementation to meet your requirements:

### Custom ItemDecoration

This is a wrapper around `ItemDecoration` abstract class. As it is stated on the documentation, these `ItemDecorations` are drawn in the order they were added, before the views and after the items.

In this implementation of the custom decoration, we are going to provide three things as parameter:
  * RecyclerView adapter
  * Root view, e.g. the fragment's root where the RecyclerView exists
  * Layout resource id for the header to be used

```kotlin
class StickyHeaderDecoration<B : ViewDataBinding>(
val adapter: StickyHeaderAdapter<*>, root: View, 
@LayoutRes headerLayout: Int) : ItemDecoration() {

  //lazily initialize the binding instance for the header view
  private val headerBinding:B by lazy {
    DataBindingUtil.inflate<B>(LayoutInflater.from(root.context),
    headerLayout, null, false)
  }

  override fun onDrawOver(canvas: Canvas, parent: RecyclerView, 
  state: State) {
  super.onDrawOver(canvas, parent, state)
  /* 
    This needs to be customized, please continue reading for the    implementation
  */
  }
}
```
Above is the basic structure for the custom ItemDecoration. Now we have to understand what customization goes inside the `onDrawOver` function. 
Let's continue customizing this function:
```kotlin
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, sate: State) {
  super.onDrawOver(canvas, parent, state)
  
  val topChild = parent.getChildAt(0)
  val secondChild = parent.getChildAt(1)

  parent.getChildAdapterPosition(topChild).let { topPosition ->
    val header = adapter.getHeaderForCurrentPosition(topPosition)
    headerView.tvStickyHeader.text = header
    
    layoutHeaderView(topChild)
    canvas.drawHeaderView(topChild, secondChild)
  }
}

private fun layoutHeaderView(topView: View) {
  headerView.measure(
    MeasureSpec.makeMeasureSpec(topView.width, MeasureSpec.EXACTLY),
    MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
   )
  headerView.layout(topView.left, 0, topView.right, headerView.measuredHeight)
}

private fun Canvas.drawHeaderView(topView: View, secondChild: View?) {
  save()
  translate(0f, calculateHeaderTop(topView, secondChild)
  headerView.draw(this)
  restore()
}

private fun calculateHeaderTop(topView: View, secondChild: View?) : Float = 
secondChild?.let { second -> 
  // If there is any custom height to be added, calculate here
  if (secondView.findViewById(headerView.id)?.visibility != View.GONE) {
    secondView.top.toFloat()
  } else {
    maxOf(topView.top, 0).toFloat()
  }
} ?: maxOf(topView.top, 0).toFloat()
```
#### onDrawOver
In this function,
  * we get the reference for the `first` and `second` item of the RecyclerView
  * we retrieve the `header` text for the top child
  * measure the header
  * calculate header top and draw the header

#### adapter.getHeaderForCurrentPosition(topPosition)
This adapter function returns the `header` text for the given position.
e.g. ```kotlin
// items is the list of objects displayed in the RecyclerView
fun getHeaderForCurrentPosition(position: Int) = if (position in items.indices) {
    items[position]
  } else {
    ""
  }
```

#### - layoutHeaderView(topView: View)
This function measures the `EXACT` width of the header view to be drawn. Note that the `height` is `unspecified` as we are using the header view's `measuredHeight`.

##### - Canvas.drawHeaderView(topView: View, secondChild: View?)
This function `saves` the canvas, `translates` the canvas to the header view's calculated top, `draws` the header and `restores` the canvas.

##### - calculateHeaderTop(topView: View, secondChild: View?)
This function calculates the `top` of the header. If second view is visible, we take reference of secondView's top, else the header's top is topView's top.

#### Basic Use
```kotlin
// This custom decoration can be used in a fragment as follows
class SomeFragment() {
  // initialization part...
  fragmentBinding.itemList.addItemDecoration(
      StickyHeaderItemDecoration<ViewStickyHeaderBinding>(
        someAdapter, fragmentBinding.root, R.layout.view_sticky_header
      )
    )
```
### Basic Scroll Behavior
When we initialize the list with the items, the header text for the first item will be displayed as soon as all data is inflated. Then as we scroll through the list, when the `top` of the `second` item (plus some threshold is optional) touches the `bottom` of the `sticky header` then the `header` for the second item is drawn. And this continues as we scroll up through the list. 
As we scroll down through the list, the reverse behavior occurs and the header text are drawn.
The adapter class is responsible for providing the header text for any given position if the position is within bounds of the items size.

Please provide feedback in the comment section.