Skip to content

Instantly share code, notes, and snippets.

@antonKozyriatskyi
Created February 16, 2019 13:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save antonKozyriatskyi/6f6ae6bdb6e2bc98e9e82a48dc63ab07 to your computer and use it in GitHub Desktop.
Save antonKozyriatskyi/6f6ae6bdb6e2bc98e9e82a48dc63ab07 to your computer and use it in GitHub Desktop.
Sticky headers for RecyclerView. Supports only one sticky header view type.
package kozyriatskyi.anton.sked.week.stickyheaders
import android.graphics.Canvas
import android.support.v4.view.ViewCompat
import android.support.v7.widget.RecyclerView
import android.util.SparseIntArray
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
abstract class StickyHeaderAdapter<T : RecyclerView.ViewHolder> : RecyclerView.Adapter<T>() {
abstract fun getHeaderPosition(itemPosition: Int): Int
abstract fun getHeaderViewHolder(parent: ViewGroup): RecyclerView.ViewHolder
abstract fun bindHeaderViewHolder(holder: RecyclerView.ViewHolder, headerPosition: Int)
abstract fun isHeader(itemPosition: Int): Boolean
}
class StickyHeaderItemDecoration(recyclerView: RecyclerView) : RecyclerView.ItemDecoration() {
private lateinit var headerHolder: RecyclerView.ViewHolder
private var currentStickyHeaderPosition: Int = RecyclerView.NO_POSITION
private val headerPositionsByItemPositions = SparseIntArray(7)
private val adapter: StickyHeaderAdapter<RecyclerView.ViewHolder> = kotlin.run {
recyclerView.adapter as? StickyHeaderAdapter<RecyclerView.ViewHolder>
?: throw IllegalArgumentException("adapter must not be null and must be a descendant of StickyHeaderAdapter")
}
init {
recyclerView.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
override fun onInterceptTouchEvent(recyclerView: RecyclerView, motionEvent: MotionEvent): Boolean {
return motionEvent.y <= headerHolder.itemView.height
}
})
val dataObserver = object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
currentStickyHeaderPosition = RecyclerView.NO_POSITION
preprocessHeaders(recyclerView)
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
onChanged()
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
onChanged()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
onChanged()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
onChanged()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
onChanged()
}
}
adapter.registerAdapterDataObserver(dataObserver)
}
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 currentHeader = getHeaderViewForItemPosition(topChildPosition, parent) ?: return
val contactPoint = currentHeader.bottom
val childInContact = getChildInContact(parent, contactPoint)
if (childInContact != null && adapter.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact)
return
}
drawHeader(c, currentHeader)
}
private fun getHeaderViewForItemPosition(itemPosition: Int, parent: RecyclerView): View? {
val headerPosition = headerPositionsByItemPositions[itemPosition]
if (headerPosition != currentStickyHeaderPosition) {
currentStickyHeaderPosition = headerPosition
adapter.bindHeaderViewHolder(headerHolder, headerPosition)
measureAndLayoutHeader(parent, headerHolder.itemView)
}
return headerHolder.itemView
}
private fun preprocessHeaders(parent: RecyclerView) {
headerHolder = adapter.getHeaderViewHolder(parent)
headerPositionsByItemPositions.clear()
var itemPosition = adapter.itemCount - 1
while (itemPosition > 0) {
val headerPosition = adapter.getHeaderPosition(itemPosition)
for (i in itemPosition downTo headerPosition - 1) {
headerPositionsByItemPositions.put(i, headerPosition)
}
itemPosition = headerPosition - 1
}
}
private fun drawHeader(c: Canvas, header: View) {
header.draw(c)
}
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? {
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
if (child.bottom > contactPoint && child.top <= contactPoint) {
// This child overlaps the contactPoint
return child
}
}
return null
}
private fun measureAndLayoutHeader(parent: RecyclerView, view: View) {
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
val paddingEnd = ViewCompat.getPaddingEnd(parent)
val paddingStart = ViewCompat.getPaddingStart(parent)
val horizontalPadding = paddingStart + paddingEnd
val verticalPadding = parent.paddingTop + parent.paddingBottom
val childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, horizontalPadding, view.layoutParams.width)
val childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, verticalPadding, view.layoutParams.height)
view.measure(childWidthSpec, childHeightSpec)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment