Created
April 1, 2022 08:46
-
-
Save DoruAdryan/96aa05c6f7c2561a85d258847c4d2f81 to your computer and use it in GitHub Desktop.
sticky item decoration
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
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