Skip to content

Instantly share code, notes, and snippets.

@UbadahJ
Last active January 26, 2022 16:18
Show Gist options
  • Save UbadahJ/70c7ab57bdad85b29bd6e154a01511e5 to your computer and use it in GitHub Desktop.
Save UbadahJ/70c7ab57bdad85b29bd6e154a01511e5 to your computer and use it in GitHub Desktop.
import android.view.View
import androidx.core.view.ViewCompat
import androidx.core.view.get
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
interface StickyViewHolder<T> {
val type: Int
val root: View
fun bind(item: T)
}
class StickyHeaderManager<T>(
private val recyclerView: RecyclerView,
private val adapter: ListAdapter<T, *>,
private val header: StickyViewHolder<T>
) {
private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
bind()
}
}
private var hidden: Boolean = false
init {
recyclerView.addOnScrollListener(scrollListener)
recyclerView.onDataChangeListener {
if (ViewCompat.isLaidOut(header.root)) bind()
else header.root.post { bind() }
}
}
fun toggleVisibility() {
if (hidden) {
hidden = false
bind()
header.root.animate()
.alpha(1f)
.setListener(null)
.start()
} else {
header.root.animate()
.alpha(0f)
.setListener { hidden = (!hidden).updateVisibility() }
.start()
}
}
private fun bind() {
val listEmpty = adapter.currentList.isEmpty().updateVisibility()
if (listEmpty) return
val first = recyclerView.getOrNull(0) ?: return
val firstPos = recyclerView.getChildAdapterPosition(first)
if (!isValidPosition(firstPos)) return
val atFirst = (firstPos == 0 && first.top == recyclerView.top).updateVisibility()
if (atFirst) return
val item: T?
if (isHeader(firstPos)) {
item = adapter.currentList[firstPos]
header.root.translationY = 0f
} else {
item = findNearestHeader(firstPos)
val secondPos = firstPos + 1
if (isValidPosition(secondPos)) {
if (isHeader(secondPos)) recyclerView.getOrNull(1)?.let { translateView(it) }
else header.root.translationY = 0f
}
}
item?.let { header.bind(it) }
hidden.updateVisibility()
}
private fun translateView(view: View) {
header.root.translationY = when (view.top <= header.root.bottom) {
true -> (view.top - header.root.height).toFloat()
false -> 0f
}
}
private fun RecyclerView.getOrNull(position: Int): View? = try {
get(position)
} catch (e: Exception) {
null
}
private fun findNearestHeader(position: Int): T? {
for (i in position downTo 0) {
if (isHeader(i)) return adapter.currentList[i]
}
return null
}
private fun isHeader(position: Int) = adapter.getItemViewType(position) == header.type
private fun Boolean.updateVisibility() = also { header.root.visible = !it }
private fun isValidPosition(position: Int): Boolean {
return !(position == RecyclerView.NO_POSITION || position >= adapter.currentList.size)
}
}
var View.visible: Boolean
get() = visibility == View.VISIBLE
set(value) {
visibility = if (value) View.VISIBLE else View.GONE
}
fun RecyclerView.onDataChangeListener(action: () -> Unit) {
adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
doOnLayout { action() }
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
onChanged()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
onChanged()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
onChanged()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
onChanged()
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
onChanged()
}
})
}
fun ViewPropertyAnimator.setListener(listener: (Animator?) -> Unit): ViewPropertyAnimator {
return this.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator?) {}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
listener(animation)
}
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment