Skip to content

Instantly share code, notes, and snippets.

@florent37
Created October 21, 2020 19:07
Show Gist options
  • Save florent37/56470d86a41fbee4642cd8965382e803 to your computer and use it in GitHub Desktop.
Save florent37/56470d86a41fbee4642cd8965382e803 to your computer and use it in GitHub Desktop.
import android.graphics.Canvas
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
class StickHeaderItemDecoration(private val adapter: StickyAdapter) : ItemDecoration() {
private var stickyHeaderHeight = 0
private var currentTopChildPosition: Int? = null
private var currentTopChildHeaderPosition: Int? = null
private var currentHeaderViewHolder: RecyclerView.ViewHolder? = null
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 headerPos = if (currentTopChildPosition == topChildPosition) {
currentTopChildHeaderPosition
} else {
currentHeaderViewHolder = null
currentTopChildHeaderPosition = getHeaderPositionForItem(topChildPosition)
currentTopChildHeaderPosition
}
currentTopChildPosition = topChildPosition
if (headerPos != null) {
val currentHeader = if (currentHeaderViewHolder != null) {
currentHeaderViewHolder!!.itemView
} else {
findHeaderViewForItem(headerPos, parent)
}
val contactPoint = currentHeader.bottom
val childInContact = getChildInContact(parent, contactPoint, headerPos)
if (childInContact != null && adapter.isStickyHeader(
parent.getChildAdapterPosition(
childInContact
)
)
) {
moveHeader(c, currentHeader, childInContact)
return
}
drawHeader(c, currentHeader)
}
}
private fun getHeaderPositionForItem(itemPosition: Int): Int? {
for (i in itemPosition downTo 0) {
if (adapter.isStickyHeader(i)) {
return i
}
}
return null
}
private fun findHeaderViewForItem(headerPosition: Int, parent: RecyclerView): View {
val headerType = adapter.getItemViewType(headerPosition)
val viewHolder = adapter.onCreateViewHolder(parent, headerType)
val header = viewHolder.itemView
adapter.onBindViewHolder(viewHolder, headerPosition)
fixLayoutSize(parent, header)
currentHeaderViewHolder = viewHolder
return header
}
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,
currentHeaderPos: Int
): View? {
var childInContact: View? = null
for (i in 0 until parent.childCount) {
var heightTolerance = 0
val child = parent.getChildAt(i)
//measure height tolerance with child if child is another header
if (currentHeaderPos != i) {
val isChildHeader = adapter.isStickyHeader(parent.getChildAdapterPosition(child))
if (isChildHeader) {
heightTolerance = stickyHeaderHeight - child.height
}
}
//add heightTolerance if child top be in display area
var childBottomPosition: Int
childBottomPosition = if (child.top > 0) {
child.bottom + heightTolerance
} else {
child.bottom
}
if (childBottomPosition > contactPoint) {
if (child.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.also {
stickyHeaderHeight = it
})
}
fun attach(recyclerView: RecyclerView) {
recyclerView.addItemDecoration(this)
}
fun detach(recyclerView: RecyclerView) {
recyclerView.removeItemDecoration(this)
}
interface StickyAdapter {
/**
* It's directly available in an adapter
* This method gets called by [StickHeaderItemDecoration] to get layout ViewHolder and View
*/
fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder
/**
* This method gets called by [StickHeaderItemDecoration] to setup the header ViewHolder.
* It's directly available in an adapter
*/
fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int)
/**
* This method gets called by [StickHeaderItemDecoration] to verify whether the item represents a header.
* @param itemPosition int.
* @return true, if item at the specified adapter's position represents a header.
*/
fun isStickyHeader(itemPosition: Int): Boolean
fun getItemViewType(position: Int): Int
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment