Is there any way to catch the dismissal/cancel of a BottomSheetDialogFragment?
Bottom sheet class
public class ContactDetailFragment 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_contactdetail, null);
dialog.setContentView(contentView);
BottomSheetBehavior mBottomSheetBehavior = BottomSheetBehavior.from(((View) contentView.getParent()));
if (mBottomSheetBehavior != null)
{
mBottomSheetBehavior.setBottomSheetCallback(mBottomSheetBehaviorCallback);
mBottomSheetBehavior.setPeekHeight((int) DisplayUtils.dpToPixels(CONTACT_DETAIL_PEEK_HEIGHT, getResources().getDisplayMetrics()));
}
}
}
What I've tried that doesn't work
in setupDialog adding either of dialog.setOnCancelListener(); or dialog.setOnDismissListener(); never gets triggered
the bottomsheet behavior's onStateChanged only gets triggered if the user drags the bottomsheet down passed the collapsed state, and there is no state for dismissed/cancelled
adding the same oncancel/ondismiss listeners to the instantiation of the BottomSheetDialogFragment, by using ContactDetailFragment.getDialog().setOnCancelListener() does not get triggered
Given that it's essentially a dialog fragment, there must be some way to catch the dismissal?
Found a simple solution.
Using onDestroy or onDetach in the BottomSheetDialogFragment allows me to get the dismissal correctly
Related
I want to control my bottom sheet's state with one button.
The control button is located at the top of the bottom sheet layout.
And if i drag up the control button, bottom sheet will expand.
I tried to search what event triggers bottomsheet's expansion, but couldn't find out.
So my question is, what method in bottomsheetbehavior.class triggers stage change?
enter image description here
You can use this way to change state based on button click
controlBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED){
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
else {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
The callback that gets triggered when your bottomsheet state changes is
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomsht, int newstate) {
if (newState == BottomSheetBehavior.STATE_DRAGGING) {
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
}
#Override
public void onSlide(#NonNull View view, float v) {
}
});
So I made a custom class that does basically nothing except boilerplate code plus customizing the callback. For whatever reason, I can't seem to make it cancel when I touch outside the bounds of the bottom sheet.
public class CustomBottomSheetDialog extends AppCompatDialog {
public CustomBottomSheetDialog(Context context) {
super(context, R.style.Theme_Design_Light_BottomSheetDialog);
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
#Override
public void setContentView(View view) {
final CoordinatorLayout coordinator = (CoordinatorLayout) View.inflate(getContext(),
R.layout.design_bottom_sheet_dialog, null);
FrameLayout bottomSheet = (FrameLayout) coordinator.findViewById(R.id.design_bottom_sheet);
BottomSheetBehavior.from(bottomSheet).setBottomSheetCallback(mBottomSheetCallback);
bottomSheet.addView(view);
super.setContentView(coordinator);
}
private BottomSheetBehavior.BottomSheetCallback mBottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
cancel(); // The only not boilerplate code here, woo
}
}
#Override
public void onSlide(View bottomSheet, float slideOffset) { }
};
Things I've tried:
bottomSheetDialog.setCancelable(true);
bottomSheetDialog.setCanceledOnTouchOutside(true);
Overriding dispatchTouchEvent, but I can't get the rectangle to equal anything besides the size of the entire screen.
If I don't use the custom class (ie simply change my CustomBottomSheetDialog call to just BottomSheetDialog), I get the cancel on touch outside, but then I don't get a cancel when I drag to hide the dialog, which I am required to have.
Finally got it. In the onCreate, I added a single line of code to find the touch_outside view and add a click listener to cancel the dialog. The touch_outside view is generated by default. I did not need to add it in my bottom sheet's XML.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
findViewById(R.id.touch_outside).setOnClickListener(v -> cancel()); // <--- this guy
getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
Thanks goes to this tutorial.
How can I listen to a FINAL dismissal of a BottomSheetDialogFragment? I want to save user changes on the final dismissal only...
I tried following:
Method 1
This only fires, if the dialog is dismissed by swiping it down (not on back press or on touch outside)
#Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
Dialog d = super.onCreateDialog(savedInstanceState);
d.setOnShowListener(new DialogInterface.OnShowListener() {
#Override
public void onShow(DialogInterface dialog) {
BottomSheetDialog d = (BottomSheetDialog) dialog;
FrameLayout bottomSheet = (FrameLayout) dialog.findViewById(android.support.design.R.id.design_bottom_sheet);
BottomSheetBehavior behaviour = BottomSheetBehavior.from(bottomSheet);
behaviour.setState(BottomSheetBehavior.STATE_EXPANDED);
behaviour.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN)
{
// Bottom Sheet was dismissed by user! But this is only fired, if dialog is swiped down! Not if touch outside dismissed the dialog or the back button
Toast.makeText(MainApp.get(), "HIDDEN", Toast.LENGTH_SHORT).show();
dismiss();
}
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
}
});
}
});
return d;
}
Method 2
This does not allow me to distinguish between a final dismissal and one that is coming from a screen rotation or activity recreation...
#Override
public void onDismiss(DialogInterface dialog)
{
super.onDismiss(dialog);
// this works fine but fires one time too often for my use case, it fires on screen rotation as well, although this is a temporarily dismiss only
Toast.makeText(MainApp.get(), "DISMISSED", Toast.LENGTH_SHORT).show();
}
Question
How can I listen to an event that indicates, that the user has finished the dialog?
Although all similar questions on SO suggest using onDismiss I think following is the correct solution:
#Override
public void onCancel(DialogInterface dialog)
{
super.onCancel(dialog);
Toast.makeText(MainApp.get(), "CANCEL", Toast.LENGTH_SHORT).show();
}
This fires if:
* the user presses back
* the user presses outside of the dialog
This fires NOT:
* on screen rotation and activity recreation
Solution
Combine onCancel and BottomSheetBehavior.BottomSheetCallback.onStateChanged like following:
public class Dailog extends BottomSheetDialogFragment
{
#Override
public void onCancel(DialogInterface dialog)
{
super.onCancel(dialog);
handleUserExit();
}
#Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
Dialog d = super.onCreateDialog(savedInstanceState);
d.setOnShowListener(new DialogInterface.OnShowListener() {
#Override
public void onShow(DialogInterface dialog) {
BottomSheetDialog d = (BottomSheetDialog) dialog;
FrameLayout bottomSheet = (FrameLayout) d.findViewById(android.support.design.R.id.design_bottom_sheet);
BottomSheetBehavior behaviour = BottomSheetBehavior.from(bottomSheet);
behaviour.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN)
{
handleUserExit();
dismiss();
}
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
}
});
}
});
return d;
}
private void handleUserExit()
{
Toast.makeText(MainApp.get(), "TODO - SAVE data or similar", Toast.LENGTH_SHORT).show();
}
}
If you extended from BottomSheetDialogFragment() just override in your class
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
//Code here
}
This will trigger when onBackPress and when you dismiss the dialog by clicking outside of it.
Make sure to NOT set your dialog as cancelable because this will not fire
bottomSheetDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
#Override
public void onDismiss(DialogInterface dialog) {
toast("dismissed");
}
});
i achieved this using this simple trick
val bottomSheetDialog = FeedbackFormsFragment.createInstance()
bottomSheetDialog.show((activity as FragmentActivity).supportFragmentManager, BOTTOM_SHEET)
// add some delay to allow the bottom sheet to be visible first so that the dialog is not null
Handler().postDelayed({
bottomSheetDialog.dialog?.setOnDismissListener {
// add code here
}
}, 1000)
While the method of #prom85 works, there is a different one. If you want to dismiss a BottomSheetDialogFragment in one case and retain in the other, it won't work. It will close the dialog in all cases.
For instance, if you entered text inside EditText of BottomSheetDialogFragment and occasionally clicked outside, it would close the dialog without any warning. I tried https://medium.com/#anitas3791/android-bottomsheetdialog-3871a6e9d538, it works, but in another scenario. When you drag the dialog down, it will dismiss it. If you click outside, it won't show any alert message and won't dismiss the dialog.
So, I used a nice advice of #shijo from https://stackoverflow.com/a/50734566/2914140.
Add these lines 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(yourClickListener);
}
In my case in yourClickListener I check the text and show an alert or dismiss the dialog:
private fun checkAndDismiss() {
if (newText == oldText) {
dismissAllowingStateLoss()
} else {
showDismissAlert()
}
}
When you create BottomSheetDialogFragment, don't call setCancelable(false) as in https://stackoverflow.com/a/42679131/2914140 or these methods probably won't work. And maybe don't set <item name="android:windowCloseOnTouchOutside">false</item> in styles or setCanceledOnTouchOutside(false) in onCreateDialog.
I also tried several ways to override cancel behavior, but they didn't succeed.
<style name="BottomSheetDialogTheme" parent="Theme.Design.Light.BottomSheetDialog">
<item name="android:windowCloseOnTouchOutside">false</item>
</style>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.BottomSheetDialogTheme)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.setOnShowListener {
val bottomSheet = dialog.findViewById<View>(
android.support.design.R.id.design_bottom_sheet) as? FrameLayout
val behavior = BottomSheetBehavior.from(bottomSheet)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
//showing the different states.
when (newState) {
BottomSheetBehavior.STATE_HIDDEN -> dismiss() //if you want the modal to be dismissed when user drags the bottomsheet down
BottomSheetBehavior.STATE_EXPANDED -> {
}
BottomSheetBehavior.STATE_COLLAPSED -> {
}
BottomSheetBehavior.STATE_DRAGGING -> {
}
BottomSheetBehavior.STATE_SETTLING -> {
}
}
}
})
dialog.setOnCancelListener {
// Doesn't matter what you write here, the dialog will be closed.
}
dialog.setOnDismissListener {
// Doesn't matter what you write here, the dialog will be closed.
}
}
return dialog
}
override fun onCancel(dialog: DialogInterface?) {
// Doesn't matter what you write here, the dialog will be closed.
super.onCancel(dialog)
}
override fun onDismiss(dialog: DialogInterface?) {
// Doesn't matter what you write here, the dialog will be closed.
super.onDismiss(dialog)
}
In an AppCompatActivity you can use the following technique:
val mgr = supportFragmentManager
val callback = object: FragmentManager.OnBackStackChangedListener {
var count = 0
override fun onBackStackChanged() {
// We come here twice, once when the sheet is opened,
// once when it's closed.
if (++count >= 2) {
mgr.removeOnBackStackChangedListener(this)
doWhatNeedsToBeDoneWhenTheSheetIsClosed()
}
}
}
mgr.addOnBackStackChangedListener(callback)
Be sure to do the above just before you call show on the sheet.
This code works for me:
bottomSheetDialogFragment.getDialog().setOnDismissListener(dialog -> {
// code goes here
});
Note, that you should call it after showing bottomSheetDialogFragment (showNow in my case), otherwise getDialog() will return you null.
LoctSetupDialog loctSetupDialog = new LoctSetupDialog();
loctSetupDialog.show(requireActivity().getSupportFragmentManager(),"loctSetup");
loctSetupDialog.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> {
if(event == Lifecycle.Event.ON_DESTROY)
{
refreshLoctName();
}
});
I am pretty curious about the behavior of the BottomSheetDialog when it is dismissed : when the user draggs it down to hide it, it will remain hidden, even if bottomSheetDialog#show() is called after. This only happens when it is dragged down, not when the user touches outside or when bottomSheetDialog#dismiss() is called programatically.
It is really annoying because I have a pretty big bottomSheetDialog with a recyclerview inside, and I have to create a new one every time I want to show the bottomSheetDialog.
So instead of just doing this :
if(bottomSheetDialog != null){
bottomSheetDialog.show();
else{
createNewBottomSheetDialog();
}
I have to create one every time.
Am I missing something or is it the normal behavior ? (Btw I use appcompat-v7:23.2.1)
So I finally managed to solve this problem by looking directly into the BottomSheetDialog implementation, and I discovered it was nothing but a simple Dialog wrapped into a regular BottomSheet.
The problem was in the BottomSheetCallBack :
#Override
public void onStateChanged(#NonNull View bottomSheet,
#BottomSheetBehavior.State int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
dismiss();
}
}
The problem occurs when the state HIDDEN is reached which happens when the dialog is dismissed by being dragged down. After that the dialog stays hidden even if bottomSheetDialog.show() is called. The most simple fix I found was to remove this state and replace it by the COLLAPSED state.
I create a classCustomBottomSheetDialog, copied the entire BottomSheetDialog class and added a single line to fix the problem :
#Override
public void onStateChanged(#NonNull View bottomSheet,
#BottomSheetBehavior.State int newState) {
if (newState == CustomBottomSheetBehavior.STATE_HIDDEN) {
dismiss();
bottomSheetBehavior.setState(CustomBottomSheetBehavior.STATE_COLLAPSED);
}
}
Here is the final code:
public class CustomBottomSheetDialog extends AppCompatDialog {
public CustomBottomSheetDialog (#NonNull Context context) {
this(context, 0);
}
public CustomBottomSheetDialog (#NonNull Context context, #StyleRes int theme) {
super(context, getThemeResId(context, theme));
// We hide the title bar for any style configuration. Otherwise, there will be a gap
// above the bottom sheet when it is expanded.
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
}
protected CustomBottomSheetDialog (#NonNull Context context, boolean cancelable,
OnCancelListener cancelListener) {
super(context, cancelable, cancelListener);
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
}
#Override
public void setContentView(#LayoutRes int layoutResId) {
super.setContentView(wrapInBottomSheet(layoutResId, null, null));
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setLayout(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
#Override
public void setContentView(View view) {
super.setContentView(wrapInBottomSheet(0, view, null));
}
#Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
super.setContentView(wrapInBottomSheet(0, view, params));
}
private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) {
final CoordinatorLayout coordinator = (CoordinatorLayout) View.inflate(getContext(),
R.layout.design_bottom_sheet_dialog, null);
if (layoutResId != 0 && view == null) {
view = getLayoutInflater().inflate(layoutResId, coordinator, false);
}
FrameLayout bottomSheet = (FrameLayout) coordinator.findViewById(R.id.design_bottom_sheet);
BottomSheetBehavior.from(bottomSheet).setBottomSheetCallback(mBottomSheetCallback);
if (params == null) {
bottomSheet.addView(view);
} else {
bottomSheet.addView(view, params);
}
// We treat the CoordinatorLayout as outside the dialog though it is technically inside
if (shouldWindowCloseOnTouchOutside()) {
coordinator.findViewById(R.id.touch_outside).setOnClickListener(
new View.OnClickListener() {
#Override
public void onClick(View view) {
if (isShowing()) {
cancel();
}
}
});
}
return coordinator;
}
private boolean shouldWindowCloseOnTouchOutside() {
if (Build.VERSION.SDK_INT < 11) {
return true;
}
TypedValue value = new TypedValue();
//noinspection SimplifiableIfStatement
if (getContext().getTheme()
.resolveAttribute(android.R.attr.windowCloseOnTouchOutside, value, true)) {
return value.data != 0;
}
return false;
}
private static int getThemeResId(Context context, int themeId) {
if (themeId == 0) {
// If the provided theme is 0, then retrieve the dialogTheme from our theme
TypedValue outValue = new TypedValue();
if (context.getTheme().resolveAttribute(
R.attr.bottomSheetDialogTheme, outValue, true)) {
themeId = outValue.resourceId;
} else {
// bottomSheetDialogTheme is not provided; we default to our light theme
themeId = R.style.Theme_Design_Light_BottomSheetDialog;
}
}
return themeId;
}
private BottomSheetBehavior.BottomSheetCallback mBottomSheetCallback
= new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet,
#BottomSheetBehavior.State int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
dismiss();
bottomSheetBehavior.setState(CustomBottomSheetBehavior.STATE_COLLAPSED);
}
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
}
};
}
Update: The problem has been resolved at some version of the support library. I don't really know the exact version that fixes it but in 27.0.2 it is fixed.
Note: The URL does no longer refer to the issue described due to some modification on the URL schema by Google.
A workaround better than copying the whole class just to add a single line
// Fix BottomSheetDialog not showing after getting hidden when the user drags it down
myBottomSheetDialog.setOnShowListener(new DialogInterface.OnShowListener() {
#Override
public void onShow(DialogInterface dialogInterface) {
BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialogInterface;
FrameLayout bottomSheet = (FrameLayout) bottomSheetDialog
.findViewById(android.support.design.R.id.design_bottom_sheet);
BottomSheetBehavior.from(bottomSheet).setState(BottomSheetBehavior.STATE_COLLAPSED);
}
});
see: https://code.google.com/p/android/issues/detail?id=202396#c7
I have sample demo I hope that is useful.
public class BottomListMenu extends BottomSheetDialog {
private List<MenuDTO> menuList;
private OnMenuItemTapped menuTapListener;
public BottomListMenu(#NonNull Context context, List<MenuDTO> menuList, OnMenuItemTapped menuTapListener) {
super(context);
this.menuList = menuList;
this.menuTapListener = menuTapListener;
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dialog_menu_list);
RecyclerView rcvList = (RecyclerView) findViewById(R.id.rcv_menu_list);
rcvList.setLayoutManager(new LinearLayoutManager(getContext()));
BottomSheetMenuListAdapter adapter = new BottomSheetMenuListAdapter(getContext(), this, menuList, menuTapListener);
rcvList.setAdapter(adapter);
}
}
--- Use ---
BottomListMenu menu = new BottomListMenu(MainActivity.this, MenuUtils.getListMenu(MainActivity.this), new OnMenuItemTapped() {
#Override
public void onClickMenuItem(MenuDTO menu) {
if (menu.getMenuTitle().equals(getString(R.string.menu_edit))) {
Toast.makeText(MainActivity.this, "Edit Clicked", Toast.LENGTH_SHORT).show();
} else if (menu.getMenuTitle().equals(getString(R.string.menu_delete))) {
Toast.makeText(MainActivity.this, "Delete Clicked", Toast.LENGTH_SHORT).show();
} else if (menu.getMenuTitle().equals(getString(R.string.menu_attach))) {
Toast.makeText(MainActivity.this, "Attach Clicked", Toast.LENGTH_SHORT).show();
}
}
});
menu.show();
-- Full Sample Code Available Here --
https://github.com/bita147/BottomSheetDialog
How can the background be dimmed just like it is shown here?
I've set it up normally using the CoordinatorLayout and the BottomSheetBehavior.
This will simply show a bottom sheet.
public class MyBottomSheet extends BottomSheetDialogFragment {
private static final String TAG = "MyBottomSheet";
#NonNull
#Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
final BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState);
final View view = View.inflate(getContext(), R.layout.my_custom_view, null);
dialog.setContentView(view);
behavior = BottomSheetBehavior.from((View) view.getParent());
return dialog;
}
public void show(final FragmentActivity fragmentActivity) {
show(fragmentActivity.getSupportFragmentManager(), TAG);
}
}
To close the dialog simply as normal call close().
bottomSheetBehavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
(your_layout/view).setAlpha(1 - slideOffset);
}
});
use BottomSheetDialog
basically it's a dialog (i.e support dim) and support modal bottom sheets behaviour give it a try