I had to make a change to 'getHeaderPositionForItem' and changed it to:
private fun getHeaderPositionForItem(recyclerView: RecyclerView): Int {
var headerPosition = RecyclerView.NO_POSITION
var currentPosition: Int = when (recyclerView.layoutManager) {
is LinearLayoutManager -> {
(recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
}
is GridLayoutManager -> {
(recyclerView.layoutManager as GridLayoutManager).findFirstVisibleItemPosition()
}
else -> {
0
}
}
do {
if (isHeader(currentPosition)) {
headerPosition = currentPosition
break
}
currentPosition -= 1
} while (0 <= currentPosition && currentPosition <= recyclerView.adapter!!.itemCount)
return headerPosition
}
It's not 100% polished as my designer changed his mind about having sticky headers but there are some possible changes there that might help you to support more possible implementations of your work. I'll attach the whole file for better clarity of the chances. Great work tho! I really liked this :)
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
/*
Sticky header implementation based of off the following git repo
https://github.com/filipkowicz/HeaderItemDecorationExample
Credit to Michal Filipek - filipkowicz
*/
class StickyHeaderItemDecoration(
recyclerView: RecyclerView,
private val isHeader: (itemPosition: Int) -> Boolean,
private val listener: OnEventListener
) : RecyclerView.ItemDecoration() {
interface OnEventListener {
fun onStickyHeaderClicked(currentHeader: RecyclerView.ViewHolder?, motionEvent: MotionEvent)
}
private var currentHeader: Pair<Int, RecyclerView.ViewHolder>? = null
init {
recyclerView.adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
// clear saved header as it can be outdated now
currentHeader = null
}
})
recyclerView.doOnEachNextLayout {
// clear saved layout as it may need layout update
currentHeader = null
}
// handle click on sticky header
recyclerView.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
override fun onInterceptTouchEvent(
recyclerView: RecyclerView,
motionEvent: MotionEvent
): Boolean {
return if (motionEvent.action == MotionEvent.ACTION_DOWN) {
if (motionEvent.y <= currentHeader?.second?.itemView?.bottom ?: 0) {
//header touched
listener.onStickyHeaderClicked(currentHeader?.second, motionEvent)
return true
} else false
} else false
}
})
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
val headerView = getHeaderViewForItem(parent) ?: return
val contactPoint = headerView.bottom + parent.paddingTop
val childInContact = getChildInContact(parent, contactPoint) ?: return
if (isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, headerView, childInContact, parent.paddingTop)
return
}
drawHeader(c, headerView, parent.paddingTop)
}
private fun getHeaderViewForItem(parent: RecyclerView): View? {
if (parent.adapter == null) {
return null
}
val headerPosition = getHeaderPositionForItem(parent)
if (headerPosition == RecyclerView.NO_POSITION) return null
val headerType = parent.adapter?.getItemViewType(headerPosition) ?: return null
// if match reuse viewHolder
if (currentHeader?.first == headerPosition && currentHeader?.second?.itemViewType == headerType) {
return currentHeader?.second?.itemView
}
val headerHolder = parent.adapter?.createViewHolder(parent, headerType)
if (headerHolder != null) {
parent.adapter?.onBindViewHolder(headerHolder, headerPosition)
fixLayoutSize(parent, headerHolder.itemView)
// save for next draw
currentHeader = headerPosition to headerHolder
}
return headerHolder?.itemView
}
private fun drawHeader(c: Canvas, header: View, paddingTop: Int) {
c.save()
//Enable to debug view
// c.saveLayerAlpha(RectF(0f, 0f, c.width.toFloat(), c.height.toFloat()), 150)
c.translate(0f, paddingTop.toFloat())
header.draw(c)
c.restore()
}
private fun moveHeader(c: Canvas, currentHeader: View, nextHeader: View, paddingTop: Int) {
c.save()
c.clipRect(0, paddingTop, c.width, paddingTop + currentHeader.height)
//Enable to debug view
// c.saveLayerAlpha(RectF(0f, 0f, c.width.toFloat(), c.height.toFloat()), 150)
c.translate(0f, (nextHeader.top - currentHeader.height).toFloat() /*+ paddingTop*/)
currentHeader.draw(c)
c.restore()
}
private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? {
var childInContact: View? = null
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
val mBounds = Rect()
parent.getDecoratedBoundsWithMargins(child, mBounds)
if (mBounds.bottom > contactPoint) {
if (mBounds.top <= contactPoint) {
// This child overlaps the contactPoint
childInContact = child
break
}
}
}
return childInContact
}
/**
* Properly measures and layouts the top sticky header.
*
* @param parent ViewGroup: RecyclerView in this case.
*/
private fun fixLayoutSize(parent: ViewGroup, view: View) {
// Specs for parent (RecyclerView)
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec =
View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
// Specs for children (headers)
val childWidthSpec = ViewGroup.getChildMeasureSpec(
widthSpec,
parent.paddingLeft + parent.paddingRight,
view.layoutParams.width
)
val childHeightSpec = ViewGroup.getChildMeasureSpec(
heightSpec,
parent.paddingTop + parent.paddingBottom,
view.layoutParams.height
)
view.measure(childWidthSpec, childHeightSpec)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
private fun getHeaderPositionForItem(recyclerView: RecyclerView): Int {
var headerPosition = RecyclerView.NO_POSITION
var currentPosition: Int = when (recyclerView.layoutManager) {
is LinearLayoutManager -> {
(recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
}
is GridLayoutManager -> {
(recyclerView.layoutManager as GridLayoutManager).findFirstVisibleItemPosition()
}
else -> {
0
}
}
do {
if (isHeader(currentPosition)) {
headerPosition = currentPosition
break
}
currentPosition -= 1
} while (0 <= currentPosition && currentPosition <= recyclerView.adapter!!.itemCount)
return headerPosition
}
}
private inline fun View.doOnEachNextLayout(crossinline action: (view: View) -> Unit) {
addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ ->
action(
view
)
}
}
Fragment:
class BookmarkListFragment : CoreBaseFragment(R.layout.fragment_bookmark_list),
BookmarkListAdapter.Listener,
GSSwipeToDeleteTouchHelper.OnEventListener,
StickyHeaderItemDecoration.OnEventListener
{
private fun initUI() {
recyclerView.addItemDecoration(StickyHeaderItemDecoration(
recyclerView,
isHeader = this.adapter!!::isStickyHeader,
listener = this
))
}
override fun onStickyHeaderClicked(currentHeader: RecyclerView.ViewHolder?, motionEvent: MotionEvent) {
if (currentHeader == null) return
//showMoreView is a textView within the Header Layout
if (currentHeader is recyclerAdapter.HeaderViewHolder && motionEvent.x > currentHeader.itemView.showMoreView.x) {
currentHeader.itemView.showMoreView.performClick()
}
}
}
Adapter
fun isStickyHeader(position: Int): Boolean {
return getItemViewType(position) == HEADER
}
companion object {
private const val HEADER = 0
private const val CARDVIEW = 1
}
Hi Michal, really like your stuff here!
I tried adapting your code to my situation. I have a recycler view with expanding sections.
When expanding the sections the Item Decoration would take the title of the bottom header so I had to modify your code a little to get this to work.
To start with I had to change your DrawOver Method to use childAt(0) but then found I didn't need it what so ever and ended up removing:
I had to make a change to 'getHeaderPositionForItem' and changed it to:
It's not 100% polished as my designer changed his mind about having sticky headers but there are some possible changes there that might help you to support more possible implementations of your work. I'll attach the whole file for better clarity of the chances. Great work tho! I really liked this :)
Fragment:
Adapter