注意! この記事はQiitaにて公開されていた内容をimportしたものです。
これらの内容は場合によっては陳腐化していて役に立たなくなっていたり、有害であったり、現在の著者の主張と異なることがあります。
皆様の判断の上でご利用いただけますと幸いです(度を超してヤバいものは著者に連絡して頂ければ対応します m(_ _)m)


Preference Support Libraryリリース!

Android MarshmallowのFinal Preview版がリリースされたと同時に、いくつかの新しいSupport Libraryが登場しました。

そのうちのv7 Preference Support Libraryは、android.support.v4.app.Fragmentを継承したPreferenceFragmentCompatを有しているため、これを継承することでandroid.support.v4.app.Fragmentを継承した設定画面を作成することが容易になりました。 よって、PreferenceFragmentだけ特別扱いをするような書き方しなくて済むようになります。

また、SwitchPreferenceなどもMaterial Designになるので、これまでデザインの統一をはかる上で難関だった部分が解消されるはずです(あんま使ってない・・・)

本稿執筆時点(v23.0.1)では、問題があり時期尚早だと感じます。無理して使うにもworkaroundな対応をいくつか盛り込まないと厳しいかな、という感想です。 こちらのリポジトリが参考になります。Gericop/Android-Support-Preference-V7-Fix

さて、Preference Support Libraryでは、SDK組み込みの(もとからある)PrefereneceFragment及びPreferenceクラスとは使い方が異なる箇所がいくつかあるので注意が必要です。 本稿とは関係がないので説明を省きますが、Preference Support Libraryでは要素(Preference)をListViewではなくRecyclerViewで表示しています。これは大きく異なる点だと思われます。

そんなわけで、本記事では、Preference Support Libraryにおけるダイアログ制御について説明します。

なお、以下のQiita記事も非常に参考になります。

Preferenceクラスからダイアログの制御ができなくなった

これまで、例えば「EditTextが空のときにダイアログの確定ボタンをdisable指定する」という要件を叶えたい場合は、EditTextPreferenceを継承しつつコードを書くことで、(やや強引気味ではあったのですが)対応することが出来ました。 以下のように記述できるのは、DialogPreferenceでDialogの生成を行っているためです。

ところがPreference Support Libraryでは、DialogPreferenceクラスでDialogの生成を行わない実装になっています。 その代わり、タイトルやメッセージなどの他、Dialog内部に表示したいlayoutのIDを受け取り保持するようになっています。 (android.support.v7.preference.DialogPreference)

DialogはPreferenceFragmentCompat#onDisplayPreferenceDialog(Preference preference)で生成される

ではこれまでPreferenceで生成されていたDialogは、どこで生成されるようになったのでしょうか? 答えは、PreferenceFragmentCompat#onDisplayPreferenceDialog(Preference preference)です。

    /**
     * Called when a preference in the tree requests to display a dialog. Subclasses should
     * override this method to display custom dialogs or to handle dialogs for custom preference
     * classes.
     *
     * @param preference The Preference object requesting the dialog.
     */
    @Override
    public void onDisplayPreferenceDialog(Preference preference) {

        boolean handled = false;
        if (getCallbackFragment() instanceof OnPreferenceDisplayDialogCallback) {
            handled = ((OnPreferenceDisplayDialogCallback) getCallbackFragment())
                    .onPreferenceDisplayDialog(this, preference);
        }
        if (!handled && getActivity() instanceof OnPreferenceDisplayDialogCallback) {
            handled = ((OnPreferenceDisplayDialogCallback) getActivity())
                    .onPreferenceDisplayDialog(this, preference);
        }

        if (handled) {
            return;
        }

        // check if dialog is already showing
        if (getFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) {
            return;
        }

        final DialogFragment f;
        if (preference instanceof EditTextPreference) {
            f = EditTextPreferenceDialogFragmentCompat.newInstance(preference.getKey());
        } else if (preference instanceof ListPreference) {
            f = ListPreferenceDialogFragmentCompat.newInstance(preference.getKey());
        } else {
            throw new IllegalArgumentException("Tried to display dialog for unknown " +
                    "preference type. Did you forget to override onDisplayPreferenceDialog()?");
        }
        f.setTargetFragment(this, 0);
        f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
    }

JavaDocになにやら書いてありますね。「PreferenceツリーにあるPreferenceがダイアログの表示をリクエストするとコールされます。カスタムダイアログの表示またはカスタムPreferenceのDialogをハンドルするときはサブクラスでこのメソッドをオーバーライドすべきです。」という具合でしょうか。 前半のboolean handledが絡む箇所についてはよく分かりませんが、PreferenceFragmentCompat#getCallbackFragment()の返り値(Fragment)やActivityでもダイアログの制御のためにinterfaceをimplementすることも許されているようです。

ちなみに「Called when a preference in the tree requests to display a dialog.」の部分は以下のようにメソッドがコールされていきます。

  1. DialogPreference#onClick()
  2. PreferenceManager#showDialog(Preference preference)
  3. PreferenceFragmentCompat#onDisplayPreferenceDialog(Preference preference)

実装例

とりあえず以下のように実装してみました。PreferenceFragmentCompatの該当箇所の後半部分そのまんまですが(;´Д`)

public class MyPreferenceFragment extends PreferenceFragmentCompat {

    private static final String DIALOG_FRAGMENT_TAG = "com.example.setting.BasePreferenceFragment.DIALOG";

...

    @Override
    public void onDisplayPreferenceDialog(Preference preference) {
        if (getFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) {
            return;
        }

        final DialogFragment f;
        if (preference instanceof EditTextPreference) {
            f = MyDialogFragment.newInstance(preference.getKey());
            f.setTargetFragment(this, 0);
            f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
        } else {
            super.onDisplayPreferenceDialog(preference);
        }
    }
}

これで、EditTextPreferenceとこれを継承したPreferenceをクリック時に開かれるダイアログはすべてMyDialogFragmentになります。

詳細なカスタムダイアログの実装は android.support.v7.preference.EditTextPreferenceDialogFragmentCompat 等を参考にしてください 少し解説すると、newInstance()でkeyを、setTargetFragment()でPreferenceFragmentCompatを継承したFragmentを渡すことで、DialogFragment側からクリックされたPreferenceを参照するようにしてあるようです。

実のところ、Dialogの中身だけを変えたい(見た目を少しいじりたい、コンポーネント類に変更がない)だけの場合は、Preferenceのxmlでandroid:dialogLayoutをセットするだけです。 しかし、「自分で定義したコンポーネントの値を使用したい」「ダイアログ自体の制御をしたい」という場合は、このような処理を実装する必要があるのではないかと思います。

まとめ

Preference Support Libraryを使いこなし、良いアプリを作っていけたらいいですね!