Search code examples
androidbottom-sheetandroid-viewpager2android-touch-event

How to pass touch events from part of ViewPager2 to the view underneath it?


Current State:

I have a Viewpager2 which contains a bottom sheet (using BottomSheetBehaviour). When the bottom sheet collapses, the Viewpager2 doesn't resize to fit the now shorter content. I also have a map (using MapBox's MapView) that shows underneath the ViewPager2.

Since this description is a little ambiguous, I have also created a minimal reproducible example (hosted on GitHub) for clarification. In order to get MapBox to show a map, you will have to enter your secret/public tokens in gradle.properties and strings.xml files respectively. Even if you don't, you can probably get what I'm saying by looking at the scale bar at the top left corner of the MapView.

What I want to happen:

When the bottom sheet collapses, the section of the view that the bottom sheet had previously occupied that is now transparent does not handle user interaction but instead passes the events on to the FragmentContainerView that is shown beneath it.

Problem:

When the bottom sheet collapses, the height of the ViewPager2 stays the same, so touch events within the aforementioned area are handled as per normal by the ViewPager2, when they should intuitively be handled by the MapView below, since it is now visible in that area. You can just drag your finger or pinch the area to get what I mean.

I want the part of the screen in the red box should interact with the map

What I've tried so far:

Making ViewPager2 dynamically 'wrap content'

It seems to me like ViewPager2 will not handle this out of the box from this Issue.

It's probably possible to get the expanded and collapsed height of each fragment in the ViewPager2 and then reset ViewPager2's height each time the fragment/page changes and each time the bottom sheet goes to the expanded/collapsed state but that that requires a lot of calls to requestLayout() which makes the UI flicker and disrupts animations.

Handling touch events

I tried the following, but it seems that perhaps ViewPager2 requires the touch events to be registered, because trying to swipe to the next page crashed the app. However, I'm not very familiar with handling touch events or ViewPager2's implementation and I didn't go too deep into exploring this, so input on the feasibility of this/whether it even makes sense will be appreciated.

viewPager2[position].setOnTouchListener { v, event ->
    // If background is collapsed, allow touch events to pass through transparent space in ViewPager2
    if (event.y < bottomSheetBehavior.peekHeight && isBottomSheetCollapsed) {
        false
    // Else, handle them as per normal
    } else {
        when (event.action) {
            ACTION_DOWN -> v.performClick()
        }
        true
    }
}

Note:

I also want the bottom sheet to be able to swipe to the next page like a viewpager, which is why I put the bottom sheet inside the viewpager. Something like how Citymapper does it.


Solution

  • I ended up doing the following:

    1. Collapse bottomSheet and change the height of viewPager2 to match the collapsed height by default
    2. Set an onTouchListener() on bottomSheet to detect upward drag events, which when detected would increase the height of viewPager2 and expand bottomSheet

    Setting the onTouchListener():

    binding.bottomSheet.setOnTouchListener { _, event ->
                when (event.action) {
                    ACTION_DOWN -> {
                        touchStartY = event.rawY
                        true
                    }
                    ACTION_UP -> {
                        false
                    }
                    ACTION_MOVE -> {
                        touchCurrentY = event.rawY
                        if (touchStartY - touchCurrentY > TOUCH_THRESHOLD && isBottomSheetCollapsed) {
                            // Update flag
                            isBottomSheetCollapsed = false
                            // Change viewPager2 height
                            viewPager2.updateLayoutParams {
                                height = bottomSheetExpandedHeight
                            }
                            // I don't think that updateLayoutParams calls requestLayout(), but you 
                            // would want to expand the bottomSheet only after the viewPager2 height has updated
                            viewPager2.doOnLayout {
                                bottomSheetBehavior.state = STATE_EXPANDED
                            }
                            false
                        } else true
                    }
                    else -> true
                }
            }
    

    Note: Might need to play around with doOnLayout() or doOnNextLayout() to get the expanded height of the bottom sheet if it is set to WRAP_PARENT in xml.