PopupWindowで画面内にダイアログ風の表示をする方法

# Dialogの実装は面倒
AndroidのDialog実装は割りと面倒くさい。
API level 13で`showDialog`が非推奨となってからは`DialogFragment`を使うのが一般的になったためだ。
`DialogFragment`を使うとコールバックや非同期処理に気を使わなくてはいけないため、どうしても直感的でない実装になることがある。
単純なダイアログ表示してボタンが押されたタイミングを知りたい、というだけの時には面倒に感じることもある。
この記事では面倒なダイアログ表示のかわりに`PopupWindow`によって表示する方法を紹介する。

# PopupWindow
`PopupWindow`は文字通り画面の上にポップアップして表示される要素となる。
挙動としては`Spinner`やActionBarのOverflowボタンを押した時に出てくるリストに近い。
通常はViewのonClickなどに対応してそのViewの近くに表示したりするが、今回は画面中央に表示する。

# 実装
`PopupWindow`はAPI level 1から存在し、あまり変更が加えられていないため、実装も見た目も2.x系と4.x系でほとんど差がない。

## 背景Drawableの用意
`PopupWindow`には標準の背景が用意されているため、あまり拘りがなければ用意する必要はない。
背景に何も指定しなかった場合、以下の様な灰色の背景色で表示される。
![標準のPopupWIndow](https://qiita-image-store.s3.amazonaws.com/0/48019/95f35f68-1bd0-5266-a0f9-3f613750bae0.jpeg)


この標準の灰色背景のまま使うのは微妙、という場合には以下の様な背景Drawableを用意する。
ここでは単純に白背景に黒枠にした。

````popup_background.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@android:color/white" />
    <stroke
        android:width="2dp"
        android:color="@android:color/black" />
</shape>
````

## レイアウトの用意
ダイアログ同様、`PopupWindow`の中にもViewを配置することができる。
今回は以下のようなレイアウトを用意することにした。
親レイアウトで`layout_width`を指定しても`PopupWindow`の表示には反映されないので注意が必要。
後述するが、`PopupWindow`の幅はコード側で表示しなくてはいけない。

````popup_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="8dp">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="PopupWindow表示" />
    <Button
        android:id="@+id/close_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="閉じる" />
</LinearLayout>
````

## ソースコード
説明が長いので先にコードを貼る。

````MyActivity.java

    private PopupWindow mPopupWindow;

    @Override
    public void onClick(View v) {

        mPopupWindow = new PopupWindow(MyActivity.this);

        // レイアウト設定
        View popupView = getLayoutInflater().inflate(R.layout.popup_layout, null);
        popupView.findViewById(R.id.close_button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mPopupWindow.isShowing()) {
                    mPopupWindow.dismiss();
                }
            }
        });
        mPopupWindow.setContentView(popupView);

        // 背景設定
        mPopupWindow.setBackgroundDrawable(getResources().getDrawable(R.drawable.popup_background));

        // タップ時に他のViewでキャッチされないための設定
        mPopupWindow.setOutsideTouchable(true);
        mPopupWindow.setFocusable(true);

        // 表示サイズの設定 今回は幅300dp
        float width = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300, getResources().getDisplayMetrics());
        mPopupWindow.setWindowLayoutMode((int) width, WindowManager.LayoutParams.WRAP_CONTENT);
        mPopupWindow.setWidth((int) width);
        mPopupWindow.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);

        // 画面中央に表示
        mPopupWindow.showAtLocation(findViewById(R.id.show_button), Gravity.CENTER, 0, 0);
    }

    @Override
    protected void onDestroy() {
        if (mPopupWindow != null && mPopupWindow.isShowing()) {
            mPopupWindow.dismiss();
        }
        super.onDestroy();
    }
````

流れとしては、`PopupWindow`に設定する`View`を`inflate`して`setContentView`し、背景やフォーカス、表示サイズの設定後に表示処理となる。
Viewの生成～`setContentView`までは特に変わった処理ではないので割愛する。

背景は先ほど用意したDrawableを`PopupWindow.setBackgroundDrawable`で指定する。
レイアウト側で指定したい場合、以下のように透明Drawableを設定するとレイアウト側で定義したもののみ表示される。

````java
    mPopupWindow.setBackgroundDrawable(new ColorDrawable(android.R.color.transparent));
````

フォーカスについては`PopupWindow.setFocus(true)`していない場合、直下のViewにタッチイベントが伝わってしまうため、ダイアログ風に使うときは必須となる。
`PopupWindow.setOutsideTouchable(true)`も同様の理由で設定。

一番面倒なのは表示サイズの設定。これはレイアウト側で指定することができない。
しかも`setWindowLayoutMode`と`setWidth/setHeight`の両方で指定する必要があった。
今回は幅300dpで表示することにしたが、pxで指定する必要が有るため`TypedValue`を使って変換している。
このソースコード上でサイズ設定をするというのがダイアログを置き換える上で一番のネックになりそう。

その他の注意点としては以下
* dissmissしないまま画面が終了すると落ちる(`onDestroy`で`PopupWindow.dismiss`する)
* `PopupWindow.setWindowLayoutMode`は2.x系には適用されない(っぽい)
* `PopupWindow.setWidth`、`PopupWindow.setHeight`は2.x系では必須。
* `PopupWindow.showAtLocation`の第一引数は画面内のViewならなんでもいいっぽい

# 結果
それぞれ、Android 4.2と2.3での表示例。
PopupWindowそのものにはほとんど差異はない。
![Android 4.2での表示](https://qiita-image-store.s3.amazonaws.com/0/48019/d0833433-e650-12d5-98b9-93ee4a36e7f6.jpeg)![Android 2.3での表示](https://qiita-image-store.s3.amazonaws.com/0/48019/1737a580-35bc-6f59-c552-b89057a498e2.jpeg)

# まとめ
結構簡単な実装で表示できてコールバックも楽に取れるのは良い。
ソースコードで表示幅指定しなければいけない問題も、工夫すればできるんじゃないかという気はする。
でもDialogと違ってバージョンごとにボタン配置とかが最適化されていない問題もあるので、結局Dialogの代わりにはならないだろうなー。