Last active
January 26, 2022 16:18
-
-
Save UbadahJ/70c7ab57bdad85b29bd6e154a01511e5 to your computer and use it in GitHub Desktop.
Simple walkthrough on how to create sticky header in Android (https://medium.com/@ubadahjafry/creating-sticky-headers-with-viewbinding-and-listadapter-5bbead3e7b38)
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.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