yuyakaido / CardStackView

📱Tinder like swipeable card view for Android
Apache License 2.0
2.37k stars 449 forks source link

Solution: Horizontal Scrolling Inside a Card #354

Open StuStirling opened 2 years ago

StuStirling commented 2 years ago

Like many others, I needed to have a RecyclerView (ViewPager2) within my cards. Here is how I worked around it.

Disable/Enable Horizontal Swiping

I needed to be able to conditionally disable and enable horizontal swiping of the cards based on events within an item. To do so, I had a listener that could be passed into the adapter upon creation.

interface MatchItemListener {
    fun disableCardSwipe()
    fun enableCardSwipe()
}

And here is the integration of this listener upon adapter creation. Notice we are disabling/enabling the horizontal scrolling in the CardStackLayoutManager assigned to our parent's layoutManager.

listener = object : MatchItemListener {
        override fun disableCardSwipe() {
            cardStackLayoutManager.setCanScrollHorizontal(false)
        }

        override fun enableCardSwipe() {
            cardStackLayoutManager.setCanScrollHorizontal(true)
        }
    }
adapter = MyAdapter(listener)

Intercepting Touches

Now we need to intercept the touches in the child items to disable/enable horizontal swiping in the parent's CardStackLayoutManager.

First, create a custom ViewGroup. In this instance, mine is a ConstraintLayout. In this we are doing some collision detection to see if the touch occurred in the view where we want to scroll horizontally. If its within this view, we call the listener to disable horizontal card swiping, otherwise we enable it again.

class MatchItemContainer @kotlin.jvm.JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

    var listener: MatchItemListener? = null

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        val x = round(ev.x).toInt()
        val y = round(ev.y).toInt()

        if (isTouchInsideView(findViewById<ViewPager2>(R.id.carousel),x, y)) {
            listener?.disableCardSwipe()
        } else listener?.enableCardSwipe()

        return super.dispatchTouchEvent(ev)
    }

    private fun isTouchInsideView(view: View, x: Int, y: Int): Boolean =
        (x > view.left && x < view.right) &&
            (y > view.top && y < view.bottom)
}

Setting Listener on Each Child

We can share the same listener on each of the children. When the adapter creates the ViewHolder or whatever mechanism you're using to inflate your layout into a view, we assign the listener to our custom ViewGroup.

Here is how I'm doing it (I'm using the FastAdapter library so its inflating a ViewBinding but the principle is the same).

override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?): ItemMatchItemBinding =
        ItemMatchItemBinding.inflate(inflater, parent, false).apply {
            container.listener = listener
        }

Hope this helps someone.