demand

  1. Achieve grouping head hover effect.
  2. The hover section supports click events.

The premise

  1. The data set should be header/items like this.
  2. The data set should already be grouped and sorted.
  3. Each item has a corresponding header
  4. 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

  1. 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.
  2. 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.

  3. 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
  4. 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