Skip to content

Instantly share code, notes, and snippets.

@DoruAdryan
Created April 1, 2022 08:46
Show Gist options
  • Save DoruAdryan/96aa05c6f7c2561a85d258847c4d2f81 to your computer and use it in GitHub Desktop.
Save DoruAdryan/96aa05c6f7c2561a85d258847c4d2f81 to your computer and use it in GitHub Desktop.
sticky item decoration
class TopStickyItemDecoration : ItemDecoration() {
// for logging purposes only (avoid multiple logs with same values).
private var lastChildTop: Int = -1
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (state.itemCount <= 0) return
val lm = requireNotNull(parent.layoutManager) as LinearLayoutManager
require(lm.orientation == VERTICAL)
val parentTopPadding = if (parent.clipToPadding) parent.paddingTop else 0
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent[i]
val vhForChild = parent.getChildViewHolder(child)
if (vhForChild !is StickyAdapter.Sticky) continue
val position = parent.getChildAdapterPosition(child)
if (position < 0) continue
stickChildToParentTop(parent, child, parentTopPadding)
}
}
/**
* Translate child to keep at same level as parent top, to look like it is sticky
* Explanation: if difference between childTop and parentPadding is negative, then we crossed parent top with our
* child (it is getting out of viewport), so we translate down the child to maintain it's position.
*
* ie. if parentPadding = 100, when:
* childTop = 120 -> translationY = 0 (lower than parent top -> no translation needed)
* childTop = 100 -> translationY = 0 (equal to parent top -> no translation needed)
* childTop = 90 -> translationY = 10 (higher than parent top -> translate down)
*/
private fun stickChildToParentTop(parent: RecyclerView, child: View, parentTopPadding: Int) {
val childTop = child.top
val negativeTopDiff = (childTop - parentTopPadding).coerceAtMost(0)
val translationY = -negativeTopDiff // invert negative difference to translate towards bottom
if (childTop != lastChildTop) {
lastChildTop = childTop
Timber.d("childTop:$childTop, parentPadding:$parentTopPadding, translationY:$translationY")
}
if (translationY > 0) {
// in order to draw view above others we need a higher elevation
raiseViewElevationAboveOtherChildren(parent, child)
} else {
resetViewElevation(child)
}
child.translationY = translationY.toFloat()
}
private fun raiseViewElevationAboveOtherChildren(recyclerView: RecyclerView, view: View) {
var originalElevation: Float? = view.getTag(R.id.sticky_item_previous_elevation) as Float?
if (originalElevation == null) {
originalElevation = view.elevation
view.elevation = 1f + findMaxElevation(recyclerView)
view.setTag(R.id.sticky_item_previous_elevation, originalElevation)
}
}
private fun findMaxElevation(recyclerView: RecyclerView): Float = recyclerView.children.maxOf { it.elevation }
private fun resetViewElevation(view: View) {
val previousElevationTag = view.getTag(R.id.sticky_item_previous_elevation) as Float? ?: return
view.elevation = previousElevationTag
view.setTag(R.id.sticky_item_previous_elevation, null)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment