demand
- Achieve grouping head hover effect.
- The hover section supports click events.
The premise
- The data set should be header/items like this.
- The data set should already be grouped and sorted.
- Each item has a corresponding header
- The first item should be a header
Code implementation
class HeaderItemDecoration(
parent: RecyclerView,
private val shouldFadeOutHeader: Boolean = false,
private val isHeader: (itemPosition: Int) -> Boolean
) : RecyclerView.ItemDecoration() {
private var currentHeader: Pair<Int, RecyclerView.ViewHolder>? = null
init {
parent.adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
// clear saved header as it can be outdated now
currentHeader = null
}
})
parent.doOnEachNextLayout {
// clear saved layout as it may need layout update
currentHeader = null
}
// handle click on sticky header
parent.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
override fun onInterceptTouchEvent(
recyclerView: RecyclerView,
motionEvent: MotionEvent
): Boolean {
return if (motionEvent.action == MotionEvent.ACTION_DOWN) {
val b = motionEvent.y <= currentHeader?.second?.itemView?.bottom ?: 0
val second = currentHeader?.second
if (b && second is ChannelHotHeadViewHolder && !second.switchSort.isChecked && (motionEvent.x >= second.switchSort.left && motionEvent.x <= second.switchSort.right)) {
//点击事件传递
EventBus.getDefault().post(ClassifyItemHeadClickEvent())
}
b
} else false
}
})
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
//val topChild = parent.getChildAt(0) ?: return
val topChild = parent.findChildViewUnder(
parent.paddingLeft.toFloat(),
parent.paddingTop.toFloat() /*+ (currentHeader?.second?.itemView?.height ?: 0 )*/
) ?: return
val topChildPosition = parent.getChildAdapterPosition(topChild)
if (topChildPosition == RecyclerView.NO_POSITION) {
return
}
val headerView = getHeaderViewForItem(topChildPosition, 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(itemPosition: Int, parent: RecyclerView): View? {
if (parent.adapter == null) {
return null
}
val adapter = parent.adapter
val headerPosition = getHeaderPositionForItem(itemPosition)
if (headerPosition == RecyclerView.NO_POSITION) return null
val headerType = adapter?.getItemViewType(headerPosition) ?: return null
// if match reuse viewHolder
if (currentHeader?.first == headerPosition && currentHeader?.second?.itemViewType == headerType) {
if (currentHeader?.second is ChannelHotHeadViewHolder && adapter is ChannelListAdapter) {
(currentHeader?.second as ChannelHotHeadViewHolder).switchSort.isChecked = adapter.isDefaultSort
}
return currentHeader?.second?.itemView
}
val headerHolder = adapter.createViewHolder(parent, headerType)
if (headerHolder is ChannelClassifyHeadViewHolder) {
headerHolder.vLine.visibility = View.GONE
}
if (adapter is ChannelListAdapter) {
if (headerType != ChannelListAdapter.HOT_HEAD_TYPE) {
parent.adapter?.onBindViewHolder(headerHolder, headerPosition)
} else if (headerHolder is ChannelHotHeadViewHolder) {
headerHolder.switchSort.isChecked = adapter.isDefaultSort
}
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()
c.translate(0f, paddingTop.toFloat())
header.draw(c)
c.restore()
}
private fun moveHeader(c: Canvas, currentHeader: View, nextHeader: View, paddingTop: Int) {
c.save()
if (!shouldFadeOutHeader) {
c.clipRect(0, paddingTop, c.width, paddingTop + currentHeader.height)
} else {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
c.saveLayerAlpha(
RectF(0f, 0f, c.width.toFloat(), c.height.toFloat()),
(((nextHeader.top - paddingTop) / nextHeader.height.toFloat()) * 255).toInt()
)
} else {
c.saveLayerAlpha(
0f, 0f, c.width.toFloat(), c.height.toFloat(),
(((nextHeader.top - paddingTop) / nextHeader.height.toFloat()) * 255).toInt(),
Canvas.ALL_SAVE_FLAG
)
}
}
c.translate(0f, (nextHeader.top - currentHeader.height).toFloat() /*+ paddingTop*/)
currentHeader.draw(c)
if (shouldFadeOutHeader) {
c.restore()
}
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(itemPosition: Int): Int {
var headerPosition = RecyclerView.NO_POSITION
var currentPosition = itemPosition
do {
if (isHeader(currentPosition)) {
headerPosition = currentPosition
break
}
currentPosition -= 1
} while (currentPosition >= 0)
return headerPosition
}
}
inline fun View.doOnEachNextLayout(crossinline action: (view: View) -> Unit) {
addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ ->
action(
view
)
}
}
Copy the code
Implementation logic
- ItemDecoration, by copying it in 3 ways:
- GetItemOffsets split the space up, down, left, and right of the ItemView.
- OnDraw draws the graph under the contents of ItemView.
- OnDrawOver draws a graph over the contents of the ItemView.
- Overwrite onDrawOver we can hover by drawing an ItemView.
-
Get the first View visible on the screen inside the onDrawOver method.
View topChild = parent.getChildAt(0); Copy the code
-
Determine the ViewHeader corresponding to the View.
int topChildPosition = parent.getChildAdapterPosition(topChild); View currentHeader = getHeaderViewForItem(topChildPosition, parent); Copy the code
-
Define the drawHeader() method to draw a hovering HeaderView in RecyclerView.
-
- Implement an animation: When a new HeaderView approaches the head, it should be able to push off the top HeaderView and eventually occupy the head position.
- Determines whether the header’s HeaderView is encountering an incoming new HeaderView.
View childInContact = getChildInContact(parent, contactPoint); Copy the code
- Get the Contact Point: the bottom of our drawn HeaderView and the soon-to-be head of the HeaderView.
int contactPoint = currentHeader.getBottom(); Copy the code
- If the Item in the list is approaching the Contact point, redraw the HeaderView so that its bottom overlaps the top of the incoming Item. Implement the Translate () method: The head of the HeaderView will slowly disappear, “as if it were slowly pushed out of the screen until it becomes invisible.” When it is completely invisible, draw a new HeaderView.
if (childInContact ! = null) { if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) { moveHeader(c, currentHeader, childInContact); } else { drawHeader(c, currentHeader); }Copy the code
- Determines whether the header’s HeaderView is encountering an incoming new HeaderView.
- Click on the event
- The HeaderView has a SwitchButton click event inside. The HeaderView is painted on Canvas, and the OntouchEvent method is not implemented internally, so it cannot respond to internal events. The click event can be implemented by intercepting the event and then sending a notification, as shown in the code comment above, which is easier to understand.
The resources
- How can I make sticky headers in RecyclerView? (Without external lib)
- Small dessert, RecyclerView ItemDecoration and advanced characteristics of practice