Skip to content

Instantly share code, notes, and snippets.

@philipborbon
Forked from filipkowicz/HeaderItemDecoration.kt
Last active October 7, 2019 03:23
Show Gist options
  • Save philipborbon/c2ff21c59bb527006b132a3b63a6a950 to your computer and use it in GitHub Desktop.
Save philipborbon/c2ff21c59bb527006b132a3b63a6a950 to your computer and use it in GitHub Desktop.
Item Decorator for sticky headers in Kotlin
/*
solution based on - based on Sevastyan answer on StackOverflow
changes:
- take to account views offsets
- transformed to Kotlin
- now works on viewHolders
- try to cache viewHolders between draw's
Source:
https://stackoverflow.com/questions/32949971/how-can-i-make-sticky-headers-in-recyclerview-without-external-lib/44327350#44327350
*/
import android.graphics.Canvas
import android.graphics.Rect
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
/**
* HeaderItemDecoration
*
* RecyclerView Adapter should implement HasHeaderItem
* to identify header position
*
*
*/
class HeaderItemDecoration(private val recyclerView: RecyclerView) : RecyclerView.ItemDecoration() {
private var currentHeader: Pair<Int, RecyclerView.ViewHolder>? = null
private val recyclerViewAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>
get() {
recyclerView.adapter?.let { adapter ->
if ((adapter is HasHeaderItem).not()) {
throw HasHeaderItemNotFoundException("RecyclerView Adapter should implement HasHeaderItem.")
}
return adapter
} ?: run {
throw HasHeaderItemNotFoundException("RecyclerView Adapter is null.")
}
}
init {
recyclerViewAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
// clear saved header as it can be outdated now
currentHeader = null
}
})
recyclerView.doOnEachNextLayout {
// clear saved layout as it may need layout update
currentHeader = null
}
// handle click on sticky header
recyclerView.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
override fun onInterceptTouchEvent(recyclerView: RecyclerView, motionEvent: MotionEvent): Boolean {
return if (motionEvent.action == MotionEvent.ACTION_DOWN) {
motionEvent.y <= currentHeader?.second?.itemView?.bottom ?: 0
} else false
}
})
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
val topChild = parent.getChildAt(0) ?: return
val topChildPosition = parent.getChildAdapterPosition(topChild)
if (topChildPosition == RecyclerView.NO_POSITION) {
return
}
val headerView = getHeaderViewForItem(topChildPosition, parent) ?: return
val contactPoint = headerView.bottom
val childInContact = getChildInContact(parent, contactPoint) ?: return
if ((parent.adapter as? HasHeaderItem)?.isHeaderItem(parent.getChildAdapterPosition(childInContact)) == true) {
moveHeader(c, headerView, childInContact)
return
}
drawHeader(c, headerView)
}
private fun getHeaderViewForItem(itemPosition: Int, parent: RecyclerView): View? {
if (parent.adapter == null) {
return null
}
val headerPosition = getHeaderPositionForItem(itemPosition)
if (headerPosition == -1) {
return null
}
val headerType = parent.adapter?.getItemViewType(headerPosition) ?: return null
// if match reuse viewHolder
if (currentHeader?.first == headerPosition && currentHeader?.second?.itemViewType == headerType) {
return currentHeader?.second?.itemView
}
val headerHolder = parent.adapter?.createViewHolder(parent, headerType)
if (headerHolder != null) {
parent.adapter?.onBindViewHolder(headerHolder, headerPosition)
fixLayoutSize(parent, headerHolder.itemView)
// save for next draw
currentHeader = headerPosition to headerHolder
}
return headerHolder?.itemView
}
private fun drawHeader(c: Canvas, header: View) {
c.save()
c.translate(0f, 0f)
header.draw(c)
c.restore()
}
private fun moveHeader(c: Canvas, currentHeader: View, nextHeader: View) {
c.save()
c.translate(0f, (nextHeader.top - currentHeader.height).toFloat())
currentHeader.draw(c)
c.restore()
}
private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? {
var childInContact: View? = null
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
val mBounds = Rect()
parent.getDecoratedBoundsWithMargins(child, mBounds)
if (mBounds.bottom > contactPoint) {
if (mBounds.top <= contactPoint) {
// This child overlaps the contactPoint
childInContact = child
break
}
}
}
return childInContact
}
/**
* Properly measures and layouts the top sticky header.
*
* @param parent ViewGroup: RecyclerView in this case.
*/
private fun fixLayoutSize(parent: ViewGroup, view: View) {
// Specs for parent (RecyclerView)
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
// Specs for children (headers)
val childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingLeft + parent.paddingRight, view.layoutParams.width)
val childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, view.layoutParams.height)
view.measure(childWidthSpec, childHeightSpec)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
private fun getHeaderPositionForItem(itemPosition: Int): Int {
var headerPosition = -1
var currentPosition = itemPosition
do {
if ((recyclerViewAdapter as? HasHeaderItem)?.isHeaderItem(currentPosition) == true) {
headerPosition = currentPosition
break
}
currentPosition -= 1
} while (currentPosition >= 0)
return headerPosition
}
interface HasHeaderItem {
fun isHeaderItem(position: Int): Boolean
}
class HasHeaderItemNotFoundException(message: String) : Exception(message)
}
inline fun View.doOnEachNextLayout(crossinline action: (view: View) -> Unit) {
addOnLayoutChangeListener { view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> action(view) }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment