Detect back button but don't dismiss dialogfragment
Asked Answered
C

15

103

I have a dialogfragment for a floating dialog which includes a special keyboard that pops up when a user presses inside an EditText field (the normal IME is stopped from being displayed).

I would like the keyboard to be dismissed (visibility = GONE) when the user presses the back button (just as with a normal IME service) but the dialog to remain visible. However, there does not appear to be a way to do this as far as I can see from my fairly extensive reading on SO and elsewhere.

If I set the dialog to be non-cancelable then I don't get notified by onCancel() or onDismiss() because the dialog isn't cancelable.

If I set the dialog to be cancelable then I get notified, but the dialog is dismissed.

I can't attach an onKeyListener to the dialog in the fragment because it is replaced by the system so that the fragment can handle the dialog's life cycle.

Is there any way to do this? Or has access to the detection of key events been entirely fenced off for the purposes of the Fragment system?

Candie answered 23/1, 2014 at 12:14 Comment(0)
V
224

The best way and cleanest way is to override onBackPressed() in the dialog you created in onCreateDialog().

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    return new Dialog(getActivity(), getTheme()){
        @Override
        public void onBackPressed() {
            //do your stuff
        }
    };
}
Vaal answered 29/8, 2014 at 18:30 Comment(9)
This does not work in DialogFragments though since there is no onBackPressed() in the DialogFragment class.Salzburg
DialogFragments wrap a Dialog - onCreateDialog creates this dialog. It works in DialogFragments.Cryostat
I may only confirm this as best and most easy solution. Thanks for sharing this @Ian WongWheelbase
Dialog could also be an instance of AppCompatDialogOldcastle
any ideas on what to do for AppCompatDialogFragment ?Teressaterete
for some reason when you do it in BottomSheetDialogFragment the dialog becomes scuffed, the theme and styles are removed after thisClance
when doing this for a BottomSheetDialogFragment you can just create a new BottomSheetDialog(context, theme) and pass in the context and theme. If you check the super method that is all it does.Amaranthine
How do you this, when creating the dialog with onCreateView instead of onCreateDialog? I went with Manimaran As solution for now. https://mcmap.net/q/209358/-detect-back-button-but-don-39-t-dismiss-dialogfragmentLeaved
For Kotlin: override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return object : Dialog(activity!!, theme) { override fun onBackPressed() { //do your stuff } } }Marijo
S
92

I had the same problem than you and I've fixed it attaching the onKeyListener to the dialogfragment.

In the method onResume() of the class that extend of DialogFragment put these piece of code:

    getDialog().setOnKeyListener(new OnKeyListener()
    {
        @Override
        public boolean onKey(android.content.DialogInterface dialog, int keyCode,android.view.KeyEvent event) {

            if ((keyCode ==  android.view.KeyEvent.KEYCODE_BACK))
                {
                     //Hide your keyboard here!!!
                     return true; // pretend we've processed it
                }
            else 
                return false; // pass on to be processed as normal
        }
    });

Here one of the problems that you can find is this code is going to be executed twice: one when the user press tha back button and another one when he leave to press it. In that case, you have to filter by event:

@Override
public void onResume() {
    super.onResume();

    getDialog().setOnKeyListener(new OnKeyListener()
    {
        @Override
        public boolean onKey(android.content.DialogInterface dialog, int keyCode,
                android.view.KeyEvent event) {

            if ((keyCode ==  android.view.KeyEvent.KEYCODE_BACK))
            {
                //This is the filter
                if (event.getAction()!=KeyEvent.ACTION_DOWN)
                        return true;
                else
                {
                    //Hide your keyboard here!!!!!!
                    return true; // pretend we've processed it
                }
            } 
            else 
                return false; // pass on to be processed as normal
        }
    });
}
Scepter answered 10/4, 2014 at 12:0 Comment(1)
Instead of using the filter, I added getDialog().setOnKeyListener(null) which prevents the second call.Prototype
E
32

As an addendum to Juan Pedro Martinez's answer I thought it would be helpful to clarify a specific question (one that I had) when looking at this thread.

If you wish to create a new DialogFragment and have it so the user can only cancel it using the back-button, which eliminates random screen touches from canceling the fragment prematurely, then this is the code that you would use.

In what ever code that you call the DialogFragment you need to set the cancelable setting to false so that NOTHING dismisses the fragment, no stray screen touches, etc.

DialogFragment mDialog= new MyDialogFragment();
mDialog.setCancelable(false);
mDialog.show(getFragmentManager(), "dialog");

Then, within your DialogFragment, in this case MyDaialogFragment.java, you add the onResume override code to have the dialog listen for the Back Button. When it's pressed it will execute the dismiss() to close the fragment.

@Override
 public void onResume() 
 {
     super.onResume();

     getDialog().setOnKeyListener(new OnKeyListener()
     {
         @Override
         public boolean onKey(android.content.DialogInterface dialog, 
                              int keyCode,android.view.KeyEvent event) 
         {
              if ((keyCode ==  android.view.KeyEvent.KEYCODE_BACK))
              {
                   // To dismiss the fragment when the back-button is pressed.
                   dismiss();
                   return true;
              }
              // Otherwise, do nothing else
              else return false;
         }
   });
}

Now your dialog will be called with the "setCancelable" to false, meaning nothing (no outside clicks) can cancel it and shut it down, and allowing (from within the dialog itself) only the back button to close it.

Ganbatte!

Eparchy answered 11/8, 2014 at 19:43 Comment(1)
Perfect, thanks, I wanted to prevent accidental outside clicks whilst maintaining back button functionality, for me all I had to do was setCancelable(false) on the dialog instance in onCreateView inside the DialogFragment.Jhvh
G
28

How has no one suggested this?

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

  // Add back button listener
  dialog.setOnKeyListener(new Dialog.OnKeyListener() {
    @Override
    public boolean onKey(DialogInterface dialogInterface, int keyCode, KeyEvent keyEvent) {
      // getAction to make sure this doesn't double fire
      if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.getAction() == KeyEvent.ACTION_UP) {
        // Your code here
        return true; // Capture onKey
      }
      return false; // Don't capture
    }
  });

  return dialog;
}
Generalize answered 10/2, 2018 at 14:51 Comment(0)
M
23

Use Fragment onCancel override method. It's called when you press back. here is a sample:

@Override
public void onCancel(DialogInterface dialog) {
    super.onCancel(dialog);

    // Add you codition
}
Melissamelisse answered 18/12, 2018 at 7:46 Comment(2)
Note, that with this approach you cannot handle whether the DialogFragment will be dismissed or not. You can just get notified that it is to be dismissedHendon
Indeed @Leo Droidcoder Any idea how to intercept back button pressed before the dialogfragment cannot be dismissed ?Obstruent
M
10

Since AppCompat 1.5.0-alpha01 AppCompatDialog is now extending ComponentDialog (source). That means in your DialogFragment you can now easily get a valid OnBackPressedDispatcher (from the fragment's dialog) that allows you to handle Back presses yourself.

kotlin:

class MyFragment: androidx.fragment.app.DialogFragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        (dialog as androidx.activity.ComponentDialog)
            .onBackPressedDispatcher
            .addCallback(viewLifecycleOwner) {
                // handle back press
            }
    }
}

Note: when you add the callback it prevents the dialog from being dismissed/cancelled automatically by pressing Back.

Masinissa answered 5/7, 2022 at 9:18 Comment(1)
This should be the correct answer so far.Sse
S
5

When creating the dialog, override both onBackPressed and onTouchEvent :

        final Dialog dialog = new Dialog(activity) {
            @Override
            public boolean onTouchEvent(final MotionEvent event) {
                //note: all touch events that occur here are outside the dialog, yet their type is just touching-down
                boolean shouldAvoidDismissing = ... ;
                if (shouldAvoidDismissing) 
                    return true;
                return super.onTouchEvent(event);
            }

            @Override
            public void onBackPressed() {
                boolean shouldAvoidDismissing = ... ;
                if (!shouldSwitchToInviteMode)
                    dismiss();
                else
                    ...
            }
        };
