DEV Community

loading...

Android RecyclerView StickyHeader without external library

bigyan4424 profile image Bigyan Thapa Updated on ・3 min read

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
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:

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.

// 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

// 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.

Discussion

pic
Editor guide
Collapse
rmdan90 profile image
rmdan90

Thank you for the great tutorial, It would be great if you share the project link.

Collapse
bigyan4424 profile image
Bigyan Thapa Author

I am glad you found it helpful. I will try to put together a sample project soon.