Created
February 16, 2019 13:24
-
-
Save antonKozyriatskyi/6f6ae6bdb6e2bc98e9e82a48dc63ab07 to your computer and use it in GitHub Desktop.
Sticky headers for RecyclerView. Supports only one sticky header view type.
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
package kozyriatskyi.anton.sked.week.stickyheaders | |
import android.graphics.Canvas | |
import android.support.v4.view.ViewCompat | |
import android.support.v7.widget.RecyclerView | |
import android.util.SparseIntArray | |
import android.view.MotionEvent | |
import android.view.View | |
import android.view.ViewGroup | |
abstract class StickyHeaderAdapter<T : RecyclerView.ViewHolder> : RecyclerView.Adapter<T>() { | |
abstract fun getHeaderPosition(itemPosition: Int): Int | |
abstract fun getHeaderViewHolder(parent: ViewGroup): RecyclerView.ViewHolder | |
abstract fun bindHeaderViewHolder(holder: RecyclerView.ViewHolder, headerPosition: Int) | |
abstract fun isHeader(itemPosition: Int): Boolean | |
} | |
class StickyHeaderItemDecoration(recyclerView: RecyclerView) : RecyclerView.ItemDecoration() { | |
private lateinit var headerHolder: RecyclerView.ViewHolder | |
private var currentStickyHeaderPosition: Int = RecyclerView.NO_POSITION | |
private val headerPositionsByItemPositions = SparseIntArray(7) | |
private val adapter: StickyHeaderAdapter<RecyclerView.ViewHolder> = kotlin.run { | |
recyclerView.adapter as? StickyHeaderAdapter<RecyclerView.ViewHolder> | |
?: throw IllegalArgumentException("adapter must not be null and must be a descendant of StickyHeaderAdapter") | |
} | |
init { | |
recyclerView.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() { | |
override fun onInterceptTouchEvent(recyclerView: RecyclerView, motionEvent: MotionEvent): Boolean { | |
return motionEvent.y <= headerHolder.itemView.height | |
} | |
}) | |
val dataObserver = object : RecyclerView.AdapterDataObserver() { | |
override fun onChanged() { | |
currentStickyHeaderPosition = RecyclerView.NO_POSITION | |
preprocessHeaders(recyclerView) | |
} | |
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { | |
onChanged() | |
} | |
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { | |
onChanged() | |
} | |
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { | |
onChanged() | |
} | |
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { | |
onChanged() | |
} | |
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { | |
onChanged() | |
} | |
} | |
adapter.registerAdapterDataObserver(dataObserver) | |
} | |
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { | |
super.onDrawOver(c, parent, state) | |
val topChild = parent.getChildAt(0) ?: return | |
val topChildPosition = parent.getChildAdapterPosition(topChild) | |
if (topChildPosition == RecyclerView.NO_POSITION) return | |
val currentHeader = getHeaderViewForItemPosition(topChildPosition, parent) ?: return | |
val contactPoint = currentHeader.bottom | |
val childInContact = getChildInContact(parent, contactPoint) | |
if (childInContact != null && adapter.isHeader(parent.getChildAdapterPosition(childInContact))) { | |
moveHeader(c, currentHeader, childInContact) | |
return | |
} | |
drawHeader(c, currentHeader) | |
} | |
private fun getHeaderViewForItemPosition(itemPosition: Int, parent: RecyclerView): View? { | |
val headerPosition = headerPositionsByItemPositions[itemPosition] | |
if (headerPosition != currentStickyHeaderPosition) { | |
currentStickyHeaderPosition = headerPosition | |
adapter.bindHeaderViewHolder(headerHolder, headerPosition) | |
measureAndLayoutHeader(parent, headerHolder.itemView) | |
} | |
return headerHolder.itemView | |
} | |
private fun preprocessHeaders(parent: RecyclerView) { | |
headerHolder = adapter.getHeaderViewHolder(parent) | |
headerPositionsByItemPositions.clear() | |
var itemPosition = adapter.itemCount - 1 | |
while (itemPosition > 0) { | |
val headerPosition = adapter.getHeaderPosition(itemPosition) | |
for (i in itemPosition downTo headerPosition - 1) { | |
headerPositionsByItemPositions.put(i, headerPosition) | |
} | |
itemPosition = headerPosition - 1 | |
} | |
} | |
private fun drawHeader(c: Canvas, header: View) { | |
header.draw(c) | |
} | |
private fun moveHeader(c: Canvas, currentHeader: View, nextHeader: View) { | |
c.save() | |
c.translate(0f, (nextHeader.top - currentHeader.height).toFloat()) | |
currentHeader.draw(c) | |
c.restore() | |
} | |
private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? { | |
for (i in 0 until parent.childCount) { | |
val child = parent.getChildAt(i) | |
if (child.bottom > contactPoint && child.top <= contactPoint) { | |
// This child overlaps the contactPoint | |
return child | |
} | |
} | |
return null | |
} | |
private fun measureAndLayoutHeader(parent: RecyclerView, view: View) { | |
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY) | |
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED) | |
val paddingEnd = ViewCompat.getPaddingEnd(parent) | |
val paddingStart = ViewCompat.getPaddingStart(parent) | |
val horizontalPadding = paddingStart + paddingEnd | |
val verticalPadding = parent.paddingTop + parent.paddingBottom | |
val childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, horizontalPadding, view.layoutParams.width) | |
val childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, verticalPadding, view.layoutParams.height) | |
view.measure(childWidthSpec, childHeightSpec) | |
view.layout(0, 0, view.measuredWidth, view.measuredHeight) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment