Created
October 21, 2020 19:07
-
-
Save florent37/56470d86a41fbee4642cd8965382e803 to your computer and use it in GitHub Desktop.
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
import android.graphics.Canvas | |
import android.view.View | |
import android.view.ViewGroup | |
import androidx.recyclerview.widget.RecyclerView | |
import androidx.recyclerview.widget.RecyclerView.ItemDecoration | |
class StickHeaderItemDecoration(private val adapter: StickyAdapter) : ItemDecoration() { | |
private var stickyHeaderHeight = 0 | |
private var currentTopChildPosition: Int? = null | |
private var currentTopChildHeaderPosition: Int? = null | |
private var currentHeaderViewHolder: RecyclerView.ViewHolder? = null | |
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 headerPos = if (currentTopChildPosition == topChildPosition) { | |
currentTopChildHeaderPosition | |
} else { | |
currentHeaderViewHolder = null | |
currentTopChildHeaderPosition = getHeaderPositionForItem(topChildPosition) | |
currentTopChildHeaderPosition | |
} | |
currentTopChildPosition = topChildPosition | |
if (headerPos != null) { | |
val currentHeader = if (currentHeaderViewHolder != null) { | |
currentHeaderViewHolder!!.itemView | |
} else { | |
findHeaderViewForItem(headerPos, parent) | |
} | |
val contactPoint = currentHeader.bottom | |
val childInContact = getChildInContact(parent, contactPoint, headerPos) | |
if (childInContact != null && adapter.isStickyHeader( | |
parent.getChildAdapterPosition( | |
childInContact | |
) | |
) | |
) { | |
moveHeader(c, currentHeader, childInContact) | |
return | |
} | |
drawHeader(c, currentHeader) | |
} | |
} | |
private fun getHeaderPositionForItem(itemPosition: Int): Int? { | |
for (i in itemPosition downTo 0) { | |
if (adapter.isStickyHeader(i)) { | |
return i | |
} | |
} | |
return null | |
} | |
private fun findHeaderViewForItem(headerPosition: Int, parent: RecyclerView): View { | |
val headerType = adapter.getItemViewType(headerPosition) | |
val viewHolder = adapter.onCreateViewHolder(parent, headerType) | |
val header = viewHolder.itemView | |
adapter.onBindViewHolder(viewHolder, headerPosition) | |
fixLayoutSize(parent, header) | |
currentHeaderViewHolder = viewHolder | |
return header | |
} | |
private fun drawHeader(c: Canvas, header: View) { | |
c.save() | |
c.translate(0f, 0f) | |
header.draw(c) | |
c.restore() | |
} | |
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, | |
currentHeaderPos: Int | |
): View? { | |
var childInContact: View? = null | |
for (i in 0 until parent.childCount) { | |
var heightTolerance = 0 | |
val child = parent.getChildAt(i) | |
//measure height tolerance with child if child is another header | |
if (currentHeaderPos != i) { | |
val isChildHeader = adapter.isStickyHeader(parent.getChildAdapterPosition(child)) | |
if (isChildHeader) { | |
heightTolerance = stickyHeaderHeight - child.height | |
} | |
} | |
//add heightTolerance if child top be in display area | |
var childBottomPosition: Int | |
childBottomPosition = if (child.top > 0) { | |
child.bottom + heightTolerance | |
} else { | |
child.bottom | |
} | |
if (childBottomPosition > contactPoint) { | |
if (child.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.also { | |
stickyHeaderHeight = it | |
}) | |
} | |
fun attach(recyclerView: RecyclerView) { | |
recyclerView.addItemDecoration(this) | |
} | |
fun detach(recyclerView: RecyclerView) { | |
recyclerView.removeItemDecoration(this) | |
} | |
interface StickyAdapter { | |
/** | |
* It's directly available in an adapter | |
* This method gets called by [StickHeaderItemDecoration] to get layout ViewHolder and View | |
*/ | |
fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder | |
/** | |
* This method gets called by [StickHeaderItemDecoration] to setup the header ViewHolder. | |
* It's directly available in an adapter | |
*/ | |
fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) | |
/** | |
* This method gets called by [StickHeaderItemDecoration] to verify whether the item represents a header. | |
* @param itemPosition int. | |
* @return true, if item at the specified adapter's position represents a header. | |
*/ | |
fun isStickyHeader(itemPosition: Int): Boolean | |
fun getItemViewType(position: Int): Int | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment