Android: Nested bottom sheet click/drag touch event issue

Tim Malseed picture Tim Malseed · May 2, 2017 · Viewed 12.3k times · Source

I have a bottom sheet nested inside another bottom sheet (FrameLayouts using the BottomSheet layout behavior)

I also have a couple of 'peek views' (FrameLayouts) which have click listeners attached, to expand the bottom sheet(s) respectively, when clicked.

So the app basically has 3 main screens. The 'main container', then the first 'bottom sheet', which can be expanded full-screen, and then at the bottom of the first bottom sheet, is the second bottom sheet, which can also be expanded full-screen.

Problem:

When I add a RecyclerView to the nested bottom sheet 'container' view, dragging stops working for the second peek view (Sheet 2 Peek). If I remove the peek view ClickListener or the RecyclerView, things seem to work perfectly fine.

Desired result:

Both bottom sheets should remain draggable, and the peek views should be able to be clicked to expand their parent bottom sheet. The bottom sheet should respond to nested scrolls as it would normally.

I've tried removing the ClickListener and using touch gestures instead, but nothing I've tried seems to help.

I'm using v25.3.1 of the design support library, and I'm able to reproduce this problem on a Galaxy S4 running 4.4.4 stock, and a Nexus 6P running 7.1.2 stock. (I don't have any other devices available).

I've also created a test project on github for anyone interested in taking a closer look: https://github.com/timusus/bottomsheet-test

Here's a couple of screenshots demonstrating the layout:

1 2 3

The layout structure looks like this (some code omitted for clarity):

<CoordinatorLayout>

    <FrameLayout
        android:id="@+id/mainContainer" 
        android:layout_height="match_parent"/>

    <FrameLayout
        android:id="@+id/sheet1" 
        android:layout_height="match_parent"
        app:layout_behavior="CustomBottomSheetBehavior"
        app:behavior_peekHeight="64dp">

        <FrameLayout
            android:id="@+id/sheet1Container"
            android:layout_height="match_parent"/>

        <CoordinatorLayout>

        <FrameLayout
            android:id="@+id/sheet2
            android:layout_height="match_parent"
            app:layout_behavior="CustomBottomSheetBehavior"
            app:behavior_peekHeight="64dp">

            <FrameLayout
                android:id="@+id/sheet2Container"
                android:layout_height="match_parent">

                <!-- Problematic RecyclerView -->
                <RecyclerView 
                android:layout_height="match_parent"/>

            </FrameLayout>

            <!-- Problematic Click Listener on this view -->
            <FrameLayout 
                android:id="@+id/sheet2PeekView"
                android:layout_height=64dp"/>

        </FrameLayout>

        </CoordinatorLayout>

        <FrameLayout
            android:id="@+id/sheet1PeekView"
            android:layout_height=64dp"/>

    </FrameLayout>
</CoordinatorLayout/>

The CustomBottomSheetBehavior is just a simple subclass of BottomSheetBehavior which prevents the first sheet from intercepting touch events if the second sheet is expanded or dragging. This allows the second sheet to be dragged from 'expanded' to 'collapsed' without also collapsing the first sheet.

public class CustomBottomSheetBehavior<V extends View> extends BottomSheetBehavior<V> {

    private boolean allowDragging = true;

    public void setAllowDragging(boolean allowDragging) {
        this.allowDragging = allowDragging;
    }

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
        if (!allowDragging) {
            return false;
        }

        return super.onInterceptTouchEvent(parent, child, event);
    }
}

I don't believe the customisation of BottomSheetBehavior is relevant to this problem, but for completeness, here's how it's used:

FrameLayout sheet1 = (FrameLayout) findViewById(R.id.sheet1);
bottomSheetBehavior1 = (CustomBottomSheetBehavior) BottomSheetBehavior.from(sheet1);

FrameLayout sheet2 = (FrameLayout) findViewById(R.id.sheet2);
       bottomSheetBehavior2 = (CustomBottomSheetBehavior) BottomSheetBehavior.from(sheet2);
       bottomSheetBehavior2.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
           @Override
           public void onStateChanged(@NonNull View bottomSheet, int newState) {
                //If the second sheet is expanded or dragging, don't allow the first sheet to respond to touch events.
               if (newState == BottomSheetBehavior.STATE_EXPANDED || newState == BottomSheetBehavior.STATE_DRAGGING) {
                   bottomSheetBehavior1.setAllowDragging(false);
               } else {
                   bottomSheetBehavior1.setAllowDragging(true);
               }
           }

I can't seem to figure out if this is to do with the onInterceptTouchEvent of the BottomSheet, nested scroll handling of the inner RecyclerView, View.ClickListener stealing touch events, a combination of the above, or something else altogether.

Any help would be much appreciated.

Answer

N J picture N J · May 9, 2017

FIXED

I can't seem to figure out if this is to do with the onInterceptTouchEvent of the BottomSheet, nested scroll handling of the inner RecyclerView, View.ClickListener stealing touch events, a combination of the above, or something else altogether.

It is a combination of the above CustomBottomSheetBehavior and View.ClickListener

Issue was bottomSheetBehavior1 is taking drag event when getSheet2PeekView is dragging so detect touch event on getSheet2PeekView and set bottomSheetBehavior1 dragging false and bottomSheetBehavior2 true


Solution

Put this code and your problem is resolved.

findViewById(getSheet2PeekViewResId()).setOnTouchListener(new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            Log.e(TAG, "onTouch: ");
            bottomSheetBehavior1.setAllowDragging(false);
            bottomSheetBehavior2.setAllowDragging(true);
            return false;
        }
    });

Also created Pull Request to your repo with fully working changes.