androidandroid-bottomsheetdialog

Is it possible to have non modal blocking behaviour by using BottomSheetDialogFragment?


Currently, we are figuring how to implement such a bottom sheet, with the following requirements.

  1. Round corner bottom sheet.
  2. Fixed height bottom sheet.
  3. Non-draggable bottom sheet.
  4. Content in the bottom sheet is scrollable.
  5. Hide bottom sheet when we tap on non-bottom sheet item.
  6. Hide sheet when we press on back button.
  7. A non-blocking bottom sheet. When we tap on non-bottom sheet item, the tapped item will get focus and bottom sheet will hide.

We are considering, whether to use BottomSheetBehavior or BottomSheetDialogFragment.

So far, we manage to implement all the requirements, by using BottomSheetBehavior.


Implementation using BottomSheetBehavior

enter image description here

However, we do not really like the solution as

Here's the code snippet by using BottomSheetBehavior.

public class MainActivity extends AppCompatActivity {

    private BottomSheetBehavior bottomSheetBehavior;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.image_button_0).setOnClickListener(view -> demo0());

        findViewById(R.id.image_button_1).setOnClickListener(view -> demo1());

        // 7) A non-blocking bottom sheet. When we tap on non-bottom sheet item, the tapped item
        // will get focus and bottom sheet will hide.
        findViewById(R.id.edit_text_0).setOnFocusChangeListener((view, b) -> {
            if (b) {
                hideBottomSheet();
            }
        });

        // 7) A non-blocking bottom sheet. When we tap on non-bottom sheet item, the tapped item
        // will get focus and bottom sheet will hide.
        findViewById(R.id.edit_text_1).setOnFocusChangeListener((view, b) -> {
            if (b) {
                hideBottomSheet();
            }
        });
    }

    public void demo0() {
        DemoBottomDialogFragment demoBottomDialogFragment = DemoBottomDialogFragment.newInstance();

        demoBottomDialogFragment.show(getSupportFragmentManager(), "demoBottomDialogFragment");
    }

    public void demo1() {
        // 1) Round corner bottom sheet.
        View view = findViewById(R.id.bottom_sheet_layout_2);

        /*
        2) Fixed height bottom sheet.

        3) Non-draggable bottom sheet.

        4) Content in the bottom sheet is scrollable.
         */
        this.bottomSheetBehavior = BottomSheetBehavior.from(view);

        bottomSheetBehavior.setPeekHeight(900, true);

        bottomSheetBehavior.setDraggable(false);
    }

    private boolean hideBottomSheet() {
        if (this.bottomSheetBehavior != null) {
            this.bottomSheetBehavior.setPeekHeight(0, true);
            this.bottomSheetBehavior = null;
            return true;
        }
        return false;
    }

    @Override
    public void onBackPressed() {
        // 5) Hide bottom sheet when we tap on non-bottom sheet item.
        if (hideBottomSheet()) {
            return;
        }

        super.onBackPressed();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 6) Hide sheet when we press on back button.
        hideBottomSheet();

        return super.onTouchEvent(event);
    }
}

If we were using BottomSheetDialogFragment, the code will be way more simpler. We can achieve all requirements, except number 7

  1. A non-blocking bottom sheet. When we tap on non-bottom sheet item, the tapped item will get focus and bottom sheet will hide.

Here's the outcome of BottomSheetDialogFragment.

Implementation using BottomSheetDialogFragment

enter image description here

The good thing of using BottomSheetDialogFragment is that,

Here's the code snippet.

public class DemoBottomDialogFragment extends BottomSheetDialogFragment {

    public static DemoBottomDialogFragment newInstance() {
        return new DemoBottomDialogFragment();
    }

    @NonNull
    @Override public Dialog onCreateDialog(Bundle savedInstanceState) {
        Dialog dialog = super.onCreateDialog(savedInstanceState);

        // https://stackoverflow.com/questions/58651661/how-to-set-max-height-in-bottomsheetdialogfragment
        dialog.setOnShowListener(new DialogInterface.OnShowListener() {
            @Override public void onShow(DialogInterface dialogInterface) {
                BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialogInterface;

                FrameLayout bottomSheet = bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet);

                ViewGroup.LayoutParams layoutParams = bottomSheet.getLayoutParams();

                // !!!
                layoutParams.height = 900;

                bottomSheet.setLayoutParams(layoutParams);
            }
        });

        return  dialog;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Make the bottom sheet non drag-able.
        setStyle(DialogFragment.STYLE_NORMAL, R.style.BottomSheetDialogStyle);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {

        View view = inflater.inflate(R.layout.bottom_sheet_layout, container,
                false);

        // get the views and attach the listener

        return view;

    }
}

I was wondering, if we were using BottomSheetDialogFragment, is there a way to achieve

  1. A non-blocking bottom sheet. When we tap on non-bottom sheet item, the tapped item will get focus and bottom sheet will hide.

As you can see, when I tap on EditText region, the bottom sheet is hidden. But, the EditText is not getting focus.

Here's the complete workable demo for testing purpose - https://github.com/yccheok/wediary-sandbox/tree/master/bottom-sheet

Thank you.


Solution

  • There are two window flags that allow passing touch events to the background windows:

    But those flags can work only for touches outside the dialog window; so setting them alone won't work if the dialog window expands to obscure the EditText's.

    So, we need to limit the dialog window to the bottom sheet desired layout height which it's hard coded as 900px. Doing this can prevent the window from obscuring the EditText's; and hence the flags do their job.

    Now, we'll hard code the window height to that value; and set the bottom sheet to the expanded state to expand to the entire window:

    So, instead of layoutParams.height = 900; We'd use:

    WindowManager.LayoutParams params = window.getAttributes();
    params.height = 900;
    params.gravity = Gravity.BOTTOM; // bias the dialog to the bottom
    getDialog().getWindow().setAttributes(params);
    

    This will achieve the desired behavior but now the rounded corners are gone as the expanded state is designed to expand to the entire available space. To solve this we'd set the rounded corner in the BottomSheet style instead of the layout.

    Here is the modified version:

    <resources>
    
        <style name="BottomSheetDialogStyle" parent="Theme.Material3.Light.BottomSheetDialog">
            <item name="behavior_draggable">false</item>
            <item name="bottomSheetStyle">@style/BottomSheetStyle</item>
    
        </style>
    
        <style name="BottomSheetStyle">
            <item name="android:background">@drawable/bottom_sheet_background</item>
        </style>
    
    </resources>
    

    Now we can safely remove android:background="@drawable/bottom_sheet_background" from the layout.

    BottomSheetDialogFragment:

    
    public class DemoBottomDialogFragment extends BottomSheetDialogFragment {
    
        public static DemoBottomDialogFragment newInstance() {
            return new DemoBottomDialogFragment();
        }
    
        @NonNull
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            Dialog dialog = super.onCreateDialog(savedInstanceState);
            // https://stackoverflow.com/questions/58651661/how-to-set-max-height-in-bottomsheetdialogfragment
            dialog.setOnShowListener(new DialogInterface.OnShowListener() {
                @Override
                public void onShow(DialogInterface dialogInterface) {
                    BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialogInterface;
                    FrameLayout bottomSheet = bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet);
                    BottomSheetBehavior<FrameLayout> behavior = BottomSheetBehavior.from(bottomSheet);
                    behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
                }
            });
    
            return dialog;
        }
    
        @Override
        public void onStart() {
            super.onStart();
    
            Window window = getDialog().getWindow();
            window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
                    WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
            window.setFlags(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
                    WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
    
            WindowManager.LayoutParams params = window.getAttributes();
            params.height = 900;
            params.gravity = Gravity.BOTTOM;
            window.setAttributes(params);
        }
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            // Make the bottom sheet non drag-able.
            setStyle(DialogFragment.STYLE_NORMAL, R.style.BottomSheetDialogStyle);
        }
    
        @Nullable
        @Override
        @SuppressLint("RestrictedApi")
        public View onCreateView(LayoutInflater inflater,
                                 @Nullable ViewGroup container,
                                 @Nullable Bundle savedInstanceState) {
    
            View view = inflater.inflate(R.layout.bottom_sheet_layout, container,
                    false);
            return view;
    
        }
    }