Created
August 4, 2023 05:50
-
-
Save preetham1316/d21be9e6987be601a7d5507081c127a8 to your computer and use it in GitHub Desktop.
Sticky Header with Click Listeners for Recycler View
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.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() | |
} | |
} |
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
<?xml version="1.0" encoding="utf-8"?> | |
<resources> | |
<item name="sticky_header_container" type="id"/> | |
</resources> |
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 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 | |
} |
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 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