安卓:如何获取模态对话框或类似的模态行为?

58

最近我正在研究在Android中模拟模态对话框。我进行了大量搜索,有很多讨论,但遗憾的是没有太多选项可以获得模态对话框效果。以下是一些背景信息,
对话框、模态对话框和阻塞
对话框/AlertDialog:如何在对话框弹出时“阻塞执行”(类似.NET)

没有直接的方法来获取模态行为,然后我想到了3种可能的解决方案:
1. 使用对话框主题的Activity,就像这个线程所说的那样,但我仍然无法使主Activity真正等待对话框Activity返回。 主Activity变成了停止状态,然后重新启动。
2. 构建一个工作线程,并使用线程同步。 但是,这对我的应用程序来说是巨大的重构工作,现在我有一个单独的主Activity和一个Service都在主UI线程中。
3. 在模态对话框弹出时接管事件处理,当对话框关闭时退出循环。 实际上这是在Windows中构建真正的模态对话框的方法。 我仍然没有原型化这种方式。

我仍然想用对话框主题的Activity来模拟它,
1. 使用startActivityForResult()启动对话框Activity
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状态。

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


这篇文章可能会对你有所帮助。 https://dev59.com/2HI-5IYBdhLWcg3wEEDu#7609503 - Daniel
2
我真的不敢相信Android在这些非常简单的事情上会出现问题。 - LEMUEL ADANE
你问题中的第一个链接解释了Android确实有模态对话框(但不是阻塞式的)。如果你改变术语以反映你真正想要的——线程阻塞,那么你的问题会更清晰明了。 - LarsH
使用广播接收器调用链中的下一个方法...直到该方法被调用之前,代码将陷入死胡同。 - me_
Google很遗憾Android不支持模态对话框。我认为他们应该解决这个问题。作为程序员,我们应该说服Google做正确的事情,而不是重新发明一个类似模态的对话框。模态对话框已经存在并由Microsoft在Windows中实现。为什么Google没有实现呢?因为Google的工程师太懒了;-) - saeed khalafinejad
13个回答

74

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

setCancelable(false);

在DialogFragment上(而不是在DialogBuilder上)。

9
运作得非常好。例如:最终的AlertDialog.Builder构造器= new AlertDialog.Builder(this).setCancelable(false); - slott
3
这不起作用。所谓模态对话框是指UI线程在对话框未关闭之前会停止工作,但是这段代码并非如此。 - Code Pope
如前所述,如果您的对话框是一个独立的片段,则应在对话框片段上完成此操作。 public class MyCustomDialog extends DialogFragment{ .... public Dialog onCreateDialog(Bundle savedInstanceState) { setCancelable(false); ...} - Alireza Fattahi
2
这并不会停止代码的执行,而是防止在返回键按下时取消对话框... 可以通过在 dialogFragment.show(fragmentTransaction, TAG); 处死掉代码,并使用 onClickListener 发送广播意图来调用所需方法,在对话框被解散后完成真正的停止。 - me_
这是最好的答案。 - slogan621
显示剩余2条评论

19

按照您的计划,这种做法是不可能的。首先,您不能阻塞UI线程,否则您的应用程序将被终止。其次,需要处理生命周期方法,当使用startActivity启动另一个活动时会调用这些方法(您原始的活动将在其他活动运行时暂停)。第三,您可能可以通过在非UI线程中使用startAlertDialog()、线程同步(例如Object.wait())和一些AlertDialog来进行某种方式的操作。然而,我强烈建议您不要这样做。它很丑陋,肯定会出问题,而且这不是事情本应该的工作方式。

重新设计您的方法以捕获这些事件的异步性质。如果您想要例如询问用户是否接受服务条款并根据该决定执行特殊操作的对话框,请创建这样的对话框:

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();
然后有两种方法来相应地处理正面或负面反馈(即继续某些操作或完成活动或任何有意义的事情)。

3
谢谢。我完全同意你上面提到的所有内容。不幸的是,对我来说这是必需的要求,而实际情况更加复杂,有很长的故事。模态行为基本上与Android的设计相冲突,我们都知道,但是...无论如何,我正在尝试寻找一种优雅的方式来解决它 :( 解决方案3是一个选项吗? - fifth
在这样的对话框中是否可能拥有丰富的控件(例如按钮、文本视图和编辑视图),并等待用户提供输入后,重新聚焦到正在运行的活动? - Kushal
4
我对这句话的措辞有异议:“按照你的计划是不可能的。首先,你不能阻塞UI线程。” ——并不需要阻塞UI线程才能显示模态对话框——如果应用程序阻塞了UI线程,Windows也会强制终止它们,但这并不会阻止它们显示模态对话框——这些应用程序可以在强制用户处理模态对话框之前继续处理消息并保持响应性(而且,父窗口也没有被阻塞——它们可以正常处理所有事件)。 - BrainSlugs83
@BrainSlugs83说:“如果应用程序阻塞UI线程,Windows会关闭它们。” - 不会的。 - bobbyalex
1
@BrainSlugs83:我认为问题不在于Stephan的措辞,而在于提问者。Stephan并没有说模态对话框需要阻塞;那是提问者的观点。 - LarsH
回答“抱歉,伙计,这是不可能的……它违反了某些规定或政策……”最简单的方法就是如果你不知道答案,请不要回答。谢谢。 - Vitaly

8
开发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线程,因为该线程可能会触发更多的断言。 - knight666

6
最终,我找到了一个非常简单明了的解决方案。
熟悉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();
        }
    }
}
}

