Skip to content

Instantly share code, notes, and snippets.

@thibseisel
Created March 13, 2018 15:40
Show Gist options
  • Save thibseisel/5382c0f460dc2ae89082654c816c2225 to your computer and use it in GitHub Desktop.
Save thibseisel/5382c0f460dc2ae89082654c816c2225 to your computer and use it in GitHub Desktop.
A RecyclerView adapter that wraps an existing adapter to split data into sections.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="viewtype_header" type="id"/>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/section_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="48dp"
android:paddingEnd="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingStart="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="?attr/colorPrimary"
android:textSize="14sp"
tools:text="3 days ago" />
import android.support.annotation.IdRes
import android.support.annotation.LayoutRes
import android.support.v7.widget.RecyclerView
import android.util.SparseArray
import android.view.ViewGroup
import android.widget.TextView
open class SectionedAdapter(
baseAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>,
@LayoutRes private val headerLayoutResId: Int,
@IdRes private val headerLabelId: Int
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val sections = SparseArray<SectionHeader>()
private var isValid: Boolean = true
private val baseAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = baseAdapter.also {
it.registerAdapterDataObserver(BaseAdapterObserver())
}
constructor(baseAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>) : this(
baseAdapter,
R.layout.item_section_header,
R.id.section_label
)
override fun getItemCount() = if (isValid) baseAdapter.itemCount + sections.size() else 0
override fun getItemViewType(position: Int): Int = if (isSectionPosition(position))
R.id.viewtype_header else baseAdapter.getItemViewType(sectionedPositionToPosition(position))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
if (viewType == R.id.viewtype_header) {
SectionViewHolder(parent, headerLayoutResId, headerLabelId)
} else {
baseAdapter.onCreateViewHolder(parent, viewType)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, sectionedPosition: Int) {
if (holder.itemViewType == R.id.viewtype_header) {
(holder as SectionViewHolder).label.text = sections[sectionedPosition].title
} else {
baseAdapter.onBindViewHolder(holder, sectionedPositionToPosition(sectionedPosition))
}
}
override fun getItemId(position: Int): Long {
return if (isSectionPosition(position)) Long.MAX_VALUE - sections.indexOfKey(position)
else baseAdapter.getItemId(sectionedPositionToPosition(position))
}
fun setSections(sectionHeaders: List<SectionHeader>) {
sections.clear()
var offset = 0
for (section in sectionHeaders.sorted()) {
section.sectionedPosition = section.position + offset
sections.append(section.sectionedPosition, section)
offset++
}
notifyDataSetChanged()
}
protected fun isSectionPosition(position: Int): Boolean = sections[position] != null
private fun positionToSectionedPosition(position: Int): Int {
var offset = 0
for (i in 0 until sections.size()) {
if (sections.valueAt(i).position > position) break
offset++
}
return position + offset
}
private fun sectionedPositionToPosition(sectionedPosition: Int): Int {
if (isSectionPosition(sectionedPosition)) {
return RecyclerView.NO_POSITION
}
var offset = 0
for (i in 0 until sections.size()) {
if (sections.valueAt(i).sectionedPosition > sectionedPosition) break
offset--
}
return sectionedPosition + offset
}
internal class SectionViewHolder(
parent: ViewGroup,
@LayoutRes headerLayoutResId: Int,
@IdRes headerLabelId: Int
) : RecyclerView.ViewHolder(parent.inflate(headerLayoutResId)) {
val label: TextView = itemView.findViewById(headerLabelId)
}
private inner class BaseAdapterObserver : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
isValid = baseAdapter.itemCount > 0
notifyDataSetChanged()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
isValid = baseAdapter.itemCount > 0
notifyItemRangeInserted(positionStart, itemCount)
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
isValid = baseAdapter.itemCount > 0
notifyItemRangeChanged(positionStart, itemCount)
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
isValid = baseAdapter.itemCount > 0
notifyItemRangeRemoved(positionStart, itemCount)
}
}
}
class SectionHeader(
val position: Int,
val title: String
) : Comparable<SectionHeader> {
internal var sectionedPosition: Int = -1
override fun compareTo(other: SectionHeader): Int {
return position - other.position
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment