How I ended up solving this for anyone interested.
when binding the data I pass in the previous item as well
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val previousItem = if (position == 0) null else getItem(position - 1)
holder.bind(item, previousItem)
}
Every view then sets a header, which is only made visible if the previous item doesn't have the same header.
val previousHeader = previousItem?.name?.capitalize().first()
val header = item?.name?.capitalize()?.first()
view.cachedContactHeader.text = header
view.cachedContactHeader.isVisible = previousHeader != header
Update 24/01/20
Since answering this I have changed to using a custom ItemDecoration
class StickyHeaderDividerItem(
context: Context,
private val stickyHeaderCallbacks: StickyHeaderCallbacks
) : ItemDecoration() {
private val headerHeight = context.resources.getDimensionPixelOffset(R.dimen.sticky_header_height)
private var headerView: View? = null
private var headerTextView: TextView? = null
private var headerEndTextView: TextView? = null
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
if (position >= 0 && stickyHeaderCallbacks.isHeader(position)) {
outRect.top = headerHeight
}
}
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: State) {
super.onDrawOver(canvas, parent, state)
if (headerView == null) {
headerView = (parent.inflate(R.layout.list_item_sticky_header)).apply {
fixLayoutSize(this, parent)
}
headerTextView = headerView?.headerText
headerEndTextView = headerView?.headerEndText
}
var previousHeader = ""
parent.children.toList().forEach { child ->
val position = parent.getChildAdapterPosition(child)
val headerTitle = position.takeIf { it >= 0 }?.let(stickyHeaderCallbacks::headerTitle).orEmpty()
val headerEndTitle = position.takeIf { it >= 0 }?.let(stickyHeaderCallbacks::headerTitleEnd)
headerTextView?.text = headerTitle
headerEndTextView?.text = headerEndTitle
if ((previousHeader != headerTitle) || (position >= 0 && stickyHeaderCallbacks.isHeader(position))) {
headerView?.let { drawHeader(canvas, child, it) }
previousHeader = headerTitle
}
}
}
private fun drawHeader(canvas: Canvas, child: View, headerView: View) {
canvas.run {
save()
translate(0F, maxOf(0, child.top - headerView.height).toFloat())
headerView.draw(this)
restore()
}
}
private fun fixLayoutSize(view: View, parent: ViewGroup) {
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
val childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingStart + parent.paddingEnd, view.layoutParams.width)
val childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, view.layoutParams.height)
view.measure(childWidth, childHeight)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
}
interface StickyHeaderCallbacks {
fun isHeader(itemPosition: Int): Boolean
fun headerTitle(itemPosition: Int): String
fun headerTitleEnd(itemPosition: Int): String? = null
}