------------------------- | Fruits | | - Apple | | - Banana | | - Orange | |----------------------- | Vegetables | | - Carrot | | - Broccoli | | - Spinach | |----------------------- | Dairy | | - Milk | | - Cheese | | - Yogurt | -------------------------
Sticky headers in Android Kotlin - Mini App: Build & Ship
import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView 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) // TODO: Set adapter with grouped data and implement sticky headers } }
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.
------------------------- | Fruits | | - Apple | | - Banana | | - Orange | |----------------------- | Vegetables | | - Carrot | | - Broccoli | | - Spinach | |----------------------- | Dairy | | - Milk | | - Cheese | | - Yogurt | -------------------------
Practice
Solution
Step 1: Understand sticky header behavior
Sticky headers keep section titles visible at the top as you scroll through grouped list items.Step 2: Compare with other options
Animations, image loading, and sorting are unrelated to sticky headers.Final Answer:
To keep section titles visible while scrolling -> Option AQuick Check:
Sticky headers = visible section titles [OK]
- Confusing sticky headers with animations
- Thinking sticky headers sort the list
- Assuming sticky headers load images
Solution
Step 1: Identify components for sticky headers
RecyclerView supports ItemDecoration which allows custom drawing for sticky headers.Step 2: Eliminate other options
ListView and ScrollView do not support sticky headers natively; ViewPager is for paging, not lists.Final Answer:
RecyclerView with ItemDecoration -> Option AQuick Check:
Sticky headers use RecyclerView + ItemDecoration [OK]
- Choosing ListView instead of RecyclerView
- Using ScrollView which lacks sticky header support
- Confusing ViewPager with list components
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val child = parent.getChildAt(0) ?: return
val position = parent.getChildAdapterPosition(child)
val header = getHeaderViewForItem(position)
c.save()
c.translate(0f, 0f)
header.draw(c)
c.restore()
}
What is the visible effect of this code when scrolling?Solution
Step 1: Analyze onDrawOver behavior
onDrawOver draws the header on top of the RecyclerView content, fixed at the top (0f, 0f).Step 2: Understand scrolling effect
Since header is drawn at fixed position, it stays visible while list scrolls underneath.Final Answer:
The header stays fixed at the top while list scrolls underneath -> Option BQuick Check:
onDrawOver + translate(0,0) = sticky header visible [OK]
- Thinking header scrolls away with items
- Assuming header appears at bottom
- Believing header blinks on scroll
Solution
Step 1: Identify flicker cause in sticky headers
Repeatedly creating and measuring header views without caching causes flicker during fast scroll.Step 2: Check other options
Layout manager type or calling super.onDrawOver() usually don't cause flicker; wrap_content affects layout size but not flicker.Final Answer:
Not caching header views causing repeated layout calls -> Option DQuick Check:
Missing header caching = flicker [OK]
- Ignoring header view caching
- Blaming layout manager type
- Forgetting super call but unrelated to flicker
Solution
Step 1: Understand smooth sticky header transition
Drawing current header at top and moving it up as next header scrolls in creates smooth push effect.Step 2: Evaluate other options
Replacing header fully causes flicker; separate TextView won't sync scrolling; disabling animations doesn't create smooth transition.Final Answer:
Draw the current header at top and translate it up as next header approaches -> Option CQuick Check:
Smooth sticky header = translate current header up [OK]
- Replacing header view causing flicker
- Using fixed TextView outside RecyclerView
- Disabling animations instead of translating header
Ready to prove it?
Pass the test and this pattern moves unknown
