Last active
September 11, 2019 14:15
-
-
Save hrules6872/563970ccfd61ed771f873aa7c037c874 to your computer and use it in GitHub Desktop.
ItemDecorator for sticky headers in Kotlin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Original (which I couldn't fork it 🤷♀️): https://gist.github.com/filipkowicz/1a769001fae407b8813ab4387c42fcbd by @filipkowicz