pepperlove
pepperlove

Reputation: 275

Sticky Header with Horizontal RecyclerView Not Receiving Touch Events or Scroll in Android

I am implementing a RecyclerView with multiple view types, and I needed a sticky header for certain items. To achieve this, I used an ItemDecoration to render the sticky header, and it works as expected.

The sticky header itself is a horizontal RecyclerView and includes clickable items. However, I am facing an issue: the horizontal RecyclerView does not handle touch events, including clicks and scrolls.

Here’s what I’ve done so far:

Implemented a custom ItemDecoration class to handle sticky headers. Used parent.addOnItemTouchListener() to try and intercept touch events. Tried adding TouchDelegate to pass touch events to the horizontal RecyclerView. Unfortunately, none of these approaches solved the issue, and the sticky header remains unresponsive to user interactions.

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) {
//          motionEvent.y <= currentHeader?.second?.itemView?.bottom ?: 0
//        } 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 headerPosition = getHeaderPositionForItem(itemPosition)
    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()
    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 {
      c.saveLayerAlpha(
        RectF(0f, 0f, c.width.toFloat(), c.height.toFloat()),
        (((nextHeader.top - paddingTop) / nextHeader.height.toFloat()) * 255).toInt()
      )

    }
    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
    )
  }
}

What I Need:

Anyone has any idea how to enable the scroll inside a sticky header.

Upvotes: 1

Views: 112

Answers (0)

Related Questions