【问题标题】:Android: How to get a modal dialog or similar modal behavior?Android:如何获得模态对话框或类似的模态行为?
【发布时间】:2011-09-01 12:15:00
【问题描述】:

这些天我正在研究在 Android 中模拟模态对话框。我用谷歌搜索了很多,有很多讨论,但遗憾的是没有太多选择可以让它成为模态。这里有一些背景,
Dialogs, Modal Dialogs and Blockin
Dialogs / AlertDialogs: How to "block execution" while dialog is up (.NET-style)

没有直接的方法来获得模态行为,然后我想出了 3 种可能的解决方案,
1.使用以对话为主题的活动,就像thread说的那样,但我仍然不能让主要活动真正等待对话活动返回。主要活动转为停止状态,然后重新启动。
2.构建一个工作线程,并使用线程同步。但是,对于我的应用来说,这是一项巨大的重构工作,现在我在主 UI 线程中都有一个主 Activity 和一个服务。
3. 当有模态对话框时在循环内接管事件处理,当对话框关闭时退出循环。实际上,这是构建真正的模态对话框的方式,就像它在 Windows 中所做的那样。我还没有用这种方式制作原型。

我仍然想用对话主题的活动来模拟它,
1. 通过 startActivityForResult()
启动对话活动 2.从onActivityResult()获取结果
这是一些来源

public class MainActivity extends Activity {

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

    MyView v = new MyView(this);
    setContentView(v);
}

private final int RESULT_CODE_ALERT = 1;
private boolean mAlertResult = false;
public boolean startAlertDialog() {
    Intent it = new Intent(this, DialogActivity.class);
    it.putExtra("AlertInfo", "This is an alert");
    startActivityForResult(it, RESULT_CODE_ALERT);

    // I want to wait right here
    return mAlertResult;
}

@Override
protected void onActivityResult (int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
    case RESULT_CODE_ALERT:
        Bundle ret = data.getExtras();
        mAlertResult = ret.getBoolean("AlertResult");
        break;
    }
}
}

startAlertDialog 的调用者将阻塞执行并期望返回结果。但是 startAlertDialog 当然会立即返回,并且主要活动在 DialogActivity 启动时进入 STOP 状态。

所以问题是,如何让主要活动真正等待结果?
谢谢。

【问题讨论】:

  • 这篇文章可能对你有所帮助。 stackoverflow.com/questions/2028697/…
  • 我真的不敢相信 Android 会在这个非常简单的东西上糟透了。
  • 您问题中的第一个链接说明 Android 确实 有模型对话框(但没有阻止)。如果您更改术语以反映您真正要求的内容 - 线程阻塞,您的问题会更加清晰。
  • 使用广播接收器调用链中的下一个方法...死锁代码直到方法被调用。
  • Android 不支持模式对话框让 Google 感到羞耻。我相信他们应该解决这个问题。作为程序员,我们应该说服 Google 做正确的事情,而不是重新发明一种类似对话框的模式。模式对话框已经存在并由 Microsoft 在 Windows 中实现。为什么不由 Google 实施?因为 Google 工程师很懒 ;-)

标签: android android-activity synchronization modal-dialog


【解决方案1】:

我在使用时得到了一个模态对话框:

setCancelable(false);

在 DialogFragment 上(不在 DialogBu​​ilder 上)。

