66

I have implemented a BottomSheet Dialog and I want to prevent the bottomsheet from dismissing when the user touches outside of the bottomsheet when it's peeking (Not fully expanded state).

I have set dialog.setCanceledOnTouchOutside(false); in the code but it doesn't seem to take any affect.

Here's my BottomSheetDialogFragment class:

public class ShoppingCartBottomSheetFragment extends BottomSheetDialogFragment  {

    private BottomSheetBehavior.BottomSheetCallback mBottomSheetBehaviorCallback = new BottomSheetBehavior.BottomSheetCallback() {

        @Override
        public void onStateChanged(@NonNull View bottomSheet, int newState) {
            if (newState == BottomSheetBehavior.STATE_HIDDEN) {
                dismiss();
            }

        }

        @Override
        public void onSlide(@NonNull View bottomSheet, float slideOffset) {
        }
    };

    @Override
    public void setupDialog(Dialog dialog, int style) {
        super.setupDialog(dialog, style);

        View contentView = View.inflate(getContext(), R.layout.fragment_shopping_cart_bottom_sheet, null);

        dialog.setCanceledOnTouchOutside(false);

        dialog.setContentView(contentView);

        CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) ((View) contentView.getParent()).getLayoutParams();
        CoordinatorLayout.Behavior behavior = params.getBehavior();

        if( behavior != null && behavior instanceof BottomSheetBehavior ) {
            ((BottomSheetBehavior) behavior).setBottomSheetCallback(mBottomSheetBehaviorCallback);
            ((BottomSheetBehavior) behavior).setPeekHeight(97);
            ((BottomSheetBehavior) behavior).setHideable(false);
        }
    }


    @Override
    public void onStart() {
        super.onStart();
        Window window = getDialog().getWindow();
        WindowManager.LayoutParams windowParams = window.getAttributes();
        windowParams.dimAmount = 0;
        windowParams.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
        window.setAttributes(windowParams);
    }
}

According to the BottomSheet specification bottom sheets can be dismissed by touching outside of the bottom sheet, therefore what are my options to override this behavior and prevent it being dismissed?

RamithDR
  • 2,103
  • 2
  • 25
  • 34

13 Answers13

61

You should use #setCancelable(false) when you create an instance of it.

    BottomSheetDialogFragment bottomSheetDialogFragment = new SequenceControlFragmentBottomSheet();
    bottomSheetDialogFragment.setCancelable(false);
    bottomSheetDialogFragment.show(getChildFragmentManager(), bottomSheetDialogFragment.getTag());
M. Erfan Mowlaei
  • 1,376
  • 1
  • 14
  • 25
  • Thanks! But if you disable cancellation, you won't be able to call touch outside listeners (see https://stackoverflow.com/questions/40616833/bottomsheetdialogfragment-listen-to-dismissed-by-user-event). For instance, if you later want to cancel the dialog in specific cases and will try to override `onCancel()` and `onStateChanged()` events, they won't call. – CoolMind Apr 17 '19 at 08:31
  • 2
    this is not working ,it is getting dismissed while scrolling down or touching outside the bottomsheet – Ajay Chauhan Jan 29 '20 at 13:27
29

all of the above answers are a little complex in case of simple bottom sheet dialog Just Use cancellable As it prevents the scrolling and clicking outside of the Dialog.

mBottomSheetDialog.setCancelable(false)
mBottomSheetDialog.setCanceledOnTouchOutside(false)

just use it for simple Bottom Sheet Dialog. it worked for Me.

Niraj
  • 903
  • 8
  • 23
Rabia Tabasam
  • 299
  • 3
  • 5
26

setCancelable(false) will prevent the bottom sheet dismiss on back press also. If we look at the layout resource for the bottom sheet in android design support library, there is a View component with ID touch_outside and there is an OnClickListener set in method wrapInBottomSheet of BottomSheetDialog, which is used for detecting clicks outside and dismiss the dialog. So, to prevent cancel on touch outside the bottom sheet we need to remove the OnClickListener.

Add these line to onActivityCreated method (or any other life cycle method after onCreateView).

@Override public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    View touchOutsideView = getDialog().getWindow()
        .getDecorView()
        .findViewById(android.support.design.R.id.touch_outside);
    touchOutsideView.setOnClickListener(null);
}

Also if you want to prevent the bottom sheet dismiss by swiping down, change the bottom sheet dialog behaviour Hideable false. To setHideable(false) add the following code to the onCreateDialog method.

