androidx.preference の 1.2.0 から PreferenceHeaderFragmentCompat
が使えるようになっています。
2ペイン対応の設定画面を簡単に作ることができます。
画面の幅が一定を超えると2ペインモード、一定以下なら1ペインモードに切り替わります。
Foldableデバイスの開閉や、タブレットの画面回転などに応じて、幅を有効活用できるレイアウトに切り替えることができます。
Deprecatedになってしまった PreferenceActivity でできていたことですが、これがモダンなライブラリを使って実現できるようになっています。
2ペインモード
タブレット
Surface Duo の場合は綺麗に2画面を使ってくれます。
1ペインモード
使い方
従来のPreferenceFragmentCompatでも設定画面の階層化だけはできていました。
SettingsActivityをテンプレートから作成するときの、「Split setting hierarchy into sub-screen」にチェックを入れると作成されます。
このときヘッダーに当たる部分は以下のようなxmlになっていて、Preference
にapp:fragment
で下階層のFragmentを指定します。
<PreferenceScreen
xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:fragment="net.mm2d.myapplication.SettingsActivity$MessagesFragment"
app:icon="@drawable/messages"
app:key="messages_header"
app:title="@string/messages_header"
/>
<Preference
app:fragment="net.mm2d.myapplication.SettingsActivity$SyncFragment"
app:icon="@drawable/sync"
app:key="sync_header"
app:title="@string/sync_header"
/>
</PreferenceScreen>
PreferenceHeaderFragmentCompat
の最低限の実装では、onCreatePreferences
を実装し、上記XMLをinflateするPreferenceFragmentCompatを返します。
class Header : PreferenceHeaderFragmentCompat() {
override fun onCreatePreferenceHeader(): PreferenceFragmentCompat = HeaderFragment()
}
class HeaderFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.header_preferences, rootKey)
}
}
あとはこのFragmentを配置するだけで、あとはPreferenceHeaderFragmentCompatが子Fragmentで画面遷移を制御してくれます。
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.settings_activity)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, Header())
.commit()
}
}
}
NavigationUp の操作を接続する
最低限の実装をしただけで、メニューのタップとバックキーによる画面遷移が動作します。
ただ、Toolbarの「←」ボタン、いわゆるNavigationUPが動作しません。
PreferenceHeaderFragmentCompatはOnBackPressedCallbackでバックキーを受け取り、UP動作を行っているため、ActivityでonSupportNavigateUp
を override して onBackPressed() を呼ぶことで動作をつなげることができます。
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
……まあ、動くといえば、動くんだけど、NavigationUPの実装で、onBackPressedコールするってなんか負けた気がするよね。
PreferenceHeaderFragmentCompat は slidingPaneLayout にアクセスできるようになっているので、closePane()
をコールすることでHeaderに戻し、戻れない場合は戻り値で判定することができるので、こっちの方がそれっぽいでしょうか?
override fun onSupportNavigateUp(): Boolean {
if ((supportFragmentManager.findFragmentById(R.id.settings) as Header)
.slidingPaneLayout.closePane()) {
return true
}
return super.onSupportNavigateUp()
}
ただまあ、もうちょっとスマートに実装できないものかと思いますが、良い方法が見つかりません。良い方法があれば教えてください。
Toolbarタイトルを画面遷移に合わせて変更する
「Split setting hierarchy into sub-screen」のテンプレートではOnPreferenceStartFragmentCallbackをActivityに実装して、遷移先に応じてToolbarのタイトルを変更する実装が入っています。これはどのように実装すれば良いでしょうか?
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
// Instantiate the new Fragment
val args = pref.extras
val fragment = supportFragmentManager.fragmentFactory.instantiate(
classLoader,
pref.fragment
).apply {
arguments = args
setTargetFragment(caller, 0)
}
// Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction()
.replace(R.id.settings, fragment)
.addToBackStack(null)
.commit()
title = pref.title
return true
}
PreferenceHeaderFragmentCompat はそのchildFragmentとしてPreferenceFragmentCompatを配置しているため、実装はPreferenceHeaderFragmentCompat を継承したクラスで行います。
PreferenceHeaderFragmentCompat は OnPreferenceStartFragmentCallback を実装しているため、onPreferenceStartFragmentをoverrideすることで、同様に画面遷移を知ることができます。
ここでActivityのtitleを変更します。ただし、2ペインの場合は画面遷移を伴わないので、1ペインの場合だけ変更するようにします。
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
title = pref.title ?: ""
if (slidingPaneLayout.isSlideable) {
requireActivity().title = title
}
return super.onPreferenceStartFragment(caller, pref)
}
これで、子フラグメントへの遷移での変更は可能になりましたが、Headerに戻った場合に戻してあげたいです。
これはslidingPaneLayoutのPanelSlideListenerで検出します。slidingPaneLayoutは、onViewCreated以降でないとアクセスできないので、onViewCreatedでListenerを設定してみましょう。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
slidingPaneLayout.addPanelSlideListener(object : PanelSlideListener {
override fun onPanelSlide(panel: View, slideOffset: Float) = Unit
override fun onPanelOpened(panel: View) = Unit
override fun onPanelClosed(panel: View) {
title = getText(R.string.title_activity_settings)
requireActivity().title = title
}
})
}
最後に、画面回転やFoldableの開閉などでモードが変わってもタイトルを引き継げるようにしておきます。
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
if (savedInstanceState != null) {
title = savedInstanceState.getCharSequence(TITLE_TAG) ?: ""
slidingPaneLayout.doOnLayout {
if (slidingPaneLayout.isSlideable) {
requireActivity().title = title
} else {
requireActivity().title = getText(R.string.title_activity_settings)
}
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (title.isEmpty()) {
title = getText(R.string.title_activity_settings)
}
outState.putCharSequence(TITLE_TAG, title)
}
SettingsActivityの実装全体としては以下のような感じです。
private const val TITLE_TAG = "settingsActivityTitle"
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.settings_activity)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, Header())
.commit()
}
}
override fun onSupportNavigateUp(): Boolean {
if ((supportFragmentManager.findFragmentById(R.id.settings) as Header)
.slidingPaneLayout.closePane()) {
return true
}
return super.onSupportNavigateUp()
}
class Header : PreferenceHeaderFragmentCompat() {
private var title: CharSequence = ""
override fun onCreatePreferenceHeader(): PreferenceFragmentCompat {
return HeaderFragment()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
slidingPaneLayout.addPanelSlideListener(object : PanelSlideListener {
override fun onPanelSlide(panel: View, slideOffset: Float) = Unit
override fun onPanelOpened(panel: View) = Unit
override fun onPanelClosed(panel: View) {
title = getText(R.string.title_activity_settings)
requireActivity().title = title
}
})
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
if (savedInstanceState != null) {
title = savedInstanceState.getCharSequence(TITLE_TAG) ?: ""
slidingPaneLayout.doOnLayout {
if (slidingPaneLayout.isSlideable) {
requireActivity().title = title
} else {
requireActivity().title = getText(R.string.title_activity_settings)
}
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (title.isEmpty()) {
title = getText(R.string.title_activity_settings)
}
outState.putCharSequence(TITLE_TAG, title)
}
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
title = pref.title ?: ""
if (slidingPaneLayout.isSlideable) {
requireActivity().title = title
}
return super.onPreferenceStartFragment(caller, pref)
}
}
class HeaderFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.header_preferences, rootKey)
}
}
class MessagesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.messages_preferences, rootKey)
}
}
class SyncFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.sync_preferences, rootKey)
}
}
}
以上、PreferenceHeaderFragmentCompatの使い方でした。
使ってみた感想としては、見落としているだけかもしれませんが、もう少しカスタマイズ性が欲しい気がしますが、簡単に2ペインの設定画面が作れるので、デザイン性とかにそれほどこだわらないのであれば手軽に利用できるのではないでしょうか。
まだできたばかりでドキュメントもあまりなさそうで、コードを読みながら手探りで使い方を調べたので、不適切な説明になっている場所があるかもしれません。よりよい方法などありましたらご指摘いただければと思います。