Strobel answered 7/7, 2016 at 9:38 Comment(0)
O
5

Prevent canceling DialogFragment:

dialog.setCanceledOnTouchOutside(false)
dialog.setCancelable(false)
dialog.setOnKeyListener { dialog, keyCode, event ->
    keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP
}
Ordzhonikidze answered 12/6, 2020 at 11:51 Comment(0)
L
4

Extending off Juan Pedro Martinez's answer above. I wrote an extension on a DialogFragment that one could set from the onCreate() which will automatically set the key listener and remove it based off the lifecycle.

fun DialogFragment.setOnBackPressListener(onBackPress: () -> Boolean) {
    val listener = DialogInterface.OnKeyListener { _, keyCode, event ->
        if (keyCode == KeyEvent.KEYCODE_BACK && event?.action != KeyEvent.ACTION_DOWN) {
            onBackPress()
        } else {
            false
        }
    }
    val observer = object : LifecycleObserver {
        @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
        fun onResume() {
             dialog?.setOnKeyListener(listener)
        }

        @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
        fun onPause() {
             dialog?.setOnKeyListener(null)
        }
    }
    lifecycle.addObserver(observer)
}

Usage in DialogFragment#onCreate

setOnBackPressListener {
    // handle back press here

    true // return true to indicate back press was handled, false if not
}
Laoighis answered 31/8, 2021 at 23:59 Comment(0)
L
4

Try this and back to upvote my comment :D

/**
 * Callback when Back button is pressed.
 * By default it gonna call back press of host activity
 */
protected open fun onBackPressed() {
    requireActivity().onBackPressed()
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    return object : BottomSheetDialog(context!!, theme) {
        override fun onBackPressed() {
            [email protected]()
        }

        override fun setOnKeyListener(onKeyListener: DialogInterface.OnKeyListener?) {
            //Do not call super function
            //This function do nothing but DON'T REMOVE this.
            //Try to set null for onKeyListener is not working.
        }
    }
}

enter image description here

Luminal answered 19/11, 2021 at 3:47 Comment(0)
S
1

Use onDismiss() callback of DialogFragment with a closeActivity flag

private var closeActivity: Boolean = true    

override fun onDismiss(dialog: DialogInterface?) {
        super.onDismiss(dialog)

        if (closeActivity) {
            activity!!.finish()
        }
    }
Sappy answered 18/9, 2018 at 7:15 Comment(0)
H
0

It's a bit tricky here because we don't get any listner or callbacks on dialog fragment. On way to do it is to override the onDismiss() which will work as system callback which tells that dialog is dismissed and based on that we can setup our own custom callback as follow

override fun onDismiss(dialog: DialogInterface) {
    super.onDismiss(dialog)
    debugLog("Dialog is dismissed")
    dismiss()
    listener?.onRemoteBackPressed()
}
Heddie answered 13/10, 2022 at 13:24 Comment(0)
M
0

You can override onDismiss method to detect the backpressed event

override fun onDismiss(dialog: DialogInterface) {
    super.onDismiss(dialog)

    // ... Do whatever you want
}
Monitor answered 23/1, 2023 at 20:10 Comment(0)
P
0

With latest androidx support library, we can use the BackPressedDispatcher to dismiss the dialog fragment. Add below code in dialog fragment class to dismiss.

Refer Android Doc

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

    requireActivity().getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
        @Override
        public void handleOnBackPressed() {
            dismiss();
        }
    });

}
Protectionist answered 13/10, 2023 at 5:56 Comment(0)
M
-1

AndroidX OnBackPressedDispatcher can also be option for someone

Marlysmarmaduke answered 24/8, 2020 at 13:29 Comment(1)
That doesn't seem to be the case: issuetracker.google.com/issues/149173280Morehead

© 2022 - 2024 — McMap. All rights reserved.