import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
sealed class ListItem {
data class Header(val title: String) : ListItem()
data class Item(val name: String) : ListItem()
}
class StickyHeadersActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sticky_headers)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
val data = listOf(
ListItem.Header("Fruits"),
ListItem.Item("Apple"),
ListItem.Item("Banana"),
ListItem.Item("Orange"),
ListItem.Header("Vegetables"),
ListItem.Item("Carrot"),
ListItem.Item("Broccoli"),
ListItem.Item("Spinach"),
ListItem.Header("Dairy"),
ListItem.Item("Milk"),
ListItem.Item("Cheese"),
ListItem.Item("Yogurt")
)
val adapter = StickyAdapter(data)
recyclerView.adapter = adapter
recyclerView.addItemDecoration(StickyHeaderDecoration(adapter))
}
}
class StickyAdapter(private val items: List<ListItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
private const val TYPE_HEADER = 0
private const val TYPE_ITEM = 1
}
override fun getItemViewType(position: Int): Int = when(items[position]) {
is ListItem.Header -> TYPE_HEADER
is ListItem.Item -> TYPE_ITEM
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == TYPE_HEADER) {
val view = LayoutInflater.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false)
HeaderViewHolder(view)
} else {
val view = LayoutInflater.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false)
ItemViewHolder(view)
}
}
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(val item = items[position]) {
is ListItem.Header -> (holder as HeaderViewHolder).bind(item)
is ListItem.Item -> (holder as ItemViewHolder).bind(item)
}
}
fun isHeader(position: Int): Boolean = items[position] is ListItem.Header
fun getHeaderTitle(position: Int): String {
for (i in position downTo 0) {
if (isHeader(i)) {
return (items[i] as ListItem.Header).title
}
}
return ""
}
class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val textView: TextView = itemView.findViewById(android.R.id.text1)
fun bind(header: ListItem.Header) {
textView.text = header.title
textView.setBackgroundColor(0xFFE0E0E0.toInt())
textView.setTypeface(null, android.graphics.Typeface.BOLD)
}
}
class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val textView: TextView = itemView.findViewById(android.R.id.text1)
fun bind(item: ListItem.Item) {
textView.text = item.name
textView.setBackgroundColor(0xFFFFFFFF.toInt())
textView.setTypeface(null, android.graphics.Typeface.NORMAL)
}
}
}
class StickyHeaderDecoration(private val adapter: StickyAdapter) : RecyclerView.ItemDecoration() {
private val headerHeight = 100
private val paint = Paint().apply {
color = 0xFFE0E0E0.toInt()
}
private val textPaint = Paint().apply {
color = 0xFF000000.toInt()
textSize = 40f
isAntiAlias = true
typeface = android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD)
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val topChild = parent.getChildAt(0) ?: return
val topChildPosition = parent.getChildAdapterPosition(topChild)
if (topChildPosition == RecyclerView.NO_POSITION) return
val headerTitle = adapter.getHeaderTitle(topChildPosition)
var contactPoint = headerHeight
for (i in 1 until parent.childCount) {
val child = parent.getChildAt(i)
val position = parent.getChildAdapterPosition(child)
if (position == RecyclerView.NO_POSITION) continue
if (adapter.isHeader(position)) {
if (child.top <= contactPoint) {
contactPoint = child.top
}
break
}
}
c.save()
c.translate(0f, (contactPoint - headerHeight).toFloat())
c.drawRect(0f, 0f, parent.width.toFloat(), headerHeight.toFloat(), paint)
c.drawText(headerTitle, 20f, 70f, textPaint)
c.restore()
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val position = parent.getChildAdapterPosition(view)
if (position == RecyclerView.NO_POSITION) return
if (adapter.isHeader(position)) {
outRect.top = headerHeight
} else {
outRect.top = 0
}
}
}This solution uses a RecyclerView with a custom adapter that supports two types of list items: headers and normal items. The adapter shows headers with bold text and a gray background, and items with normal text and white background.
To create sticky headers, a custom ItemDecoration is added to the RecyclerView. This decoration draws the header at the top of the list and moves it up when the next header approaches, creating the sticky effect.
The data is a simple list mixing header and item objects. The adapter provides helper methods to identify headers and get the current header title for the sticky header drawing.
This approach keeps the UI smooth and visually clear, making it easy for users to see which group they are browsing.