@Override public Dialog onCreateDialog(Bundle savedInstanceState) {
    final BottomSheetDialog bottomSheetDialog =
        (BottomSheetDialog) super.onCreateDialog(savedInstanceState);

    bottomSheetDialog.setOnShowListener(new DialogInterface.OnShowListener() {
      @Override public void onShow(DialogInterface dialog) {
        FrameLayout bottomSheet =
        bottomSheetDialog.findViewById(android.support.design.R.id.design_bottom_sheet);

        if (null != bottomSheet) {
          BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
        behavior.setHideable(false);
        }
      }
    });
    return bottomSheetDialog;
  }
shijo
  • 890
  • 7
  • 8
  • 1
    **Note**: This works in `onActivityCreated`, however it does not work in `onViewCreated` despite it coming after `onCreateView`. – AdamHurwitz Oct 29 '18 at 19:44
  • 1
    Thanks for `onActivityCreated`. @AdamHurwitz, yes, for `onViewCreated` see https://stackoverflow.com/questions/45614271/bottomsheetdialogfragment-doesnt-show-full-height-in-landscape-mode, there you can use `addOnGlobalLayoutListener`. – CoolMind Apr 16 '19 at 16:27
  • 4
    See also https://medium.com/@betakuang/make-your-bottomsheetdialog-noncancelable-e50a070cdf07. For AndroidX use `com.google.android.material.R.id.design_bottom_sheet` (I didn't test). – CoolMind Apr 17 '19 at 07:34
  • You are genius! Your `onActivityCreated` method helped me very much. I read https://medium.com/@anitas3791/android-bottomsheetdialog-3871a6e9d538, it worked, but I couldn't disable dismiss of `BottomSheetDialogFragment` in one case and enable in the other. See also https://stackoverflow.com/a/55726976/2914140. – CoolMind Apr 17 '19 at 11:55
  • Can I override the onClick to COLLAPSE the bottomsheet when clicked outside? – Sairaj Sawant Mar 06 '20 at 14:11
  • See also https://stackoverflow.com/a/61813321/2914140 to access `behavior` simplier. – CoolMind Jul 20 '20 at 08:07
  • 3
    For AndroidX, it's `com.google.android.material.R.id.touch_outside` you need to get, not `design_bottom_sheet` – Pierre-Olivier Dybman Oct 13 '20 at 20:46
  • 1
    This works just fine. Also thanks to @Pierre-OlivierDybman for updating the correct reference for the `id`. I have added one more solution with addon below. – sud007 Nov 19 '20 at 18:46
16

Simplest way is to set setCanceledOnTouchOutside to false on the BottomSheetDialogFragment's dialog. with Kotlin,

class XFragment : BottomSheetDialogFragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        dialog?.setCanceledOnTouchOutside(false)
    }

}
All Іѕ Vаиітy
  • 24,861
  • 16
  • 87
  • 111
3

The answer by M. Erfan Mowlaei is useful but I was looking for a way to enforce this behavior every time an instance of the class is created without having to remember to call setCancelable(false). See below.

class MyBottomSheet : BottomSheetDialogFragment() {

companion object {
    fun newInstance() = MyBottomSheet().apply {
        isCancelable = false
      }
   }
}
Ivan Wooll
  • 4,145
  • 3
  • 23
  • 34
2

I tried all the answers but here is the best working solution

The only thing that worked for me

Style.xml

<style name="BottomSheetDialogTheme" parent="@style/Theme.Design.Light.BottomSheetDialog">
        <item name="android:statusBarColor">@android:color/transparent</item>
        <item name="android:windowIsFloating">false</item>
        <item name="android:windowSoftInputMode">adjustResize|stateAlwaysVisible</item>
        <item name="android:navigationBarColor">@color/white</item>
        <item name="bottomSheetStyle">@style/BottomSheet</item>
</style>

<!-- set the rounded drawable as background to your bottom sheet -->
<style name="BottomSheet" parent="@style/Widget.Design.BottomSheet.Modal">
    <item name="android:background">@drawable/bg_bottom_sheet_dialog_fragment</item>
</style>

RoundedBottomSheetDialogFragment

open class RoundedBottomSheetDialogFragment : BottomSheetDialogFragment() {

    override fun getTheme(): Int = R.style.BottomSheetDialogTheme

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return BottomSheetDialog(requireContext(), theme)
    }

}

class UserDetailsSheet : RoundedBottomSheetDialogFragment() {

    init {
        isCancelable = false
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.user_info_fragment, container, false)
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val dialog = super.onCreateDialog(savedInstanceState)
        dialog.setCancelable(false)
        dialog.setCanceledOnTouchOutside(false)
        return dialog
    }
}
Ronny Shibley
  • 2,030
  • 22
  • 25
2

I could see a lot of answers but could not get them to work unltil found this one nice bolg post here