希望这能有所帮助。

17
不要这样做。这段代码正在使用私有API。很明显它在使用私有API。任何人如果这样做,都可能导致他们的应用程序崩溃。 - hackbod
2
这段代码很尴尬。理智的人不应该使用这些技巧。 - CommonsWare
10
这只是表明安卓框架在设计上存在缺陷。怎么可能没有一个简单的模态对话框机制?! - Saideira
1
使用另一个Activity作为对话框的主要问题在于,您无法区分失去整个应用程序的焦点和因为应用程序中的另一个活动在onPause()上方而失去焦点。在这种情况下,使用类似模态的对话框可能是有意义的。特别是如果客户需求已经改变,但您不是“企业级”,没有三个月的重构预算... - Torp
3
尽管这是糟糕的编程,但我还是点赞了,因为它证明了概念。 - pellucide
显示剩余2条评论

6
这适用于我:将Activity作为对话框创建。然后,
1. 将以下内容添加到您的活动清单中: android:theme="@android:style/Theme.Dialog"
2. 在您的活动的onCreate中添加以下内容: setFinishOnTouchOutside(false);
3. 重写您的活动中的onBackPressed: @Override public void onBackPressed() { // 防止"返回"离开此活动 }
第一步使活动具有对话框外观。 后两步使其像模态对话框一样运行。

谢谢你。正是我在寻找的。 - Andre Aus B
1
这需要最低API级别为11。 - Matt G

3
我有一个类似于 fifth 的解决方案,但它更简单,不需要反射。我的想法是,为什么不使用异常来退出循环呢?因此,我的自定义循环器如下所示:

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

我最近发现了一些与Android 5.1.1相关的问题,不要从主菜单调用模态对话框,而是发布一个事件来调用模态对话框。如果不发布,主菜单将会停滞,并且我在我的应用程序中看到了Looper :: pollInner() SIGSEGVs。


2
正如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;
     }
}

你能解释一下什么是“嵌套事件循环”吗? - CodyBugstein
1
这意味着您的调用堆栈上有两个事件循环调用。Android提供了自己的事件循环(主要的Looper,它会自动创建)。如果您想要一个模态对话框或类似的东西,您需要创建自己的事件循环,因此您将在调用堆栈上嵌套两个事件循环。 - CpnCrunch

1

我不确定这是否是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();
    }
}

1

这并不难。

假设您在所有者活动上有一个标志(名为waiting_for_result),每当您的活动恢复时:

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

这将确保所有者活动,除非模态对话框被关闭,否则每当它尝试获取焦点时,将传递到模态对话框活动。


1
我已经在第一个线程中更新了一些源代码。我不明白你的意思,这与onResume有关吗?谢谢。 - fifth

1
一个解决方案是:
  1. 将每个选定按钮的所有代码放入每个按钮的监听器中。
  2. alert.show(); 必须是调用 Alert 的函数中的最后一行代码。此行代码之后的任何代码都不会等待关闭 Alert,而是立即执行。
希望能帮到您!

这对我有用。我能够通过调用dialog.setOnDissmissListener来模拟模态。 - BrainSlugs83

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