TL;DR
- AppCompatActivityはもちろん、PreferenceActivityに載せても動かない
- 自前でハンドリングする必要があり、以下のいずれかの手法を取る必要がある。
- ActivityにOnPreferenceStartScreenCallback や OnPreferenceStartFragmentCallback を実装させる (Activity起動 or View使い回し)
- PreferenceFragmentCompat#onNavigateToScreen(PreferenceScreen) をoverrideする (上記をPreferenceFragmentCompat上で処理する用)
- boolean PreferenceFragmentCompat#onPreferenceTreeClick(Preference) をoverrideする (Fragment起動用)
PreferenceFragmentCompat
Support Library v23から導入された android.support.v4.Fragment をベースにしたPreferenceFragment。
3rd party ライブラリを使って凌いでいた人も多かったはず。
そして僕が気付いたのはv24.2.0なので今更感がある。
PreferenceScreenによる遷移が自動では処理されない問題
今までのPreferenceと同様、xmlによる構築が可能。
xml内にルートでないPreferenceScreenがある場合、そのPreferenceScreenをクリックすると新しい画面が開かれる仕様となっている。
公式リファレンスを見るとxmlにPreferenceScreenが書かれており、その子要素の注釈として、Next screenで表示されるものを記述できるぞ!と書いてある。
ということで、PreferenceFragmentCompatでもやってみたが、画面が開かれない・・・
でもIntentを記述するとちゃんと起動するので、click処理は諸々走っている様子。
A PreferenceScreen object should be at the top of the preference hierarchy. Furthermore, subsequent PreferenceScreen in the hierarchy denote a screen break -- that is the preferences contained within subsequent PreferenceScreen should be shown on another screen. The preference framework handles this by calling onNavigateToScreen(PreferenceScreen).
こうも書いてあるけど、これで「自分でハンドリングして」と読むのはちょっと厳しくない・・・?
とりあえず onNavigateToScreen はいつ呼ばれるのかというと、PreferenceScreen#onClick()
で処理されていた。
@Override protected void onClick() { if (getIntent() != null || getFragment() != null || getPreferenceCount() == 0) { return; } final PreferenceManager.OnNavigateToScreenListener listener = getPreferenceManager().getOnNavigateToScreenListener(); if (listener != null) { listener.onNavigateToScreen(this); } }
ちなみに PreferenceManager#getOnNavigateToScreenListener()
はPreferenceFragmentCompat自身を返し、それは OnNavigateToScreenListener
を実装している。
うーん、いやしかし、これを見ると中のListenerを呼ぶ条件は満たしている。
とりあえず、Intentは別の部分で処理されていると考えて良さそうで、分けて考えて良さそう。
一個一個自分でclick listenerをbindしてもいいけど・・・それは根本解決ではないし、今後困ってしまうので探してみる。
まずPreferenceScreenをクリックしたときに呼ばれる上述のlistenerの中身は以下のようになっている.(v24.2.0)
// in PreferenceFragmentCompat.java /** * Called by * {@link android.support.v7.preference.PreferenceScreen#onClick()} in order to navigate to a * new screen of preferences. Calls * {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback#onPreferenceStartScreen} * if the target fragment or containing activity implements * {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback}. * @param preferenceScreen The {@link android.support.v7.preference.PreferenceScreen} to * navigate to. */ @Override public void onNavigateToScreen(PreferenceScreen preferenceScreen) { boolean handled = false; if (getCallbackFragment() instanceof OnPreferenceStartScreenCallback) { handled = ((OnPreferenceStartScreenCallback) getCallbackFragment()) .onPreferenceStartScreen(this, preferenceScreen); } if (!handled && getActivity() instanceof OnPreferenceStartScreenCallback) { ((OnPreferenceStartScreenCallback) getActivity()) .onPreferenceStartScreen(this, preferenceScreen); } }
つまり以下の条件のうち一方を満たす必要がある。
- PreferenceFragmentCompat#callbackFragment is an instance of
OnPreferenceStartScreenCallback
- PreferenceFragmentCompat#activity is an instance of
OnPreferenceStartScreenCallback
で、じゃあそこらへんどうなってるんだっけ・・・っていうと
- PreferenceFragmentCompat#getCallbackFragment() は常にnullを返す
- Preference用のCompatActivityはないのでAppCompatActivityを使っていたが、当然実装してない
何もしてないやんけ。自前でハンドリングが必要だなぁ・・・。
とここまでやったところで記述を見つける。
・・・うぇい!
調べたら onPreferenceTreeClick と OnPreferenceStartFragmentCallback
周りも同じ感じだった。
実装方法の検討
じゃあどれが一番いいんだろう、と。
このdocによると、Activityに実装させる方法が正攻法っぽい。
他のやり方でもできそうなので色々調べてみる。
PreferenceFragmentCompat#getCallbackFragment()
のJavadocを見てみると
/** * Basically a wrapper for getParentFragment which is v17+. Used by the leanback preference lib. * @return Fragment to possibly use as a callback * @hide */
あー・・・なんか用途が違う。実現可能ではあるけれど、提供された目的と違う方法はあまり好ましくない。
なのでやるとしたら、まあ以下の通りになりそう。
- ActivityにOnPreferenceStartScreenCallback や OnPreferenceStartFragmentCallback を実装させる
- onNavigateToScreen(PreferenceScreen) をoverrideする
- boolean onPreferenceTreeClick(Preference) をoverrideする
- getCallbackFragment() をoverrideして、OnPreferenceStartScreenCallback や OnPreferenceStartFragmentCallback を実装したFragmentを返すようにする (本来の用途と違うのでやめた方がいい)
実装にあたって
気をつけないといけないことはいくつかあるが、PreferenceScreen
はParcelableでもなんでもないということを念頭に置く必要がある。
つまりそのPreferenceScreenをそのまま新しく作成するPreferenceFragmentCompatのインスタンスに渡して云々・・・は出来ない。
備忘録も兼ねて、いくつか思いついた実装を書いておく。
OnPreferenceStartScreenCallback
- Fragment, Intentが設定されておらず、そのPreferenceScreenは1個以上の子要素を持つことが保証されている。
xmlに書いたPreferenceScreen(子孫)に表示したい全ての要素が記述してあるとき
PreferenceFragmentCompatを再利用する
boolean onPreferenceStartScreen(caller: PreferenceFragmentCompat, pref: PreferenceScreen) { if (pref.getKey() == null) { return false; } caller.setPreferencesFromResource(R.xml.setting, pref.key); return true; }
どこからか、利用されているxmlのidを拾ってくる必要はあるが、PreferenceFragmentCompatの使い回しができる。
けどback処理が面倒くさそう。
PreferenceFragmentCompatを新しく作る
class HogeFragment extends PreferenceFragmentCompat { static PreferenceFragmentCompat newInstance(String rootKey) { PreferenceFragmentCompat fragment = new PreferenceFragmentCompat(); Bundle bundle = new Bundle(); bundle.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, rootKey); fragment.setArguments(bundle); return fragment; } void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.setting, rootKey) } } // ↓ Activityで実装する boolean onPreferenceStartScreen(caller: PreferenceFragmentCompat, pref: PreferenceScreen) { if (pref.getKey() == null) { return false; } getSupportFragmentManager().beginTransaction().replace(R.id.container, HogeFragment.newInstance(pref.key)).addToBackstack(null).commit(); return true; }
どんなFragmentで扱うかを知っている必要がある。Fragment attributeを設定してしまうと、ここが呼ばれないことを留意する必要がある。
PreferenceScreenのkeyとxmlのid名を対応させる
boolean onPreferenceStartScreen(caller: PreferenceFragmentCompat, pref: PreferenceScreen) { if (pref.getKey() == null) { return false; } int id = getResource().getIdentifier(pref.getKey(), "xml", getPackageName()); caller.addPreferencesFromResource(id); // lint抑制をする return true; }
ゴリ押し。PreferenceFragmentCompatの使い回しができる。けどやっぱりback処理が面倒くさそう。
試してないけど。
OnPreferenceStartFragmentCallback
- Fragmentが設定されていることが保証されている。
Fragment名から愚直に作成する
boolean onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference) { if (!(pref instanceof ScreenPreference)) { return false; } String fragmentName = pref.getFragment(); // name に合わせてFragmentを生成して色々する return true; }
普通っぽい。