在安卓系统中防止屏幕旋转导致对话框消失

102

我正在尝试防止使用Alert builder构建的对话框在Activity重新启动时被取消。

如果我重载onConfigurationChanged方法,我可以成功地做到这一点,并将布局重置为正确的方向,但我会失去edittext的粘性文本功能。因此,在解决对话框问题时,我创建了这个edittext问题。

如果我保存edittext中的字符串,并在onCofiguration更改中重新分配它们,它们仍然似乎默认为旋转前输入的初始值。即使我强制无效也不会更新它们。

我真的需要解决对话框问题或edittext问题。

感谢您的帮助。


1
你如何保存/恢复编辑的EditText的内容?你能展示一些代码吗? - Peter Knego
我找到了问题所在,我在重置布局后忘记通过ID再次获取视图了。 - draksia
13个回答

143

现在避免这个问题的最佳方法是使用一个DialogFragment

创建一个继承DialogFragment的新类。重写onCreateDialog方法并返回旧的Dialog或者一个AlertDialog

之后,您可以使用DialogFragment.show(fragmentManager, tag)显示它。

以下是一个带有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();
    }
}

在活动中调用:

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

这个答案可以帮助解释以下三个问题(以及它们的答案):


我的应用程序中有一个按钮可以调用.show(),我必须记住警报对话框的状态,即显示/解除显示。是否有一种方法可以在不调用.show()的情况下保持对话框? - Alpha Huang
1
它说现在onAttach已被弃用。应该做什么替代? - farahm
3
@faraz_ahmed_kamran,你应该使用onAttach(Context context)android.support.v4.app.DialogFragment。现在onAttach方法的参数是context而不是activity - Stan Mots
1
不过,可能没有必要使用 YesNoListener。请参见此答案 - Mygod

49
// 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);
    }

6
能运作,虽不知其原理。简洁明了的抽象解决方案,感谢。 - nmvictor
1
很酷的东西 @ChungIW,这就是我所崇拜的编码。 - Zuko
3
我认为这段代码不好。doLogout()函数引用了包含活动的上下文。由于活动无法销毁,可能会导致内存泄漏。我曾尝试从静态上下文中使用AlertDialog,但现在我确定这是不可能的。我认为结果只能是垃圾。 - The incredible Jan
2
它看起来像是在工作,但对于新创建的活动或片段没有任何连接(每次方向更改时都会重新创建)。因此,在对话框按钮的“OnClickListener”中无法执行需要“上下文”的任何操作。 - Udo Klimaschewski
2
这段代码可以工作,但绝不推荐使用。它泄漏了Activity的引用,这就是为什么对话框会持久存在的原因。这是一种非常糟糕的做法,会导致内存泄漏。 - Hexise
显示剩余8条评论

5
如果您在屏幕方向改变时更改布局,我不建议在清单文件中添加android:configChanges="orientation",因为您将重新创建视图。
使用以下方法保存活动的当前状态(如输入文本、显示对话框、显示数据等)。
@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
}

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

这样,活动将再次经历onCreate方法,然后调用onRestoreInstanceState方法,在这个方法中,您可以重新设置EditText的值。

如果您想存储更复杂的对象,可以使用

@Override
public Object onRetainNonConfigurationInstance() {
}

在这里,您可以存储任何对象,并且在onCreate中只需调用getLastNonConfigurationInstance();即可获取该对象。


4
OnRetainNonConfigurationInstance() 现在已经被弃用,根据文档提示:http://developer.android.com/reference/android/app/Activity.html#onRetainNonConfigurationInstance(),应该使用 setRetainInstance(boolean retain) 来代替。参考链接:http://developer.android.com/reference/android/app/Fragment.html#setRetainInstance(boolean) - ForceMagic
@ForceMagic setRetainInstance是完全不同的:它是用于Fragments的,而且它并不能保证实例会被保留。 - Miha_x64

4
只需在AndroidManifest.xml文件中的activity元素中添加android:configChanges="orientation"即可。
示例:
<activity
            android:name=".YourActivity"
            android:configChanges="orientation"
            android:label="@string/app_name"></activity>

这可能会导致在某些情况下对话框显示不正确。 - Sam
2
android:configChanges="orientation|screenSize"注意:如果您的应用程序目标为Android 3.2(API级别13)或更高版本,则还应声明“screenSize”配置,因为当设备在纵向和横向方向之间切换时,它也会发生变化。 - tsig

1

最好的方法是使用DialogFragment。

以下是我的包装类解决方案,可帮助防止在一个Fragment(或通过小的重构在Activity中)中关闭不同的对话框。此外,如果由于某些原因代码中散布着大量具有轻微差异的AlertDialog(例如在操作、外观或其他方面),它还可以避免大规模的代码重构。

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);
    }
}

在处理Activity时,您可以在onCreateDialog()中调用getContext(),将其转换为DialogProvider接口,并通过mDialogId请求特定对话框。应删除所有与目标片段相关的逻辑。

来自片段的用法:

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);
        }
    }
}

你可以在我的博客上阅读完整文章如何防止对话框被关闭?并与源代码一起玩耍。

1

即使“一切正确”并使用DialogFragment等工具,似乎这仍然是一个问题。

Google Issue Tracker上有一个线程声称,这是由于旧的关闭消息留在消息队列中造成的。提供的解决方法非常简单:

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

        super.onDestroyView();
    }

令人难以置信的是,在该问题首次报告7年后,仍然需要它。


请参见 https://dev59.com/Q2Uq5IYBdhLWcg3wPuJQ。 - CoolMind

1

这个问题早就有了答案。

但是我使用的解决方案是非hacky简单的,你也可以在你的应用程序中使用它。

我为自己创建了这个帮助类,所以你也可以使用它。

用法如下:

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

或者

 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;
                }          
        }
}

1
一个非常简单的方法是使用onCreateDialog()方法创建对话框(参见下面的注释)。您通过showDialog()显示它们。这样,Android会为您处理旋转,您不必在onPause()中调用dismiss()以避免WindowLeak,也无需恢复对话框。从文档中可以看到:

显示此活动管理的对话框。第一次针对给定id调用此方法时,将使用相同的id调用onCreateDialog(int, Bundle)。从那时起,对话框将自动保存和恢复。

有关更多信息,请参见Android docs showDialog()。希望能帮助到某些人!

注意:如果使用AlertDialog.Builder,请不要在onCreateDialog()中调用show(),而应该调用create()。如果使用ProgressDialog,只需创建对象,设置所需的参数并返回即可。总之,在onCreateDialog()中调用show()会导致问题,只需创建对话框实例并返回即可。这样应该可以解决问题!(我曾经在onCreate()中使用showDialog()时遇到过问题-实际上没有显示对话框-但如果您在onResume()或侦听器回调中使用它,则可以正常工作)。


你需要哪种情况下的代码?在onCreateDialog()中还是用builder显示并调用show()方法? - Caumons
我已经成功做到了,但问题是,onCreateDialog()现在已经被弃用了 :-\ - neteinstein
好的!请注意,大多数Android设备仍然使用2.X版本,因此您可以随意使用它!请查看Android平台版本使用情况 - Caumons
除了onCreateDialog,还有其他选项吗? - neteinstein
1
你可以使用构建器类,例如AlertDialog.Builder。如果你在onCreateDialog() 中使用它,而不是使用show(),则返回create()的结果。否则,调用show()并将返回的AlertDialog存储到Activity的属性中,在onPause()中如果正在显示,则dismiss()它,以避免WindowLeak。希望能帮到你! - Caumons

0

您可以将对话框的onSave/onRestore方法与活动的onSave/onRestore方法结合使用,以保持对话框的状态。

注意:此方法适用于那些“简单”的对话框,例如显示警报消息。它不会重现嵌入在对话框中的WebView的内容。如果您真的想在旋转期间防止复杂对话框被解除,请尝试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());
}

0
我遇到了类似的问题:当屏幕方向改变时,即使用户没有关闭对话框,对话框的onDismiss监听器也会被调用。我通过使用onCancel监听器来解决这个问题,它在用户按下返回按钮或触摸对话框外部时都会触发。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接