Skip to content

Instantly share code, notes, and snippets.

@preetham1316
Created August 4, 2023 05:50
Show Gist options
  • Save preetham1316/d21be9e6987be601a7d5507081c127a8 to your computer and use it in GitHub Desktop.
Save preetham1316/d21be9e6987be601a7d5507081c127a8 to your computer and use it in GitHub Desktop.
Sticky Header with Click Listeners for Recycler View
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.stickyheader.databinding.FragmentHomeListBinding
import com.example.stickyheader.header.StickyHeaderItemDecorator
import com.example.stickyheader.model.ItemModel
class HomeFragment : Fragment() {
private var _binding: FragmentHomeListBinding? = null
private val binding get() = _binding
private var stickyHeaderItemDecorator: StickyHeaderItemDecorator? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
_binding = FragmentHomeListBinding.inflate(inflater, container, false)
return binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initAdapter()
initStickyItemDecorator()
}
private fun initStickyItemDecorator() {
stickyHeaderItemDecorator?.clearReferences()
stickyHeaderItemDecorator = null
stickyHeaderItemDecorator = StickyHeaderItemDecorator()
binding?.recyclerList?.let {
val adapter = it.adapter
if (adapter is ListRecyclerAdapter) {
stickyHeaderItemDecorator?.attachRecyclerView(
adapter,
it,
adapter
)
}
}
}
private fun initAdapter() {
binding?.recyclerList?.apply {
layoutManager = LinearLayoutManager(context)
adapter = ListRecyclerAdapter(getDummyItems()) {
(activity as? MainActivity)?.launchDetailPage(it)
}
}
}
private fun getDummyItems(): List<ItemModel> {
return arrayListOf(
ItemModel(),
ItemModel(),
ItemModel(isHeader = true),
ItemModel(),
ItemModel(),
ItemModel(),
ItemModel(),
ItemModel(isHeader = true),
ItemModel(),
ItemModel(),
ItemModel(),
ItemModel(isHeader = true),
ItemModel(),
ItemModel(),
ItemModel(),
ItemModel(),
ItemModel(),
ItemModel()
)
}
companion object {
@JvmStatic
fun newInstance() = HomeFragment()
}
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="sticky_header_container" type="id"/>
</resources>
class ListRecyclerAdapter(
private val itemsList: List<ItemModel>,
private val onHeaderItemClick: (Int) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(),
StickyHeaderItemDecorator.StickyHeaderInterface {
companion object {
const val TYPE_HEADER = 1
const val TYPE_CONTENT = 2
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_HEADER -> HeaderViewHolder(
ItemHeaderBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
else -> ItemBodyViewHolder(
ItemListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> {
holder.contentView.text = "Header $position"
holder.nextButton.setOnClickListener { onHeaderItemClick(position) }
}
is ItemBodyViewHolder -> holder.contentView.text = "Content body at position $position"
}
}
override fun getItemCount(): Int = itemsList.size
override fun getItemViewType(position: Int) =
if (itemsList[position].isHeader) TYPE_HEADER else TYPE_CONTENT
inner class ItemBodyViewHolder(binding: ItemListBinding) :
RecyclerView.ViewHolder(binding.root) {
val contentView: TextView = binding.content
}
inner class HeaderViewHolder(binding: ItemHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
val contentView: TextView = binding.content
val nextButton: TextView = binding.nextText
}
override fun isHeader(itemPosition: Int) = itemsList[itemPosition].isHeader
}
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.stickyheader.ListRecyclerAdapter
import com.example.stickyheader.R
class StickyHeaderItemDecorator {
private lateinit var listener: StickyHeaderInterface
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: ListRecyclerAdapter
private val currentHeaderViewMap: MutableMap<Int, Boolean> by lazy { mutableMapOf() }
private var stickyHeaderContainer: LinearLayout? = null
fun attachRecyclerView(
listener: StickyHeaderInterface,
recyclerView: RecyclerView,
adapter: ListRecyclerAdapter
) {
this.listener = listener
this.recyclerView = recyclerView
this.adapter = adapter
initContainer()
clearHeaderViews()
refreshHeader()
}
private fun initContainer() {
stickyHeaderContainer =
(recyclerView.parent as? ViewGroup)?.findViewById(R.id.sticky_header_container)
if (stickyHeaderContainer == null) {
// RecyclerViews parent should be FrameLayout or RelativeLayout to add sticky header at top
val linearLayout = LinearLayout(recyclerView.context)
stickyHeaderContainer = linearLayout
val params = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
stickyHeaderContainer?.id = R.id.sticky_header_container
stickyHeaderContainer?.orientation = LinearLayout.VERTICAL
(recyclerView.parent as? ViewGroup)?.addView(stickyHeaderContainer, params)
}
recyclerView.addOnScrollListener(onScrollChangeListener)
}
private fun clearHeaderViews() {
currentHeaderViewMap.clear()
stickyHeaderContainer?.removeAllViews()
}
private fun addHeaderViewFromPosition(position: Int) {
if (currentHeaderViewMap[position] == true) return // return if header already added
stickyHeaderContainer?.let {
val vh = adapter.createViewHolder(it, adapter.getItemViewType(position))
adapter.bindViewHolder(vh, position)
val view = vh.itemView
view.tag = position
it.addView(
view,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
currentHeaderViewMap[position] = true
recyclerView.post { it.requestLayout() }
}
}
private fun removeHeaderViewFromPosition(position: Int) {
if (currentHeaderViewMap[position] != true) return // already removed
val view = stickyHeaderContainer?.findViewWithTag<View>(position)
view?.let {
stickyHeaderContainer?.removeView(it)
}
currentHeaderViewMap[position] = false
}
private fun drawHeaders() {
val layoutManager = recyclerView.layoutManager as? LinearLayoutManager?
var topChildPosition = layoutManager?.findFirstVisibleItemPosition() ?: 0
var i = 0
while (i <= topChildPosition) {
if (i == RecyclerView.NO_POSITION) break
if (listener.isHeader(i)) {
removeExistingHeaders(i) // Can be removed if needed stacked type sticky headers
addHeaderViewFromPosition(i)
topChildPosition++
}
i++
}
}
private fun removeExistingHeaders(i: Int) {
currentHeaderViewMap.forEach { (key, value) ->
if ((!listener.isHeader(key) || key < i) && value) {
removeHeaderViewFromPosition(key)
}
}
}
private fun removeInvalidHeaders() {
val layoutManager = recyclerView.layoutManager as? LinearLayoutManager?
val topChildPosition =
layoutManager?.findFirstVisibleItemPosition() ?: 0
currentHeaderViewMap.forEach { (key, value) ->
if ((!listener.isHeader(key) || key > topChildPosition) && value) {
removeHeaderViewFromPosition(key)
}
}
}
private val onScrollChangeListener: RecyclerView.OnScrollListener =
object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
refreshHeader()
}
}
fun refreshHeader() {
recyclerView.post {
removeInvalidHeaders()
drawHeaders()
}
}
fun clearReferences() {
recyclerView.removeOnScrollListener(onScrollChangeListener)
}
interface StickyHeaderInterface {
fun isHeader(itemPosition: Int): Boolean
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment