【Android】DialogFragmentの背景を透過しようとしてハマった話


この記事の内容

DialogFragmentの背景を透過しようとしたらハマったので、調べたことのまとめ

・dialog.window().setBackgroundDrawableResource()で透過できる
・onCreateDialog()では、dialogがnullになる
・DialogFragmentとlifecycleの話

やりたいこと

dialogFragmentで表れる余白のbackgroundを透明にしたい。
Before → After
  

試したこと

とりあえずstack overflow を検索するとピッタリなやつがあった。
dialogの"setBackgroundDrawable()"メソッドで透過背景を指定すればいいらしい。簡単そう。

Answer : Add this code.
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent)

さっそく試して実行してみる。

HogeDialogFragment.kt
class HogeDialogFragmnet: DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        //背景を透過する処理
        dialog!!.window!!.setBackgroundDrawableResource(android.R.color.transparent)

        val builder = AlertDialog.Builder(activity)
        //レイアウトの処理は割愛
        return builder.create()
    }
}

ダメでした。

E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.kabos.sample, PID: 2681
         java.lang.NullPointerException

DialogFragmentのソースコードを確認してみる

dialogインスタンスがnullになってエラーがでてしまいました。
onCreateDialogで取得しようとしたのがマズかったかもしれない仮説が浮上したので
DialogFragmentのソースコードを確認してみました。

DialogFragment.java
public class DialogFragment extends Fragment
        implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener {

    //③getDialog()でメンバー変数のDialogインスタンスを取得する
    @Nullable
    public Dialog getDialog() {
        return mDialog;
    }

    //②DialogインスタンスはonGetLayoutInflater()の中で、onCreateDialog()から取得している
    @Override
    @NonNull
    public LayoutInflater onGetLayoutInflater(@Nullable Bundle savedInstanceState) {
        LayoutInflater layoutInflater = super.onGetLayoutInflater(savedInstanceState);
        if (!mShowsDialog || mCreatingDialog) {
            return layoutInflater;
        }

        try {
            mCreatingDialog = true;
            mDialog = onCreateDialog(savedInstanceState);//←ココ
            setupDialog(mDialog, mStyle);
        } finally {
            mCreatingDialog = false;
        }

        return layoutInflater.cloneInContext(requireDialog().getContext());
    }

    //①onCreateDialogで初めてDialogがインスタンス化される
    @MainThread
    @NonNull
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        return new Dialog(requireContext(), getTheme());
    }
}

確認してみたところ、dialogがインスタンス化されるのは、onCreateDialog()のタイミングになっていました。
自分の最初のコードではonCreateDialog()の中でdialogを参照していたので、nullになったようです。
そのため、onCreateDialog()よりも後のタイミングである以下のタイミングで呼び出す必要があります。
・onCreateView()
・onStart()

またDialogFragmentはfragmentを継承しているので、Fragmentと同様のライフサイクルになります。
・onCreate() / onCreateDialog() 
・onCreateView()
・onStart()
・onResume()

以下のように、dialogにアクセスするタイミングを変えると、当初の目的通り背景が透過されます。

HogeDialogFragment.kt
class HogeDialogFragmnet: DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        //レイアウトの処理は割愛
    }

    override fun onStart() {
        super.onStart()
        //dialogはonCreateDialogでインスタンス化されたので、アクセスできる
        dialog!!.window!!.setBackgroundDrawableResource(android.R.color.transparent)
    }
}

まとめ

・dialog.window().setBackgroundDrawableResource()でdialogの背景を透過できる
・dialogはonCreateDialog()でインスタンス化されるので、それより後にアクセスする

ライフサイクルについて曖昧な理解のままだったので、ライフサイクルについて学ぶいい機会になりました。