最近では、Androidでのモーダルダイアログのシミュレーションに取り組んでいます。私は多くのことをグーグルで調べましたが、多くの議論がありますが、残念なことに、それをモーダルにするオプションはあまりありません。背景を以下に示します。
ダイアログ、モーダルダイアログ、およびブロックイン
ダイアログ/アラートダイアログ:ダイアログが起動しているときに「実行をブロック」する方法(.NETスタイル)
モーダルな振る舞いを取得する直接的な方法はありません。その後、3つの可能な解決策を思いつきました
1。 thread 前述のように、ダイアログをテーマにしたアクティビティを使用しますが、メインアクティビティがダイアログアクティビティの戻りを本当に待つことはできません。メインアクティビティは停止ステータスに変わり、その後再起動されました。
2。 1つのワーカースレッドを構築し、スレッド同期を使用します。しかし、それは私のアプリにとって大きなリファクタリングの仕事であり、メインUIスレッドに単一のメインアクティビティとサービスがあります。
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ステータスになりました。
質問は、主な活動が本当に結果を待つようにする方法ですか?
ありがとう。
使用中にモーダルダイアログが表示されました:
setCancelable(false);
(DialogBuilderではなく)DialogFragmentで。
あなたが計画した方法は不可能です。まず、UIスレッドをブロックすることは許可されていません。アプリケーションは終了します。第二に、別のアクティビティがstartActivity
で開始されたときに呼び出されるライフサイクルメソッドを処理する必要があります(他のアクティビティの実行中は元のアクティビティが一時停止されます)。第三に、UIスレッドからではなく、startAlertDialog()
を使用して、スレッド同期(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();
次に、それに応じて正または負のフィードバックを処理する2つの方法を用意します(つまり、何らかの操作を進めるか、アクティビティを終了するか、意味のあること)。
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;
}
これは私のために働く:あなたのダイアログとしてアクティビティを作成します。次に、
これをアクティビティのマニフェストに追加します。
Android:theme = "@ Android:style/Theme.Dialog"
これをアクティビティのonCreateに追加します
setFinishOnTouchOutside(false);
アクティビティでonBackPressedをオーバーライドします。
@Override public void onBackPressed(){//このアクティビティから「戻る」ことを防ぐ}
最初のものは、アクティビティにダイアログの外観を与えます。後者の2つにより、モーダルダイアログのように動作します。
最後に、私は本当にまっすぐでシンプルな解決策になりました。
Win32プログラミングに精通している人は、おそらくモーダルダイアログの実装方法を知っているでしょう。一般に、モーダルダイアログが起動すると、ネストされたメッセージループ(GetMessage/PostMessageによる)を実行します。そこで、この伝統的な方法で独自のモーダルダイアログを実装しようとしました。
最初は、AndroidはUIスレッドメッセージループに挿入するインターフェイスを提供していなかったか、見つかりませんでした。ソースのLooper.loop()を調べたところ、しかし、MessageQueue/Messageはパブリックインターフェイスを提供していませんが、幸いなことに、Javaに反映されています。ネストされたモーダルダイアログはテストしていませんが、理論的には動作します。
これが私のソースコードです。
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();
}
}
}
}
うまくいけば、これが役立つでしょう。
1つの解決策は次のとおりです。
alert.show();
は、Alertを呼び出す関数の最後のコード行でなければなりません。この行の後のコードは、アラートを閉じるのを待たずにすぐに実行されます。ヘルプを願っています!
他のコンポーネントをクリックしてダイアログボックスを閉じることができるため、これが100%モーダルであるかどうかはわかりませんが、ループ構造と混同されたため、別の可能性としてこれを提供します。私にとってはうまくいったので、そのアイデアを共有したいと思います。 1つのメソッドでダイアログボックスを作成して開き、コールバックメソッドでダイアログボックスを閉じると、プログラムはダイアログの応答を待ってからコールバックメソッドを実行できます。その後、新しいスレッドでコールバックメソッドの残りを実行すると、残りのコードが実行される前に、ダイアログボックスも最初に閉じます。あなたがする必要がある唯一のことは、さまざまな方法がそれにアクセスできるように、グローバルダイアログボックス変数を持つことです。そのため、次のようなものが機能します。
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();
}
}
Hackbodなどが指摘しているように、Androidは意図的にネストされたイベントループを実行する方法を提供していません。この理由を理解していますが、それを必要とする特定の状況があります。さまざまなプラットフォームで実行されている独自の仮想マシンがあり、Androidに移植したいと考えていました。内部にはネストされたイベントループが必要な場所が多く、Androidだけをすべて書き換えることは実際には不可能です。 、ここに解決策があります(基本的に 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;
}
}
難しくありません。
所有者のアクティビティにフラグがあると仮定します(名前はwaiting_for_result
)、アクティビティが再開されるたび:
public void onResume(){
if (waiting_for_result) {
// Start the dialog Activity
}
}
これにより、モーダルダイアログが閉じられない限り、フォーカスを取得しようとするたびにモーダルダイアログアクティビティに渡される所有者アクティビティが保証されます。
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();
}
}
カスタムルーパーの使用は非常に簡単です。ダイアログfooがある場合、ダイアログfooをモーダルに呼び出す場所で次の操作を実行します。
a)fooを呼び出すとき:
foo.show();
KillLooper.loop();
ダイアログfoo内で終了するには、カスタムルーパーのquitメソッドを呼び出すだけです。これは次のようになります。
b)fooを終了する場合:
dismiss();
KillLooper.quit(getContentView());
私は最近、5.1.1 Androidでいくつかの問題を見てきました。メインメニューからモーダルダイアログを呼び出さず、代わりにモーダルダイアログを呼び出すイベントをポストします。メインメニューを表示しないとストールし、アプリでLooper :: pollInner()SIGSEGVを確認しました。
アクティビティで必要な次のメソッドを呼び出す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拡張クラスを構築し、アクティビティを通じてそのクラスのインスタンスを呼び出すことに依存しています。
しかしながら...
シンプル、クリア、簡単、そして本当にモーダル。