【Android】PreferenceFragmentCompatでよく見る設定画面を作る

[PreferenceFragmentCompat](https://developer.android.com/guide/topics/ui/settings/organize-your-settings?hl=ja)、とても便利ですが痒いところに手が届きません。特にこのような設定レイアウトはよく見ますが、標準では用意されていません。


|完成図|参考レイアウト（Twitter）|
|:---:|:---:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/985195/828307bb-e609-b342-64fa-2041af082036.jpeg)|![IMG_20201231_122559.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/985195/8c0edc9c-6a4e-a108-39b1-500a3da07348.png)|

# 実装
Android Studioに用意されているテンプレート`Setting Activity`を元に作っていきます。

### 1. 土台となるコード
PreferenceFragmentCompatはxmlで背景色を指定する方法がありませんので、`setBackgroundColor`を使用して背景色を変更します。

```kotlin:SettingsFragment.kt
package com.sakusaku.test

import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.PreferenceFragmentCompat

class SettingsFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.root_preferences, rootKey)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = super.onCreateView(inflater, container, savedInstanceState)
        // 背景色の設定
        view?.setBackgroundColor(Color.parseColor("#F5F5F5"))
        return view
    }
}
```

次に設定画面のレイアウトを作成します。

```xml:res/layout/root_preferences.xml
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <ListPreference
        android:defaultValue="online"
        android:entries="@array/entries_preference_status"
        android:entryValues="@array/values_preference_status"
        android:key="preference_status"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_status"
        app:useSimpleSummaryProvider="true" />

    <ListPreference
        android:defaultValue="short"
        android:entries="@array/entries_preference_scan_period"
        android:entryValues="@array/values_preference_scan_period"
        android:key="preference_scan_period"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_scan_period"
        app:useSimpleSummaryProvider="true" />

    <PreferenceScreen
        android:layout="@layout/preference_diver"
        android:selectable="false" />

    <PreferenceScreen
        android:key="preference_notice"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_notice" />

    <PreferenceScreen
        android:key="preference_about"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_about" />

    <PreferenceScreen
        android:key="preference_send_feedback"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_send_feedback" />

    <PreferenceScreen
        android:key="preference_privacy_policy"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_privacy_policy" />

    <PreferenceScreen
        android:key="preference_oss_license"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_oss_license" />
</PreferenceScreen>
```

リソース等は下記の通りです。

```xml:res/values/string.xml
<resources>
    <string name="preference_status">ステータスを設定</string>
    <string name="preference_scan_period">スキャンの間隔</string>
    <string name="preference_notice">お知らせ</string>
    <string name="preference_about">このアプリについて</string>
    <string name="preference_send_feedback">フィードバックを送る</string>
    <string name="preference_privacy_policy">プライバシーポリシー</string>
    <string name="preference_oss_license">オープンソースライセンス</string>
</resources>
```

```xml:res/values/array.xml
<resources>
    <string-array name="entries_preference_status">
        <item>オンライン</item>
        <item>オフライン</item>
    </string-array>
    <string-array name="values_preference_status">
        <item>online</item>
        <item>offline</item>
    </string-array>
    <string-array name="entries_preference_scan_period">
        <item>短い</item>
        <item>中間</item>
        <item>長い</item>
    </string-array>
    <string-array name="values_preference_scan_period">
        <item>short</item>
        <item>middle</item>
        <item>long</item>
    </string-array>
</resources>
```

### 2. `PreferenceScreen`と`ListPreference`用のカスタムレイアウトを作成
Preferenceには標準で透過色が設定されているため、白の背景色を設定したカスタムレイアウトを作成します。
元のレイアウトは[AOSP](https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/res/res/layout/preference_material.xml)上に公開されています。

```xml:res/layout/custom_preference.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at
          http://www.apache.org/licenses/LICENSE-2.0
     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<!-- Layout for a Preference in a PreferenceActivity. The
     Preference is able to place a specific widget for its particular
     type in the "widget_frame" layout. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white"
    android:baselineAligned="false"
    android:clipToPadding="false"
    android:foreground="?attr/selectableItemBackground"
    android:gravity="center_vertical"
    android:minHeight="?android:attr/listPreferredItemHeightSmall"
    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">

    <LinearLayout
        android:id="@+android:id/icon_frame"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="-4dp"
        android:gravity="start|center_vertical"
        android:minWidth="60dp"
        android:orientation="horizontal"
        android:paddingStart="0dp"
        android:paddingTop="4dp"
        android:paddingEnd="12dp"
        android:paddingBottom="4dp">

        <com.android.internal.widget.PreferenceImageView
            android:id="@+android:id/icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:maxWidth="48dp"
            android:maxHeight="48dp" />
    </LinearLayout>

    <RelativeLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:paddingTop="16dp"
        android:paddingBottom="16dp">

        <TextView
            android:id="@+android:id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ellipsize="marquee"
            android:singleLine="true"
            android:textAppearance="?android:attr/textAppearanceListItem" />

        <TextView
            android:id="@+android:id/summary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+android:id/title"
            android:layout_alignStart="@+android:id/title"
            android:ellipsize="end"
            android:maxLines="10"
            android:textAppearance="?android:attr/textAppearanceListItemSecondary"
            android:textColor="?android:attr/textColorSecondary" />
    </RelativeLayout>

    <LinearLayout
        android:id="@+android:id/widget_frame"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:gravity="end|center_vertical"
        android:orientation="vertical"
        android:paddingStart="16dp"
        android:paddingEnd="0dp" />
</LinearLayout>
```

`android:id`が赤色に表示されますが問題ありません。

### 3. Preference同士の隙間（区切り）を表示
Preferenceの区切りごとに隙間を開けるカスタムレイアウトを作成します。

```xml:res/layout/preference_diver.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <View
        android:layout_width="match_parent"
        android:layout_height="16dp" />
</LinearLayout>
```

表示の際は`PreferenceScreen`に上記のレイアウトを追加します。`android:selectable="false"`を指定することでクリックを無効化出来ます。

```xml:res/layout/custom_preference.xml
<PreferenceScreen
        android:layout="@layout/preference_diver"
        android:selectable="false" />
```

`PreferenceScreen`を`PreferenceCategory`に置き換えることで影をつけることも可能です。
<img width="50%" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/985195/1e1e0729-b72a-d48d-d2cd-9949353b4466.jpeg">