【讨论】:

  • 像魅力一样工作。示例:final AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(false);
  • 这不起作用。模态对话框的含义是,UI 线程停止工作,直到对话框被关闭,而这段代码并非如此
  • 如前所述,如果您的对话框是单独的片段 public class MyCustomDialog extends DialogFragment{ .... public Dialog onCreateDialog(Bundle savedInstanceState) { setCancelable(false); ...},则应在对话框片段上完成
  • 扩展android基础Dialog类也是有效的
  • 这不会停止代码的执行,它可以防止对话框在后按时被取消......代码可以通过在 dialogFragment.show(fragmentTransaction, TAG);并使用 onClickListener 发送广播意图,调用对话框关闭后所需的方法。
【解决方案2】:

这不可能按照您的计划进行。首先,不允许阻塞 UI 线程。您的申请将被终止。其次,需要处理使用startActivity 启动另一个活动时调用的生命周期方法(当另一个活动正在运行时,您的原始活动将被暂停)。第三,您可能可以通过使用startAlertDialog() 而不是从 UI 线程、线程同步(如Object.wait())和一些AlertDialog 以某种方式破解它。但是,我强烈建议您不要这样做。它很丑陋,肯定会坏掉,而且这不是事情的预期工作方式。

重新设计您的方法以捕捉这些事件的异步特性。例如,如果您想要一些询问用户决定的对话框(例如是否接受 ToS)并根据该决定执行特殊操作,请创建如下对话框:

AlertDialog dialog = new AlertDialog.Builder(context).setMessage(R.string.someText)
                .setPositiveButton(android.R.string.ok, new OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                        // Do stuff if user accepts
                    }
                }).setNegativeButton(android.R.string.cancel, new OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                        // Do stuff when user neglects.
                    }
                }).setOnCancelListener(new OnCancelListener() {

                    @Override
                    public void onCancel(DialogInterface dialog) {
                        dialog.dismiss();
                        // Do stuff when cancelled
                    }
                }).create();
dialog.show();

然后有两种方法相应地处理正反馈或负反馈(即进行某些操作或完成活动或任何有意义的事情)。

【讨论】:

  • 谢谢。我完全同意你上面介绍的一切。不幸的是,这对我来说是必须的要求,而实际情况要复杂得多,说来话长。模态行为基本上与 Android 的设计相冲突,我们都知道,但是......无论如何,我正在尝试寻找一些优雅的方法来解决它:( 解决方案 3 是一种选择吗?
  • 在这样的对话框中是否可以有丰富的控件(如按钮、文本视图和编辑视图)?它可以等到用户提供某些内容作为输入,然后重新关注正在运行的活动。
  • 我不同意这种措辞“这不可能按照您的计划进行。首先,您不允许阻塞 UI 线程。” -- 不需要阻止 UI 线程来显示模态对话框 -- 如果它们也阻止 UI 线程,Windows 将终止您的应用程序,但它不会阻止它们具有模态对话框 -- 这些应用程序继续处理消息和在强制用户在返回父窗口之前处理模态对话框时做出响应(不,父窗口也没有阻塞——它们正常处理所有事件)。
  • @BrainSlugs83:“如果它们也阻止 UI 线程,Windows 会杀死你的应用程序......” - 不会。
  • @BrainSlugs83:我认为问题不在于 Stephan 的措辞,而在于 OP。 Stephan 没有说模态对话框需要阻塞。这就是OP。
【解决方案3】:

Android 和 iOS 的开发人员认为他们足够强大和聪明,可以拒绝模态对话框的概念(这种概念已经在市场上销售了很多年,并且以前没有打扰过任何人),这对我们来说很不幸。

这是我的解决方案,效果很好:

    int pressedButtonID;
    private final Semaphore dialogSemaphore = new Semaphore(0, true);
    final Runnable mMyDialog = new Runnable()
    {
        public void run()
        {
            AlertDialog errorDialog = new AlertDialog.Builder( [your activity object here] ).create();
            errorDialog.setMessage("My dialog!");
            errorDialog.setButton("My Button1", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    pressedButtonID = MY_BUTTON_ID1;
                    dialogSemaphore.release();
                    }
                });
            errorDialog.setButton2("My Button2", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    pressedButtonID = MY_BUTTON_ID2;
                    dialogSemaphore.release();
                    }
                });
            errorDialog.setCancelable(false);
            errorDialog.show();
        }
    };

    public int ShowMyModalDialog()  //should be called from non-UI thread
    {
        pressedButtonID = MY_BUTTON_INVALID_ID;
        runOnUiThread(mMyDialog);
        try
        {
            dialogSemaphore.acquire();
        }
        catch (InterruptedException e)
        {
        }
        return pressedButtonID;
    }

【讨论】:

  • 谢谢,这对我有用。至于用例:我需要一个阻止输入的模式对话框,因为它用于由 C++ 触发的断言。该对话框必须阻止 UI 线程,因为该线程可能会触发更多断言。
【解决方案4】:

这对我有用:创建一个 Activity 作为您的对话框。那么,

  1. 将此添加到您的活动清单中:

    android:theme="@android:style/Theme.Dialog"

  2. 将此添加到您的活动的 onCreate

    setFinishOnTouchOutside (false);

  3. 在您的活动中覆盖 onBackPressed:

    @覆盖 公共无效 onBackPressed() { // 防止“返回”离开这个活动 }

第一个为活动提供对话框外观。后两者使其表现得像一个模态对话框。

【讨论】:

  • 谢谢。正是我想要的。
  • 这至少需要 API 级别 11
【解决方案5】:

最后我得到了一个非常直接和简单的解决方案。

熟悉 Win32 编程的人可能知道如何实现模态对话框。通常,当有一个模态对话框时,它会运行一个嵌套的消息循环(通过 GetMessage/PostMessage)。因此,我尝试以这种传统方式实现我自己的模态对话框。

一开始android没有提供注入ui线程消息循环的接口,或者我没有找到。当我查看源代码 Looper.loop() 时,我发现它正是我想要的。但是,MessageQueue/Message 仍然没有提供公共接口。幸运的是,我们在 java 中有反射。 基本上,我只是完全复制了 Looper.loop() 所做的,它阻止了工作流并且仍然正确处理了事件。我还没有测试过嵌套模式对话框,但理论上它会起作用。

这是我的源代码,

public class ModalDialog {

private boolean mChoice = false;        
private boolean mQuitModal = false;     

private Method mMsgQueueNextMethod = null;
private Field mMsgTargetFiled = null;

public ModalDialog() {
}

public void showAlertDialog(Context context, String info) {
    if (!prepareModal()) {
        return;
    }

    // build alert dialog
    AlertDialog.Builder builder = new AlertDialog.Builder(context);
    builder.setMessage(info);
    builder.setCancelable(false);
    builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            ModalDialog.this.mQuitModal = true;
            dialog.dismiss();
        }
    });

    AlertDialog alert = builder.create();
    alert.show();

    // run in modal mode
    doModal();
}

public boolean showConfirmDialog(Context context, String info) {
    if (!prepareModal()) {
        return false;
    }

    // reset choice
    mChoice = false;

    AlertDialog.Builder builder = new AlertDialog.Builder(context);
    builder.setMessage(info);
    builder.setCancelable(false);
    builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            ModalDialog.this.mQuitModal = true;
            ModalDialog.this.mChoice = true;
            dialog.dismiss();
        }
    });

    builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            ModalDialog.this.mQuitModal = true;
            ModalDialog.this.mChoice = false;
            dialog.cancel();
        }
    });

    AlertDialog alert = builder.create();
    alert.show();

    doModal();
    return mChoice;
}

private boolean prepareModal() {
    Class<?> clsMsgQueue = null;
    Class<?> clsMessage = null;

    try {
        clsMsgQueue = Class.forName("android.os.MessageQueue");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
        return false;
    }

    try {
        clsMessage = Class.forName("android.os.Message");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
        return false;
    }

    try {
        mMsgQueueNextMethod = clsMsgQueue.getDeclaredMethod("next", new Class[]{});
    } catch (SecurityException e) {
        e.printStackTrace();
        return false;
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
        return false;
    }

    mMsgQueueNextMethod.setAccessible(true);

    try {
        mMsgTargetFiled = clsMessage.getDeclaredField("target");
    } catch (SecurityException e) {
        e.printStackTrace();
        return false;
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
        return false;
    }

    mMsgTargetFiled.setAccessible(true);
    return true;
}

private void doModal() {
    mQuitModal = false;

    // get message queue associated with main UI thread
    MessageQueue queue = Looper.myQueue();
    while (!mQuitModal) {
        // call queue.next(), might block
        Message msg = null;
        try {
            msg = (Message)mMsgQueueNextMethod.invoke(queue, new Object[]{});
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        if (null != msg) {
            Handler target = null;
            try {
                target = (Handler)mMsgTargetFiled.get(msg);
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

            if (target == null) {
                // No target is a magic identifier for the quit message.
                mQuitModal = true;
            }

            target.dispatchMessage(msg);
            msg.recycle();
        }
    }
}
}

希望这会有所帮助。

【讨论】:

  • 不要这样做。此代码使用私有 API。它显然是使用私有 API。任何这样做的人都可以预期他们的应用程序会崩溃。
  • 这段代码很尴尬。头脑正常的人都不应该使用这些技术。
  • 这只是告诉你android框架一开始就设计得很糟糕。你怎么能没有一个简单的模态对话框机制???!
  • 使用另一个Activity作为对话框的主要问题是你无法区分失去整个应用程序的焦点和失去焦点,因为你的应用程序中的另一个活动在onPause()之上。在这种情况下,使用类似模式的对话框可能是有意义的。特别是如果客户要求发生了变化,但您不是“企业”,并且没有 3 个月的重构预算......
  • 尽管这是一个糟糕的编程,但我只是为了概念验证而投票。
【解决方案6】:

正如 hackbod 和其他人所指出的,Android 故意不提供执行嵌套事件循环的方法。我理解这样做的原因,但在某些情况下需要它们。在我们的案例中,我们有自己的虚拟机在各种平台上运行,我们希望将其移植到 Android。在内部有很多地方需要嵌套的事件循环,并且仅仅为 Android 重写整个事情是不可行的。无论如何,这里有一个解决方案(基本上取自How can I do non-blocking events processing on Android?,但我添加了超时):

private class IdleHandler implements MessageQueue.IdleHandler
{
    private Looper _looper;
    private int _timeout;
    protected IdleHandler(Looper looper, int timeout)
    {
        _looper = looper;
        _timeout = timeout;
    }

    public boolean queueIdle()
    {
        _uiEventsHandler = new Handler(_looper);
        if (_timeout > 0)
        {
            _uiEventsHandler.postDelayed(_uiEventsTask, _timeout);
        }
        else
        {
            _uiEventsHandler.post(_uiEventsTask);
        }
        return(false);
    }
};

private boolean _processingEventsf = false;
private Handler _uiEventsHandler = null;

private Runnable _uiEventsTask = new Runnable()
{
    public void run() {
    Looper looper = Looper.myLooper();
    looper.quit();
    _uiEventsHandler.removeCallbacks(this);
    _uiEventsHandler = null;
    }
};

public void processEvents(int timeout)
{
    if (!_processingEventsf)
    {
        Looper looper = Looper.myLooper();
        looper.myQueue().addIdleHandler(new IdleHandler(looper, timeout));
        _processingEventsf = true;
        try
        {
            looper.loop();
        } catch (RuntimeException re)
        {
            // We get an exception when we try to quit the loop.
        }
        _processingEventsf = false;
     }
}

【讨论】:

  • 你能解释一下nested event loop是什么意思吗?
  • 这意味着您的调用堆栈上有两个事件循环调用。 Android 提供了它自己的事件循环(主 Looper,它是自动创建的)。如果你想有一个模态对话框或类似的,你需要创建自己的事件循环,所以你最终会在调用堆栈上得到两个嵌套的事件循环。
【解决方案7】:

我有类似fifth 的解决方案,但有点 更简单,不需要反思。我的想法是,为什么不 使用异常退出looper。所以我的自定义弯针 内容如下:

1) 抛出的异常:

final class KillException extends RuntimeException {
}

2) 自定义循环器:

public final class KillLooper implements Runnable {
    private final static KillLooper DEFAULT = new KillLooper();

    private KillLooper() {
    }

    public static void loop() {
        try {
            Looper.loop();
        } catch (KillException x) {
            /* */
        }
    }

    public static void quit(View v) {
        v.post(KillLooper.DEFAULT);
    }

    public void run() {
        throw new KillException();
    }

}

自定义looper的使用非常简单。认为 您有一个对话框 foo,然后只需执行以下操作 你想在哪里调用对话框 foo 模态:

a) 调用 foo 时:

foo.show();
KillLooper.loop();

在对话框 foo 中,当你想退出时,你只需 调用自定义looper的quit方法。这看起来 如下:

b) 从 foo 退出时:

dismiss();
KillLooper.quit(getContentView());

我最近看到 5.1.1 Android 的一些问题, 不要从主菜单调用模态对话框,而是发布一个 调用模态对话框的事件。不张贴 主菜单会停止,我已经看到 Looper::pollInner() 我的应用程序中的 SIGSEGV。

【讨论】:

    【解决方案8】:

    这并不难。

    假设您的所有者活动有一个标志(名为waiting_for_result),只要您的活动恢复:

    public void onResume(){
        if (waiting_for_result) {
            // Start the dialog Activity
        }
    }
    

    这保证了所有者活动,除非模式对话框被解除,否则每当它试图获得焦点时都会传递给模式对话框活动。

    【讨论】:

    • 我已经用一些来源更新了第一个线程。我没关注你,是不是和onResume有关系?谢谢。
    【解决方案9】:

    一个解决方案是:

    1. 将每个选定按钮的所有代码放入每个按钮的侦听器中。
    2. alert.show(); 必须是调用警报的函数中的最后一行代码。此行之后的任何代码都不会等待关闭警报,而是会立即执行。

    希望帮助!

    【讨论】:

    • 这对我有用。我可以通过调用 dialog.setOnDissmissListener 来模拟模态。
    【解决方案10】:

    我不确定这是否是 100% 模态的,因为您可以单击其他组件来关闭对话框,但我对循环结构感到困惑,因此我将其作为另一种可能性提供。它对我来说效果很好,所以我想分享这个想法。您可以在一种方法中创建并打开对话框,然后在回调方法中将其关闭,程序将在执行回调方法之前等待对话框回复。如果您随后在新线程中运行其余的回调方法,则对话框也将首先关闭,然后再执行其余代码。您唯一需要做的就是拥有一个全局对话框变量,以便不同的方法可以访问它。所以像下面这样的东西可以工作:

    public class MyActivity extends ...
    {
        /** Global dialog reference */
        private AlertDialog okDialog;
    
        /** Show the dialog box */
        public void showDialog(View view) 
        {
            // prepare the alert box
            AlertDialog.Builder alertBox = new AlertDialog.Builder(...);
    
            ...
    
            // set a negative/no button and create a listener
            alertBox.setNegativeButton("No", new DialogInterface.OnClickListener() {
                // do something when the button is clicked
                public void onClick(DialogInterface arg0, int arg1) {
                    //no reply or do nothing;
                }
            });
    
            // set a positive/yes button and create a listener
            alertBox.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                // do something when the button is clicked
                public void onClick(DialogInterface arg0, int arg1) {
                    callbackMethod(params);
                }
            });
    
            //show the dialog
            okDialog = alertBox.create();
            okDialog.show();
        }
    
    
        /** The yes reply method */
        private void callbackMethod(params)
        {
            //first statement closes the dialog box
            okDialog.dismiss();
    
            //the other statements run in a new thread
            new Thread() {
                public void run() {
                    try {
                        //statements or even a runOnUiThread
                    }
                    catch (Exception ex) {
                        ...
                    }
                }
            }.start();
        }
    }
    

    【讨论】:

      【解决方案11】:

      使用调用活动中所需的下一个方法的 BroadcastReceiver。

      dialogFragment.show(fragmentTransaction, TAG);并在 onReceive() 中继续它——我不是 100% 肯定的,但我会为 startActivityForResult(); 存钱。正是基于这个概念。

      在从接收方调用该方法之前,代码将等待用户交互而不发生 ANR。

      DialogFragment 的 onCreateView 方法

      private static final String ACTION_CONTINUE = "com.package.name.action_continue";
      
      @Nullable
      @Override
      public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
          View v = inflater.inflate(R.layout.fragment_dialog, container, false);
              Button ok_button = v.findViewById(R.id.dialog_ok_button);
              ok_button.setOnClickListener(new View.OnClickListener() {
                  @Override
                  public void onClick(View v) {
                      Intent i = new Intent();
                      i.setAction(ACTION_CONTINUE);
                      getActivity().getApplicationContext().sendBroadcast(i);
                      dismiss();
                  }
              });
      
      
          return v;
      }
      

      此方法依赖于构建 DialogFrament 扩展类并通过 Activity 调用该类的实例。

      不过……

      简单、清晰、容易和真正的模态。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-01-30
        • 1970-01-01
        • 1970-01-01
        • 2012-04-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多