モーダルダイアログが欲しい (その2)

いろいろと考えたが、モーダルダイアログと近似した振る舞いのダイアログとするのはよいが、Androidの場合UIスレッドを止めるのは御法度なので、本来の意味のモーダルは実現できない。なのでAsyncTask等を使用して他のスレッドを使用している場合を想定し、そのスレッドの処理をブロックするスレッドモーダル(造語)を実現することにする。

実現するには以下の仕組みが必要だ。

1. ダイアログが閉じたことを関知する仕組み
2. ダイアログが閉じるまで処理スレッドをブロックする仕組み

1.に関しては、DialogクラスはOnDismissListenerという、ダイアログが閉じられた際に割り込むためのリスナがあるので、これで監視できる。2.に関しては、OnDismissListener#onDismissが発生した時点をダイアログ表示の完了として、それまでを待ち合せる処理が書ければ良い。ただし、気をつけて無くてはならないのは、ダイアログが表示されるスレッドは必ずしも非UIスレッドとは限らないので、そこも考慮する必要ある。

  • showDialogWaitDismissメソッド(static)
public static final void showDialogWaitDismiss(final Handler handler, final Dialog dialog
        , final OnDismissListener dismissListener ) {
    final CountDownLatch signal = new CountDownLatch(1);
    dialog.setOnDismissListener(new OnDismissListener() {
        @Override
        public void onDismiss(DialogInterface dialog) {
            try {
                if ( dismissListener != null ) {
                    dismissListener.onDismiss(dialog);
                }
            } finally {
                signal.countDown();
            }
        }
    });
    if (Thread.currentThread() != handler.getLooper().getThread()) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                dialog.show();
            }
        });
        try {
            signal.await();
        } catch (InterruptedException e) {
            //ignore
        }
    } else {
        dialog.show();
    }

}

引数にOnDismissListenerが必要なのは、ダイアログに対して既に同リスナの処理が記述されている場合、リスナの設定(setOnDismissListener)を実行してしまうと以前の処理が上書きされてしまうため、事前に実行する必要があるからだ。

では、テストしてみよう。まずは普通にUIスレッドから呼び出す。

  • DialogTestActivity.java (UIスレッド)
Handler handler = new Handler();

public class DialogTest extends Activity {
    private Handler handler = new Handler();

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

        final Dialog dialog = new AlertDialog.Builder(this) 
            .setMessage("1. モーダルに見えるかい?")   
            .setPositiveButton("OK", null).create();
        
        showDialogWaitDismiss(DialogTest.this.handler, dialog
                , new OnDismissListener() {
                    @Override
                    public void onDismiss(DialogInterface dialog) {
                        Toast.makeText(DialogTest.this, "2. ダイアログ閉じたよ"
                                , Toast.LENGTH_SHORT).show();
                    }
        });

        Toast.makeText(DialogTest.this
                , "3. showDialogWaitDismiss抜けたよ", Toast.LENGTH_SHORT).show();
        
    }
}
  • 実行結果


このように、ダイアログが表示されると同時に「3. showDialogWaitDismiss抜けたよ」が表示されるのは処理が実行されているのがUIスレッドなのでブロックされていない(しちゃいけない)。その後ダイアログを閉じるとOnDismissListenerに書いたトーストが表示される。


次はAsyncTask等を使用してバックグラウンドからダイアログの表示が行われたことを想定して、別スレッドから同じ処理を実行してみよう

  • DialogTestActivity.java (非UIスレッド)
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    final Dialog dialog = new AlertDialog.Builder(this) 
        .setMessage("1. モーダルに見えるかい?")   
        .setPositiveButton("OK", null).create();
    
    new Thread(new Runnable() {
        @Override
        public void run() {
            showDialogWaitDismiss(DialogTest.this.handler, dialog
                    , new OnDismissListener() {
                        @Override
                        public void onDismiss(DialogInterface dialog) {
                            Toast.makeText(DialogTest.this, "2. ダイアログ閉じたよ"
                                    , Toast.LENGTH_SHORT).show();
                        }
            });

            runOnUiThread(new Runnable(){
                @Override
                public void run() {
                    Toast.makeText(DialogTest.this
                            , "3. showDialogWaitDismiss抜けたよ", Toast.LENGTH_SHORT).show();
                }
            });
        }
    }).start();
    
}

変更したのはonCreateメソッドだけ。showDialogWaitDismissを別スレッドから起動したことと、スレッド中にトーストを生成すると例外が発生するため、Activity#runOnUiThreadメソッドでUIスレッドと同期するようにしただけだ。

  • 実行結果


スレッドの処理(showDialogWaitDismissメソッド)はブロックされており、トーストはまだ表示されない。


ダイアログを閉じるとonDismissにより2.のトーストが表示される。


最後に後続のトーストが処理される

とかなりパズルチックになってはしまったが、これで非UIスレッドの処理でダイアログを表示した場合にスレッドをブロックすることが出来たので、これをHttpClientからHTTP BASIC認証を実行する際などに適用してみようと思う。