Skip to content

Instantly share code, notes, and snippets.

@hrules6872
Last active September 11, 2019 14:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hrules6872/563970ccfd61ed771f873aa7c037c874 to your computer and use it in GitHub Desktop.
Save hrules6872/563970ccfd61ed771f873aa7c037c874 to your computer and use it in GitHub Desktop.
ItemDecorator for sticky headers in Kotlin
import android.graphics.*
import android.view.*
import android.view.MotionEvent.ACTION_DOWN
import androidx.recyclerview.widget.RecyclerView
class StickyHeaderItemDecoration(
parent: RecyclerView,
private val isHeader: (position: Int) -> Boolean
) : RecyclerView.ItemDecoration() {
private var currentHeader: Pair<Int, RecyclerView.ViewHolder>? = null
init {
parent.adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
currentHeader = null
}
})
parent.doOnEachNextLayout {
currentHeader = null
}
parent.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
override fun onInterceptTouchEvent(
recyclerView: RecyclerView,
motionEvent: MotionEvent
): Boolean = if (motionEvent.action == ACTION_DOWN) {
motionEvent.y <= currentHeader?.second?.itemView?.bottom ?: 0
} else false
})
}
override fun onDrawOver(
canvas: Canvas,
parent: RecyclerView,
state: RecyclerView.State
) {
super.onDrawOver(canvas, 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 (isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(canvas, headerView, childInContact)
return
}
drawHeader(canvas, headerView)
}
private fun getHeaderViewForItem(
position: Int,
parent: RecyclerView
): View? {
if (parent.adapter == null) return null
val headerPosition = getHeaderPositionForItem(position)
val headerType = parent.adapter?.getItemViewType(headerPosition) ?: return null
if (currentHeader?.first == headerPosition && currentHeader?.second?.itemViewType == headerType) return currentHeader?.second?.itemView
val headerHolder = parent.adapter?.createViewHolder(parent, headerType)
headerHolder?.let { safeHeaderHolder ->
parent.adapter?.onBindViewHolder(safeHeaderHolder, headerPosition)
fixLayoutSize(parent, safeHeaderHolder.itemView)
currentHeader = headerPosition to safeHeaderHolder
}
return headerHolder?.itemView
}
private fun drawHeader(
canvas: Canvas,
header: View
) = with(canvas) {
save()
translate(0f, 0f)
header.draw(this)
restore()
}
private fun moveHeader(
canvas: Canvas,
currentHeader: View,
nextHeader: View
) = with(canvas) {
save()
translate(0f, (nextHeader.top - currentHeader.height).toFloat())
currentHeader.draw(this)
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 bounds = Rect()
parent.getDecoratedBoundsWithMargins(child, bounds)
if (bounds.bottom > contactPoint && bounds.top <= contactPoint) {
// this child overlaps the contactPoint
childInContact = child
break
}
}
return childInContact
}
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.apply {
measure(childWidthSpec, childHeightSpec)
layout(0, 0, view.measuredWidth, view.measuredHeight)
}
}
private fun getHeaderPositionForItem(position: Int): Int {
var headerPosition = 0
var currentPosition = position
do {
if (isHeader(currentPosition)) {
headerPosition = currentPosition
break
}
currentPosition -= 1
} while (currentPosition >= 0)
return headerPosition
}
}
inline fun View.doOnEachNextLayout(crossinline action: (view: View) -> Unit) {
addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ -> action(view) }
}
@hrules6872
Copy link
Author

Original (which I couldn't fork it 🤷‍♀️): https://gist.github.com/filipkowicz/1a769001fae407b8813ab4387c42fcbd by @filipkowicz

@filipkowicz
Copy link

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment