【问题标题】:Prevent dialog dismissal on screen rotation in Android防止在Android中的屏幕旋转对话框解除
【发布时间】:2011-11-25 07:34:19
【问题描述】:

我试图防止在 Activity 重新启动时关闭使用警报生成器构建的对话框。

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

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

我真的需要解决对话框问题或编辑文本问题。

感谢您的帮助。

【问题讨论】:

  • 如何保存/恢复编辑过的EditText的内容?你能显示一些代码吗?
  • 我发现了问题所在,我在重置布局后忘记通过 Id 再次获取视图。

标签: android dialog android-edittext onconfigurationchanged


【解决方案1】:

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

创建一个扩展 DialogFragment 的新类。覆盖 onCreateDialog 并返回旧的 DialogAlertDialog

然后你可以用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();
    }
}

在你调用的 Activity 中:

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

此答案有助于解释其他三个问题(及其答案):

【讨论】:

  • 我的应用程序中有一个按钮可以调用.show(),我必须记住警报对话框的状态,显示/关闭。有没有办法在不调用 .show() 的情况下保留对话框?
  • 它说onAttach 现在已被弃用。应该怎么做?
  • @faraz_ahmed_kamran,您应该使用onAttach(Context context)android.support.v4.app.DialogFragmentonAttach 方法现在采用 context 而不是 activity 作为参数。
  • 可能不需要YesNoListener。见this answer
【解决方案2】:
// 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);
    }

【讨论】:

  • 谁觉得我的代码没用,点击之前先试试吧 :-)
  • 有效,不知道如何但有效!干净简单抽象的解决方案,谢谢。
  • 我认为这段代码很糟糕。 doLogout() 具有对上下文的引用,该上下文是/包含活动。活动不能被破坏,这可能导致内存泄漏。我一直在寻找从静态上下文中使用 AlertDialog 的可能性,但现在我确定这是不可能的。结果只能是我认为的垃圾。
  • 它看起来就像它工作。对话框保持打开状态,但它与新创建的活动或片段没有任何联系(它在每次方向更改时都新创建)。所以你不能做任何需要在对话框按钮OnClickListener 内使用Context 的事情。
  • 此代码有效,但根本不推荐。它泄漏了活动引用,这就是对话框可以持续存在的原因。这是一种非常糟糕的做法,会导致内存泄漏。
【解决方案3】:

如果您在方向更改时更改布局,我不会将 android:configChanges="orientation" 放入清单中,因为无论如何您都在重新创建视图。

使用以下方法保存活动的当前状态(如输入的文本、显示的对话框、显示的数据等):

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

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

这样,Activity 再次通过 onCreate,然后调用 onRestoreInstanceState 方法,您可以在其中再次设置 EditText 值。

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

@Override
public Object onRetainNonConfigurationInstance() {
}

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

【讨论】:

【解决方案4】:

只需在您的活动中添加 android:configChanges="orientation" AndroidManifest.xml 中的元素

例子:

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

【讨论】:

  • 这会导致对话框在某些情况下显示不正确。
  • android:configChanges="orientation|screenSize" 注意:如果您的应用程序面向 Android 3.2(API 级别 13)或更高版本,那么您还应该声明“screenSize”配置,因为它也会在设备在纵向和横向之间切换。
【解决方案5】:

一个非常简单的方法是从方法onCreateDialog() 创建对话框(见下面的注释)。您通过showDialog() 向他们展示。这样,Android 会为您处理旋转,您不必在 onPause() 中调用 dismiss() 来避免 WindowLeak,然后您也不必恢复对话框。来自文档:

显示由该活动管理的对话框。第一次为给定 id 调用 onCreateDialog(int, Bundle) 时,将使用相同的 id 进行调用。此后,对话框将自动保存和恢复。

请参阅Android docs showDialog() 了解更多信息。希望它可以帮助别人!

注意:如果使用 AlertDialog.Builder,不要从onCreateDialog() 调用show(),而是调用create()。如果使用 ProgressDialog,只需创建对象,设置您需要的参数并返回它。总之,show() 内的onCreateDialog() 会导致问题,只需创建 de Dialog 实例并返回它。这应该工作! (我在使用 onCreate() 中的 showDialog() 时遇到过问题——实际上没有显示对话框——但如果你在 onResume() 或侦听器回调中使用它,效果很好)。

【讨论】:

  • 在哪种情况下您需要一些代码? onCreateDialog() 还是使用构建器显示它并调用 show() ?
  • 我已经设法做到了.. 但问题是,onCreateDialog() 现在已被弃用:-\
  • 好的!请记住,大多数 Android 设备仍然可以使用 2.X 版本,所以无论如何您都可以使用它!看看Android platform versions usage
  • 不过,如果不是 onCreateDialog,还有什么选择?
  • 您可以使用构建器类,例如AlertDialog.Builder。如果在onCreateDialog() 中使用它,而不是使用show(),则返回create() 的结果。否则,调用 show() 并将返回的 AlertDialog 存储到 Activity 的属性中,如果显示则在 onPause() dismiss() 中,以避免 WindowLeak。希望对您有所帮助!
【解决方案6】:

这个问题很久以前就回答过了。

然而,这是我自己使用的非hacky简单解决方案。

我为自己做了this helper class,所以你也可以在你的应用程序中使用它。

用法是:

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

【讨论】:

    【解决方案7】:

    当然,最好的方法是使用 DialogFragment。

    这是我的包装类解决方案,它有助于防止不同的对话框在一个 Fragment(或具有小重构的 Activity)中被解除。此外,如果由于某些原因有很多 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);
        }
    }
    

    当涉及到活动时,您可以在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);
            }
        }
    }
    

    您可以在我的博客How to prevent Dialog being dismissed? 上阅读完整的文章并与source code 一起玩。

    【讨论】:

      【解决方案8】:

      似乎这仍然是一个问题,即使“做正确的事”并使用 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 年后仍然需要这样做。

      【讨论】:

      【解决方案9】:

      您可以将 Dialog 的 onSave/onRestore 方法与 Activity 的 onSave/onRestore 方法结合使用,以保持 Dialog 的状态。

      注意:此方法适用于那些“简单”的对话框,例如显示警报消息。它不会重现嵌入在 Dialog 中的 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());
      }
      

      【讨论】:

        【解决方案10】:

        我遇到了类似的问题:当屏幕方向改变时,即使用户没有关闭对话框,也会调用对话框的 onDismiss 侦听器。我可以通过使用 onCancel 侦听器来解决此问题,当用户按下后退按钮和用户在对话框外触摸时都会触发该侦听器。

        【讨论】:

          【解决方案11】:

          如果没有任何帮助,并且您需要一个可行的解决方案,您可以采取安全措施,每次打开对话框时都将其基本信息保存到活动 ViewModel(并在您关闭对话框时将其从该列表中删除)。此基本信息可能是对话框类型和一些 id(打开此对话框所需的信息)。此 ViewModel 在 Activity 生命周期更改期间不会被销毁。假设用户打开一个对话框以留下对餐厅的引用。所以对话类型将是 LeaveReferenceDialog 并且 id 将是餐厅 id。打开此对话框时,将此信息保存在可以调用 DialogInfo 的 Object 中,并将此对象添加到 Activity 的 ViewModel 中。此信息将允许您在调用活动 onResume() 时重新打开对话框:

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

          调用者:

              fun restoreDialogs() {
              mainActivityViewModel.setIsRestoringDialogs(true) // lock list in view model
          
              for (dialogInfo in mainActivityViewModel.openDialogs)
                  openDialog(dialogInfo)
          
              mainActivityViewModel.setIsRestoringDialogs(false) // open lock
          }
          

          当 ViewModel 中的 IsRestoringDialogs 设置为 true 时,对话框信息将不会添加到视图模型中的列表中,这很重要,因为我们现在正在恢复该列表中已经存在的对话框。否则,在使用时更改列表会导致异常。所以:

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

          然后在关闭对话框时删除它:

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

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

          实际上,您以相同的顺序重新打开了之前打开的所有对话框。但是他们如何保留他们的信息呢?每个对话框都有自己的 ViewModel,它在 Activity 生命周期中也不会被销毁。所以当你打开对话框时,你会得到 ViewModel 并像往常一样使用对话框的这个 ViewModel 来初始化 UI。

          【讨论】:

            【解决方案12】:

            是的,我同意@Brais Gabin 给出的使用DialogFragment 的解决方案,只是想建议对他给出的解决方案进行一些更改。

            在定义扩展 DialogFragment 的自定义类时,我们需要一些接口来最终通过调用对话框的活动或片段来管理操作。但是在 onAttach(Context context) 方法中设置这些监听器接口有时可能会导致 ClassCastException 导致应用崩溃。

            所以为了避免这个异常,我们可以创建一个方法来设置监听接口,并在创建对话框片段的对象后调用它。 这是一个示例代码,可以帮助您了解更多-

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

            在你调用的Activity或者Fragment中——

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

            并在你的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
                                  }
                            
                             }
            

            始终确保在调用其任何方法之前对侦听器接口进行空检查,以防止出现 NullPointerException(屏幕旋转将导致侦听器接口为空)。

            如果您觉得此答案有帮助,请告诉我。谢谢。

            【讨论】:

              【解决方案13】:

              随便用

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

              应用会知道如何处理旋转和屏幕尺寸。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2011-02-13
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2013-01-08
                相关资源
                最近更新 更多