可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
These days I'm working on simulating modal dialog in Android. I've googled a lot, there's much discussions but sadly there's not much options to get it modal. Here's some background,
Dialogs, Modal Dialogs and Blockin
Dialogs / AlertDialogs: How to "block execution" while dialog is up (.NET-style)
There's no straight way to get modal behavior, then I came up with 3 possible solutions,
1. Use a dialog-themed activity, like this thread said, but I still can't make main activity truly wait for dialog-activity return. Main activity turned to stop status and got restarted then.
2. Build one worker thread, and use thread synchronization. However, it's a huge refactoring job for my app, now I have a single main activity and a service both in main UI thread.
3. Take over event handling within a loop when there is a modal dialog up, and quit loop when dialog gets closed. Actually it's the way to build a real modal dialog like what it exactly does in Windows. I still haven't prototyped this way.
I'd still like to simulate it with a dialog-themed activity,
1. start dialog-activity by startActivityForResult()
2. get result from onActivityResult()
Here's some source
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;
}
}
}
The caller of startAlertDialog will block execution and expect returned result. But startAlertDialog returned immediately of course, and main activity went into STOP status while DialogActivity was up.
So the question is, how to make main activity really wait for result?
Thanks.
回答1:
I got a modal Dialog while using:
setCancelable(false);
on the DialogFragment (not on the DialogBuilder).
回答2:
It is not possible the way you planned. First, you are not allowed to block the UI thread. Your application will be terminated. Second, need to handle the lifecycle methods that are called when another activity is started with startActivity
(your original acitvity will be paused while the other activity is running). Third, you probably could somehow hack it by using startAlertDialog()
not from the UI thread, with thread synchronization (like Object.wait()
) and some AlertDialog
. However, I strongly encourage you to not do this. It is ugly, will certainly break and it's just not the way things are intended to work.
Redesign your approach to capture the asynchronous nature of these events. If you want for example some dialog which asks the user for a decsision (like accepting the ToS or not) and do special actions based on that decision create a dialog like this:
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();
Then have two methods handling positive or negative feedback accordingly (i.e. proceeding with some operation or finishing the activity or whatever makes sense).
回答3:
Developers of Android and iOS decided that they are powerful and smart enough to reject Modal Dialog conception (that was on market for many-many years already and didn't bother anyone before), unfortunately for us.
Here is my solution, it works great:
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;
}
回答4:
This works for me: create an Activity as your dialog. Then,
Add this to your manifest for the activity:
android:theme="@android:style/Theme.Dialog"
Add this to onCreate of your activity
setFinishOnTouchOutside (false);
Override onBackPressed in your activity:
@Override
public void onBackPressed()
{
// prevent "back" from leaving this activity
}
The first gives the activity the dialog look. The latter two make it behave like a modal dialog.
回答5:
Finally I ended up with a really straight and simple solution.
People who's familiar with Win32 programming possibly knows how to implement a modal dialog. Generally it runs a nested message loop (by GetMessage/PostMessage) when there is a modal dialog up. So, I tried to implement my own modal dialog in this traditional way.
At the first, android didn't provide interfaces to inject into ui thread message loop, or I didn't find one. When I looked into source, Looper.loop(), I found it's exactly what I wanted. But still, MessageQueue/Message haven't provided public interfaces. Fortunately, we have reflection in java.
Basically, I just copied exactly what Looper.loop() did, it blocked workflow and still properly handled events. I haven't tested nested modal dialog, but theoretically it would work.
Here's my source code,
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();
}
}
}
}
Hopefully this would help.
回答6:
It's not difficult.
Assume you have a flag on your owner activity (named waiting_for_result
), whenever your activity is resumed:
public void onResume(){
if (waiting_for_result) {
// Start the dialog Activity
}
}
This guaranteed the owner activity, unless the modal dialog is dismissed, whenever it try to get focus will pass to the modal dialog activity.
回答7:
One solution is :
- Put all code for each selected button into the listener of each button.
alert.show();
must be the last code line in the function calling the Alert. Any code after this line will not wait to close the Alert, but will execute immediately.
Hope Help!
回答8:
As hackbod and others have pointed out, Android deliberately doesn't provide a method for doing nested event loops. I understand the reasons for this, but there are certain situations that require them. In our case we have our own virtual machine running on various platforms and we wanted to port it to Android. Internally there a lot of places where it requires a nested event loop, and it isn't really feasible to rewrite the whole thing just for Android. Anyway, here is a solution (basically taken from How can I do non-blocking events processing on Android?, but I have added a timeout):
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;
}
}
回答9:
I have a similar solution like fifth, but its a little bit
simpler and doesn't need reflection. My thinking was, why not
use an exception to exit the looper. So my custom looper
reads as follows:
1) The exception that is thrown:
final class KillException extends RuntimeException {
}
2) The custom looper:
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();
}
}
The use of the custom looper is quite simple. Suppose
you have a dialog foo, then simply do the following
where you want to call the dialog foo modally:
a) When calling into foo:
foo.show();
KillLooper.loop();
Inside the dialog foo, when you want to exit, you simply
call the quit method of the custom looper. This looks
as follows:
b) When exiting from foo:
dismiss();
KillLooper.quit(getContentView());
I have recently seen some problems with 5.1.1 Android,
do not call a modal dialog from main menu, instead post an
event that calls the modal dialog. Without posting the
main menu will stall, and I have seen Looper::pollInner()
SIGSEGVs in my app.
回答10:
I am not sure if this is 100% modal, as you can click on some other component to close the dialog box, but I got confused with the loops constructs and so I offer this as another possibility. It worked nicely for me, so I would like to share the idea. You can create and open the dialog box in one method and then close it in the callback method and the program will wait for the dialog reply before executing the callback method. If you then run the rest of the callback method in a new thread, the dialog box will also close first, before the rest of the code is executed. The only thing you need to do is to have a global dialog box variable, so that different methods can acccess it. So something like the following can work:
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:
Use a BroadcastReceiver that calls the next method required in the activity.
Dead-end the activity code at dialogFragment.show(fragmentTransaction, TAG); and continue it in onReceive()--i'm not 100% positive but I would lay money that startActivityForResult(); is based on exactly this concept.
Until that method is invoked from the receiver, the code will stand in wait for user interaction without ANR.
DialogFragment's onCreateView Method
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;
}
This method depends on building a DialogFrament extension class and calling an instance of that class through the activity.
However...
Simple, clear, easy and truly modal.