Here's one more solution on top of what is proposed by user @shijo above. All I did for my BottomSheetDialogFragment is just following code snippet inside onActivityCreated and was able to achieve these two behaviors.

  • disable the draggable behavior.

  • disable the cancel on touch outside.

      override fun onActivityCreated(savedInstanceState: Bundle?) {
      super.onActivityCreated(savedInstanceState)
      val dialog = dialog as BottomSheetDialog?
    
      val bottomSheet =
          dialog?.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
      val behavior: BottomSheetBehavior<View> = BottomSheetBehavior.from(bottomSheet as View)
      behavior.state = BottomSheetBehavior.STATE_EXPANDED
      behavior.peekHeight = 0
    
      // Disable Draggable behavior
      behavior.isDraggable = false
    
      // Disable cancel on touch outside
      val touchOutsideView =
          getDialog()?.window?.decorView?.findViewById<View>(com.google.android.material.R.id.touch_outside)
      touchOutsideView?.setOnClickListener(null)
    

This worked wonders for me. Since my bottomsheet UX demanded it to be non-cancelable and non-draggable, I was able to achieve it correctly with these changes.

sud007
  • 5,824
  • 4
  • 56
  • 63
2

Try Kotlin way

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            dialog?.setCancelable(false)
            dialog?.setCanceledOnTouchOutside(false)
            view.viewTreeObserver.addOnGlobalLayoutListener {
                val dialog = dialog as BottomSheetDialog?
                val bottomSheet =
                    dialog!!.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) as FrameLayout
                val behavior = BottomSheetBehavior.from(bottomSheet)
                behavior.state = BottomSheetBehavior.STATE_EXPANDED
                behavior.peekHeight = 0
                behavior.isDraggable = false
            }
    }
Mujahid Khan
  • 1,712
  • 1
  • 18
  • 24
1

I think all the above answers are a little incomplete, I will explain the reason why.

  1. setCancelable will stop outside click behavior but it will also stop your bottomsheetDialoagFragment from back press.
  2. Few answers are overriding the setCancelable() method to manage click outside, which is little complex as you will need to handle cancelable and hideable.

Solution

override fun onStart() {
       super.onStart()
       stopOutsideClick()
}

private fun stopOutsideClick() {
       val touchOutsideView = dialog?.window?.decorView?.findViewById<View>(com.google.android.material.R.id.touch_outside)
       touchOutsideView?.setOnClickListener(null)
}

you will have to call this method in onStart() where instance of dialog will always be there.

aamitarya
  • 719
  • 6
  • 11
  • That's a very good way to intercept the closing of the BottomSheet if you want to validate some data or ask the user if he really wants to exit before closing. – JU5T1C3 Aug 06 '21 at 14:35
1

simple and short working solution

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        getDialog().setCanceledOnTouchOutside(false);
    }

override onActivityCreated() method of BottomSheetDialogFragment in your customDialogFragment and setCanceledOnTouchOutside(false)

1

In kotlin is so simple , you can do it like this ;

class MyBottomSheetDialog : BottomSheetDialogFragment() {
/*********/

    override fun onCreateDialog(savedInstanceState: Bundle?) =
        super.onCreateDialog(savedInstanceState).apply {
            setCanceledOnTouchOutside(false)
            setOnShowListener { expand() } /**to Expand your bottomSheet according to the content**/
        }

/*******/

}
mazend
  • 456
  • 1
  • 7
  • 37
Anis MARZOUK
  • 266
  • 2
  • 9
0

I faced same issue. But I needed to prevent bottom sheet fragment from close by click outside but to leave opportunity to close by swipe down. Just add variable isHideable: Boolean to your BSFragment class and add next snippet

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    val dialog = super.onCreateDialog(savedInstanceState)
    dialog.setOnShowListener {
        val d = dialog as BottomSheetDialog
        val bottomSheet =
            d.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
        BottomSheetBehavior.from(bottomSheet!!).state = initialState()
        BottomSheetBehavior.from(bottomSheet).setHideable(isHideable)
    }
    return dialog
}

and when you call your fragment - set isHideable to true

private fun showBSFragmentDialog() {
  val bSFDialog = BSFragment.newInstance()
  bSFDialog.isCancelable = false
  bSFDialog.isHideable = true
  bSFDialog.show(supportFragmentManager, "bSFDialog")  }

now user cannot close it by click outside but able to swipe down and close dialog

Kirguduck
  • 748
  • 1
  • 9
  • 20
0

For me, I was looking for the opposite, to close the BottomSheetDialogFragment when I click outside:

class MyBottomSheetDialogFragment : BottomSheetDialogFragment() {
    ...
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
        super.onCreateDialog(savedInstanceState).apply {
            setCanceledOnTouchOutside(true)
        }
}

To disable the canceling on outside click, just set the value to false

Yasser AKBBACH
  • 539
  • 5
  • 7