AlertDialog#findViewByIdはshow以降じゃないと使えない

Androidはビューをインフレートすることでカスタムなダイアログを生成することができる。

  • Activity中にて
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    LayoutInflater factory = LayoutInflater.from(this);
    View layoutForDialog = factory.inflate(R.layout.layoutfordialog, null);

    Dialog dialog = new AlertDialog.Builder(this)
        .setIcon(android.R.drawable.ic_input_get) 
        .setTitle("ダイアログのタイトル")
        .setCancelable(true)
        .setMessage("ダイアログのメッセージ") 
        .setView(layoutForDialog) 
        .setPositiveButton("OK", null)
        .setNegativeButton("CANCEL", null)
        .create();

    dialog.show();
  • 実行結果

このとき、ダイアログが表示される前にダイアログ中のビューに一工夫しようと以下のようにコードを追加するも上手くいかない。

    TextView tv = (TextView)dialog.findViewById(R.id.TextView01);
    tv.setText("テキストを変える");
    
    dialog.show();

トレースするとfindViewByIdの戻りがnullのようだ。(Caused by: java.lang.NullPointerException) しかし、R.id.TextView01で識別されるビューは間違い無く存在している。

ならばと、Dialog#showの後にコードを移すと

    dialog.show();

    TextView tv = (TextView)dialog.findViewById(R.id.TextView01);
    tv.setText("テキストを変える");

今度は成功する。(以前に書いたようにAndroidのダイアログはスレッドをブロックしない(モーダルではない)ので、showメソッドは瞬時にリターンする)
こりゃおかしいなとshowメソッドの前後でビューの階層を調べてみたのだが、

  • Dialog.Show()前のビュー階層
dialog.getWindow.getDecorView().
    mChildren    View[12]
        [0]    LinearLayout
            :
            mChildren    View[12]
                [0]    TextView
                [1]    FrameLayout
                    :
                    mChildren    View[12]
                        [0]    FrameLayout
                            :
                            mChildren    View[12]
                                [0]    null    
                                :
  • Dialog.Show()後のビュー階層
dialog.getWindow.getDecorView().
    mChildren    View[12]
        [0]    FrameLayout
            :
            mChildren    View[12] 
                [0]    WeightedLinearLayout
                    :
                    mChildren    View[12]
                        [0]    LinearLayout 
                        [1]    LinearLayout
                        [2]    FrameLayout
                            :
                            mChildren    View[12]
                                [0]    FrameLayout
                                    :
                                    mChildren    View[12]
                                        [0]    LinearLayout
                                            :
                                            mChildren    View[12]
                                                [0]    TextView  <= TextView1
                                                [1]    TextView
                                                [2]    TextView
                                                :

と全く違う。
どうやら流し込まれたビューの階層構造はshowメソッドを実行しないと完成しないようだ。

前述した通り、AndroidのダイアログはshowメソッドがUIスレッドをブロックすることはないので、showメソッドリターン後にビューを取得、変更する処理を書いても問題は無いが、以前に書いたように他のスレッドでダイアログを生成した場合や、ダイアログを別スレッドから表示するなど、showメソッドの実行前に処理を書きたい場合はどうすればよいのだろう。

答えは簡単で、インフレートしたビューをダイアログに渡す前に目的のビューを取得しておけば良いだけだ。

    • 修正後
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    LayoutInflater factory = LayoutInflater.from(this);
    View layoutForDialog = factory.inflate(R.layout.layoutfordialog, null);
    TextView tv = (TextView)layoutForDialog.findViewById(R.id.TextView01);
    tv.setText("テキストを変える");

    Dialog dialog = new AlertDialog.Builder(this)
        .setIcon(android.R.drawable.ic_input_get) 
        .setTitle("ダイアログのタイトル")
        .setCancelable(true)
        .setMessage("ダイアログのメッセージ") 
        .setView(layoutForDialog) 
        .setPositiveButton("OK", null)
        .setNegativeButton("CANCEL", null)
        .create();

    dialog.show();
    • 実行結果

追記:例として使用した、ダイアログに流し込むレイアウト用XMLを載せるのを忘れていた。

  • /layout/layoutfordialog.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="vertical"
>
 <TextView
  android:id="@+id/TextView01"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="ビュー1"
 ></TextView>
 <TextView
  android:id="@+id/TextView02"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="ビュー2"
 ></TextView>
 <TextView
  android:id="@+id/TextView03"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="ビュー3"
 ></TextView>
</LinearLayout>