简体   繁体   English

防止在 Android 屏幕旋转时关闭对话框

[英]Prevent dialog dismissal on screen rotation in Android

I am trying to prevent dialogs built with Alert builder from being dismissed when the Activity is restarted.我试图防止在重新启动活动时关闭使用警报构建器构建的对话框。

If I overload the onConfigurationChanged method I can successfully do this and reset the layout to correct orientation but I lose sticky text feature of edittext.如果我重载 onConfigurationChanged 方法,我可以成功地做到这一点并将布局重置为正确的方向,但我失去了edittext的粘性文本功能。 So in solving the dialog problem I have created this edittext problem.因此,在解决对话框问题时,我创建了这个 edittext 问题。

If I save the strings from the edittext and reassign them in the onCofiguration change they still seem to default to initial value not what was entered before rotation.如果我从 edittext 保存字符串并在 onCofiguration 更改中重新分配它们,它们似乎仍然默认为初始值,而不是在旋转之前输入的值。 Even if I force an invalidate does seem to update them.即使我强制无效似乎也会更新它们。

I really need to solve either the dialog problem or the edittext problem.我真的需要解决对话框问题或编辑文本问题。

Thanks for the help.谢谢您的帮助。

The best way to avoid this problem nowadays is by using a DialogFragment .现在避免这个问题的最好方法是使用DialogFragment

Create a new class which extends DialogFragment .创建一个扩展DialogFragment的新类。 Override onCreateDialog and return your old Dialog or an AlertDialog .覆盖onCreateDialog并返回旧的DialogAlertDialog

Then you can show it with DialogFragment.show(fragmentManager, tag) .然后你可以用DialogFragment.show(fragmentManager, tag)显示它。

Here's an example with a Listener :这是一个带有Listener的示例:

public class MyDialogFragment extends DialogFragment {

    public interface YesNoListener {
        void onYes();

        void onNo();
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        if (!(activity instanceof YesNoListener)) {
            throw new ClassCastException(activity.toString() + " must implement YesNoListener");
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        return new AlertDialog.Builder(getActivity())
                .setTitle(R.string.dialog_my_title)
                .setMessage(R.string.dialog_my_message)
                .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        ((YesNoListener) getActivity()).onYes();
                    }
                })
                .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        ((YesNoListener) getActivity()).onNo();
                    }
                })
                .create();
    }
}

And in the Activity you call:在您调用的 Activity 中:

new MyDialogFragment().show(getSupportFragmentManager(), "tag"); // or getFragmentManager() in API 11+

This answer helps explain these other three questions (and their answers):这个答案有助于解释其他三个问题(及其答案):

// Prevent dialog dismiss when orientation changes
private static void doKeepDialog(Dialog dialog){
    WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
    lp.copyFrom(dialog.getWindow().getAttributes());
    lp.width = WindowManager.LayoutParams.WRAP_CONTENT;
    lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
    dialog.getWindow().setAttributes(lp);
}
public static void doLogout(final Context context){     
        final AlertDialog dialog = new AlertDialog.Builder(context)
        .setIcon(android.R.drawable.ic_dialog_alert)
        .setTitle(R.string.titlelogout)
        .setMessage(R.string.logoutconfirm)
        .setPositiveButton("Yes", new DialogInterface.OnClickListener()
        {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                ...   
            }

        })
        .setNegativeButton("No", null)      
        .show();    

        doKeepDialog(dialog);
    }

If you're changing the layout on orientation change I wouldn't put android:configChanges="orientation" in your manifest because you're recreating the views anyway.如果您在更改方向时更改布局,我不会将android:configChanges="orientation"放在您的清单中,因为无论如何您都在重新创建视图。

Save the current state of your activity (like text entered, shown dialog, data displayed etc.) using these methods:使用以下方法保存活动的当前状态(如输入的文本、显示的对话框、显示的数据等):

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
}

That way the activity goes through onCreate again and afterwards calls the onRestoreInstanceState method where you can set your EditText value again.这样,活动再次通过 onCreate,然后调用 onRestoreInstanceState 方法,您可以在其中再次设置 EditText 值。

If you want to store more complex Objects you can use如果你想存储更复杂的对象,你可以使用

@Override
public Object onRetainNonConfigurationInstance() {
}

Here you can store any object and in onCreate you just have to call getLastNonConfigurationInstance();在这里你可以存储任何对象,在 onCreate 你只需要调用getLastNonConfigurationInstance(); to get the Object.获取对象。

Just add android:configChanges="orientation" with your activity element in AndroidManifest.xml只需在 AndroidManifest.xml 中使用您的活动元素添加 android:configChanges="orientation"

Example:例子:

<activity
            android:name=".YourActivity"
            android:configChanges="orientation"
            android:label="@string/app_name"></activity>

This question was answered a long time ago.这个问题很久以前就有答案了。

Yet this is non-hacky and simple solution I use for myself.然而,这是我为自己使用的非hacky简单的解决方案。

I didthis helper class for myself, so you can use it in your application too.我为自己做了这个助手类,所以你也可以在你的应用程序中使用它。

Usage is:用法是:

PersistentDialogFragment.newInstance(
        getBaseContext(),
        RC_REQUEST_CODE,
        R.string.message_text,
        R.string.positive_btn_text,
        R.string.negative_btn_text)
        .show(getSupportFragmentManager(), PersistentDialogFragment.TAG);

Or要么

 PersistentDialogFragment.newInstance(
        getBaseContext(),
        RC_EXPLAIN_LOCATION,
        "Dialog title", 
        "Dialog Message", 
        "Positive Button", 
        "Negative Button", 
        false)
    .show(getSupportFragmentManager(), PersistentDialogFragment.TAG);





public class ExampleActivity extends Activity implements PersistentDialogListener{

        @Override
        void onDialogPositiveClicked(int requestCode) {
                switch(requestCode) {
                  case RC_REQUEST_CODE:
                  break;
                }
        }

        @Override
        void onDialogNegativeClicked(int requestCode) {
                switch(requestCode) {
                  case RC_REQUEST_CODE:
                  break;
                }          
        }
}

Definitely, the best approach is by using DialogFragment.当然,最好的方法是使用 DialogFragment。

Here is mine solution of wrapper class that helps to prevent different dialogs from being dismissed within one Fragment (or Activity with small refactoring).这是我的包装类解决方案,有助于防止在一个 Fragment(或具有小重构的 Activity)中关闭不同的对话框。 Also, it helps to avoid massive code refactoring if for some reasons there are a lot of AlertDialogs scattered among the code with slight differences between them in terms of actions, appearance or something else.此外,如果由于某些原因,代码中散布着大量AlertDialogs ,它们之间在操作、外观或其他方面略有不同,这有助于避免大规模的代码重构。

public class DialogWrapper extends DialogFragment {
    private static final String ARG_DIALOG_ID = "ARG_DIALOG_ID";

    private int mDialogId;

    /**
     * Display dialog fragment.
     * @param invoker  The fragment which will serve as {@link AlertDialog} alert dialog provider
     * @param dialogId The ID of dialog that should be shown
     */
    public static <T extends Fragment & DialogProvider> void show(T invoker, int dialogId) {
        Bundle args = new Bundle();
        args.putInt(ARG_DIALOG_ID, dialogId);
        DialogWrapper dialogWrapper = new DialogWrapper();
        dialogWrapper.setArguments(args);
        dialogWrapper.setTargetFragment(invoker, 0);
        dialogWrapper.show(invoker.getActivity().getSupportFragmentManager(), null);
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mDialogId = getArguments().getInt(ARG_DIALOG_ID);
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        return getDialogProvider().getDialog(mDialogId);
    }

    private DialogProvider getDialogProvider() {
        return (DialogProvider) getTargetFragment();
    }

    public interface DialogProvider {
        Dialog getDialog(int dialogId);
    }
}

When it comes to Activity you can invoke getContext() inside onCreateDialog() , cast it to the DialogProvider interface and request a specific dialog by mDialogId .当涉及到 Activity 时,您可以在onCreateDialog()调用getContext() ,将其转换为DialogProvider接口并通过mDialogId请求特定对话框。 All logic to dealing with a target fragment should be deleted.应删除处理目标片段的所有逻辑。

Usage from fragment:片段的用法:

public class MainFragment extends Fragment implements DialogWrapper.DialogProvider {
    private static final int ID_CONFIRMATION_DIALOG = 0;

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        Button btnHello = (Button) view.findViewById(R.id.btnConfirm);
        btnHello.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                DialogWrapper.show(MainFragment.this, ID_CONFIRMATION_DIALOG);
            }
        });
    }

    @Override
    public Dialog getDialog(int dialogId) {
        switch (dialogId) {
            case ID_CONFIRMATION_DIALOG:
                return createConfirmationDialog(); //Your AlertDialog
            default:
                throw new IllegalArgumentException("Unknown dialog id: " + dialogId);
        }
    }
}

You can read the complete article on my blog How to prevent Dialog being dismissed?您可以在我的博客如何防止 Dialog 被解雇? and play with the source code .并使用源代码

It seems that this is still an issue, even when "doing everything right" and using DialogFragment etc.似乎这仍然是一个问题,即使在“做正确的一切”并使用DialogFragment等时也是DialogFragment

There is a thread on Google Issue Tracker which claims that it is due to an old dismiss message being left in the message queue.谷歌问题跟踪器上有一个线程声称这是由于消息队列中留下了旧的关闭消息。 The provided workaround is quite simple:提供的解决方法非常简单:

    @Override
    public void onDestroyView() {
        /* Bugfix: https://issuetracker.google.com/issues/36929400 */
        if (getDialog() != null && getRetainInstance())
            getDialog().setDismissMessage(null);

        super.onDestroyView();
    }

Incredible that this is still needed 7 years after that issue was first reported.令人难以置信的是,在首次报告该问题 7 年后仍然需要这样做。

A very easy approach is to create the dialogs from the method onCreateDialog() (see note below).一个非常简单的方法是从方法onCreateDialog()创建对话框(见下面的注释)。 You show them through showDialog() .您可以通过showDialog()显示它们。 This way, Android handles the rotation for you and you do not have to call dismiss() in onPause() to avoid a WindowLeak and then you neither have to restore the dialog.这样,机器人会为你转动,你不必调用dismiss()onPause()以避免WindowLeak,然后你既没有恢复对话。 From the docs:从文档:

Show a dialog managed by this activity.显示由该活动管理的对话框。 A call to onCreateDialog(int, Bundle) will be made with the same id the first time this is called for a given id.第一次调用给定 id 时,将使用相同的 id 调用 onCreateDialog(int, Bundle)。 From thereafter, the dialog will be automatically saved and restored.此后,对话框将自动保存和恢复。

See Android docs showDialog() for more info.有关更多信息,请参阅Android 文档 showDialog() Hope it helps somebody!希望它可以帮助某人!

Note: If using AlertDialog.Builder, do not call show() from onCreateDialog() , call create() instead.注意:如果使用 AlertDialog.Builder,不要从onCreateDialog()调用show() onCreateDialog() ,而是调用create() If using ProgressDialog, just create the object, set the parameters you need and return it.如果使用ProgressDialog,只需创建对象,设置您需要的参数并返回它。 In conclusion, show() inside onCreateDialog() causes problems, just create de Dialog instance and return it.总之, onCreateDialog() show()会导致问题,只需创建 de Dialog 实例并将其返回即可。 This should work!这应该有效! (I have experienced issues using showDialog() from onCreate() -actually not showing the dialog-, but if you use it in onResume() or in a listener callback it works well). (我在使用 onCreate() 中的 showDialog() 时遇到了问题-实际上没有显示对话框-,但是如果您在 onResume() 或侦听器回调中使用它,则效果很好)。

You can combine the Dialog's onSave/onRestore methods with the Activity's onSave/onRestore methods to keep the state of the Dialog.您可以将Dialog 的 onSave/onRestore方法与Activity 的 onSave/onRestore方法结合使用,以保持 Dialog 的状态。

Note: This method works for those "simple" Dialogs, such as displaying an alert message.注意:此方法适用于那些“简单”对话框,例如显示警报消息。 It won't reproduce the contents of a WebView embedded in a Dialog.它不会重现嵌入在对话框中的 WebView 的内容。 If you really want to prevent a complex dialog from dismissal during rotation, try Chung IW's method.如果您真的想防止在轮换期间关闭复杂的对话框,请尝试 Chung IW 的方法。

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
     super.onRestoreInstanceState(savedInstanceState);
     myDialog.onRestoreInstanceState(savedInstanceState.getBundle("DIALOG"));
     // Put your codes to retrieve the EditText contents and 
     // assign them to the EditText here.
}

@Override
protected void onSaveInstanceState(Bundle outState) {
     super.onSaveInstanceState(outState);
     // Put your codes to save the EditText contents and put them 
     // to the outState Bundle here.
     outState.putBundle("DIALOG", myDialog.onSaveInstanceState());
}

I had a similar problem: when the screen orientation changed, the dialog's onDismiss listener was called even though the user didn't dismiss the dialog.我有一个类似的问题:当屏幕方向改变时,即使用户没有关闭对话框,对话框的onDismiss侦听器也会被调用。 I was able to work around this by instead using the onCancel listener, which triggered both when the user pressed the back button and when the user touched outside of the dialog.我可以通过使用onCancel侦听器来解决此问题,该侦听器在用户按下后退按钮和用户触摸对话框外时都会触发。

In case nothing helps, and you need a solution that works, you can go on the safe side, and each time you open a dialog save its basic info to the activity ViewModel (and remove it from this list when you dismiss dialog).如果没有任何帮助,并且您需要一个有效的解决方案,您可以安全起见,每次打开对话框时将其基本信息保存到活动 ViewModel(并在您关闭对话框时将其从该列表中删除)。 This basic info could be dialog type and some id (the information you need in order to open this dialog).此基本信息可以是对话框类型和一些 id(打开此对话框所需的信息)。 This ViewModel is not destroyed during changes of Activity lifecycle.这个 ViewModel 在 Activity 生命周期的变化期间不会被销毁。 Let's say user opens a dialog to leave a reference to a restaurant.假设用户打开一个对话框以留下对餐厅的引用。 So dialog type would be LeaveReferenceDialog and the id would be the restaurant id.所以对话框类型是 LeaveReferenceDialog,id 是餐厅 id。 When opening this dialog, you save this information in an Object that you can call DialogInfo, and add this object to the ViewModel of the Activity.打开此对话框时,您将此信息保存在一个可以调用 DialogInfo 的对象中,并将此对象添加到 Activity 的 ViewModel。 This information will allow you to reopen the dialog when the activity onResume() is being called:此信息将允许您在调用活动 onResume() 时重新打开对话框:

// On resume in Activity
    override fun onResume() {
            super.onResume()
    
            // Restore dialogs that were open before activity went to background
            restoreDialogs()
        }

Which calls:其中调用:

    fun restoreDialogs() {
    mainActivityViewModel.setIsRestoringDialogs(true) // lock list in view model

    for (dialogInfo in mainActivityViewModel.openDialogs)
        openDialog(dialogInfo)

    mainActivityViewModel.setIsRestoringDialogs(false) // open lock
}

When IsRestoringDialogs in ViewModel is set to true, dialog info will not be added to the list in view model, and it's important because we're now restoring dialogs which are already in that list.当 ViewModel 中的 IsRestoringDialogs 设置为 true 时,对话框信息将不会添加到视图模型中的列表中,这很重要,因为我们现在正在恢复已经在该列表中的对话框。 Otherwise, changing the list while using it would cause an exception.否则,在使用列表时更改列表会导致异常。 So:所以:

// Create new dialog
        override fun openLeaveReferenceDialog(restaurantId: String) {
            var dialog = LeaveReferenceDialog()
            // Add id to dialog in bundle
            val bundle = Bundle()
            bundle.putString(Constants.RESTAURANT_ID, restaurantId)
            dialog.arguments = bundle
            dialog.show(supportFragmentManager, "")
        
            // Add dialog info to list of open dialogs
            addOpenDialogInfo(DialogInfo(LEAVE_REFERENCE_DIALOG, restaurantId))
    }

Then remove dialog info when dismissing it:然后在关闭它时删除对话框信息:

// Dismiss dialog
override fun dismissLeaveReferenceDialog(Dialog dialog, id: String) {
   if (dialog?.isAdded()){
      dialog.dismiss()
      mainActivityViewModel.removeOpenDialog(LEAVE_REFERENCE_DIALOG, id)
   }
}

And in the ViewModel of the Activity:在 Activity 的 ViewModel 中:

fun addOpenDialogInfo(dialogInfo: DialogInfo){
    if (!isRestoringDialogs){
       val dialogWasInList = removeOpenDialog(dialogInfo.type, dialogInfo.id)
       openDialogs.add(dialogInfo)
     }
}


fun removeOpenDialog(type: Int, id: String) {
    if (!isRestoringDialogs)
       for (dialogInfo in openDialogs) 
         if (dialogInfo.type == type && dialogInfo.id == id) 
            openDialogs.remove(dialogInfo)
}

You actually reopen all the dialogs that were open before, in the same order.您实际上以相同的顺序重新打开了之前打开的所有对话框。 But how do they retain their information?但是他们如何保留他们的信息呢? Each dialog has a ViewModel of its own, which is also not destroyed during the activity lifecycle.每个对话框都有自己的 ViewModel,它在活动生命周期中也不会被销毁。 So when you open the dialog, you get the ViewModel and init the UI using this ViewModel of the dialog as always.因此,当您打开对话框时,您将获得 ViewModel 并像往常一样使用对话框的此 ViewModel 初始化 UI。

Yes, I agree with the solution of using DialogFragment given by @Brais Gabin, just want to suggest some changes to the solution given by him.是的,我同意@Brais Gabin 给出的使用 DialogFragment 的解决方案,只是想对他给出的解决方案提出一些更改建议。

While defining our custom class that extends DialogFragment, we require some interfaces to manage the actions ultimately by the activity or the fragment that has invoked the dialog.在定义扩展 DialogFragment 的自定义类时,我们需要一些接口来最终通过 Activity 或调用对话框的片段来管理操作。 But setting these listener interfaces in the onAttach(Context context) method may sometimes cause ClassCastException that may crash the app.但是在 onAttach(Context context) 方法中设置这些侦听器接口有时可能会导致 ClassCastException 可能会导致应用程序崩溃。

So to avoid this exception, we can create a method to set the listener interfaces and call just it after creating the object of the dialog fragment.所以为了避免这个异常,我们可以创建一个方法来设置监听器接口,并在创建对话框片段的对象后调用它。 Here is a sample code that could help you understand more-这是一个示例代码,可以帮助您了解更多-

AlertRetryDialog.class AlertRetryDialog.class

    public class AlertRetryDialog extends DialogFragment {

       public interface Listener{
         void onRetry();
         }

    Listener listener;

     public void setListener(Listener listener)
       {
       this.listener=listener;
       }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
    AlertDialog.Builder builder=new AlertDialog.Builder(getActivity());
    builder.setMessage("Please Check Your Network Connection").setPositiveButton("Retry", new 
    DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
             //Screen rotation will cause the listener to be null
            //Always do a null check of your interface listener before calling its method
            if(listener!=null&&listener instanceof HomeFragment)
            listener.onRetry();
        }
       }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            dialog.dismiss();
         }
     });
     return builder.create();
    }

   }

And in the Activity or in the Fragment you call-在您调用的 Activity 或 Fragment 中-

                   AlertRetryDialog alertRetryDialog = new AlertRetryDialog();
                    alertRetryDialog.setListener(HomeFragment.this);
                    alertRetryDialog.show(getFragmentManager(), "tag");

And implement the methods of your listener interface in your Activity or the Fragment-并在您的 Activity 或 Fragment 中实现您的侦听器接口的方法-

              public class YourActivity or YourFragment implements AlertRetryDialog.Listener{ 
                
                  //here's my listener interface's method
                    @Override
                    public void onRetry()
                    {
                     //your code for action
                      }
                
                 }

Always make sure that you do a null check of the listener interfaces before calling any of its methods to prevent NullPointerException (Screen rotation will cause the listener interfaces to be null).始终确保在调用任何方法之前对侦听器接口进行空检查以防止 NullPointerException(屏幕旋转将导致侦听器接口为空)。

Please do let me know if you find this answer helpful.如果您觉得这个答案有帮助,请告诉我。 Thank You.谢谢。

Just use只需使用

ConfigurationChanges = Android.Content.PM.ConfigChanges.Orientation | Android.Content.PM.ConfigChanges.ScreenSize

and app will know how to handle rotation and screen size.和应用程序将知道如何处理旋转和屏幕大小。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM