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
andsecond
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.
Top comments (2)
Thank you for the great tutorial, It would be great if you share the project link.
I am glad you found it helpful. I will try to put together a sample project soon.