创建 Android 设置界面 (第三部分)

原文:Building an Android Settings Screen (Part 3) 

创建 Android 设置界面 (第一部分) 

创建 Android 设置界面 (第二部分)

创建 Android 设置界面 (第三部分)   

创建 Android 设置界面 (第四部分) 

本教程的第一部分 我们已经探讨了Setting的创建和主题,第二部分我们修复了dialog的布局和主题会存在的问题。现在我们继续v7.preference library,学习如何自定义一个preference。

理解这个库是如何工作的

鉴于v7.preference库只提供了4个基本的preference(如果包含了v14.preference库的话是5个),你很可能需要一个自定义的preference。但是在写代码之前你需要知道该做些什么。所以我们先看看 v7.preference 库的结构,以了解它的工作原理。你应该仔细阅读下面的内容,它阐述了后面会用到的基础知识。(如果你想自己去研究这些知识,可以 在这里 找到源码)。我将只关注重要的问题。

这个库的结构是怎样的

1-uuP5q8sh_DTt1a-2dmZkBg.png

如图所示,有四个主要的类:

  • PreferenceFragmentCompat: 这是设置主界面的fragment(注意这是一个抽象类,因此不可以直接实例化,但是可以被继承)。

  • Preference: 这是显示在设置街面上的基类preference,任何预定义的preference都继承(间接)自这个类。

  • PreferenceDialogFragmentCompat: 一个preference的对话框基类。所有preference的对话框都继承自这个类。(注意这是一个抽象类,不能实例化,但是可以继承然后实例化。是的,这可能是最长的命名之一了)

  • PreferenceManager:它提供了通向SharedPreferences的通道,PreferenceFragmentCompat 和所有属于它的Preference都共享同一个PreferenceManger。

Preference分为两种类型:TwoStatePreference,只能存储和切换布尔值;以及DialogPreference,用户可以与之交互。(注意这两个类也是抽象的)。

对话框是如何打开的

dialog类与和它相关的DialogPreference类是分开的。比如,EditTextPreference和它的相关的对话框EditTextDialogFragmentCompat在两个不同的类中。因此必须在某个地方明确打开对话框。当我们阅读DialogPreference源码的时候( 可以在 这里 找到),可以发现下面的代码片段。

@Override
protected void onClick() {
    getPreferenceManager().showDialog(this);
}

同时在PreferenceManager中我们可以找到下面的代码片段。

public void showDialog(Preference preference) {
    if (mOnDisplayPreferenceDialogListener != null) {
        mOnDisplayPreferenceDialogListener
            .onDisplayPreferenceDialog(preference);
    }
}
...
public interface OnDisplayPreferenceDialogListener {
    void onDisplayPreferenceDialog(Preference preference);
}

它告诉我们,如果点击了一个DialogPreference,它就调用PreferenceManger中的一个方法来显示这个preference的对话框。然后PreferenceManager把调用转接给了一个注册的Listener。PreferenceFragmentCompat implement了PreferenceManager提供的这个interface,因此它可以把自己注册为这个dialog的Listener。

总的来说就是,当点击了一个DialogPreference后,事件最终传到了PreferenceFragmentCompat的onDisplayPreferenceDialog(Preference preference) 方法中,因此我们需要重写它来打开一个自定义的dialog。

构建一个自定的Preference

这里我打算用创建一个自定义的TimePreference为例。它将打开一个选择时间的对话框。

1-FQYSvF0GgACtrdFb4uGdvQ.png

当我们想创建的自定义preference和已有的某个preference相似的时候,你可以继承并修改已有的preference。比如,如果你想要一个NumberPreference,你可以继承EditTextPreferenceand并修改之,它只允许用户输入数字。我这里使用的方法是直接继承DialogPreference。

构建对话框的布局

新建名为pref_dialog_time.xml的资源文件作为对话框的布局。TimePicker是该对话框所需的唯一控件。因此把它作为根view添加到布局文件中。然后我们应用本教程第二部分中修改的主题(最后三行)。

<?xml version="1.0" encoding="utf-8"?>
<TimePicker
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/edit"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="@dimen/alert_def_padding"
    android:paddingBottom="@dimen/alert_def_padding"
    android:theme="@style/AppAlertDialogContent" />

别忘了添加id edit,否则app会崩溃。

构建我们的Preference

现在可以创建我们的自定义preference了。因为我们想让preference打开一个带TimePicker的对话框,所以需要创建一个继承了DialogPreference的类:TimePreference。

import android.support.v7.preference.DialogPreference;
public class TimePreference extends DialogPreference {
    ...
}

完了之后,我们就可以添加preference的逻辑了。首先从TimePreference所需的全局变量开始。对话框中的TimePicker可以提供整数类型的小时和分钟。为了把这个值保存在一个SharedPreference中,我决定把时间转换成分钟。我还决定把对话框的布局的id也存在一个全局变量中。在TimePreference中添加:

private int mTime;
private int mDialogLayoutResId = R.layout.pref_dialog_time;

现在转向构造函数。我们先从参数最少的构造函数开始,缺省的参数用默认值填充,然后逐渐到全参数的构造函数。下面是TimePreference类:

public TimePreference(Context context) {
    this(context, null);
}
public TimePreference(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}
public TimePreference(Context context, AttributeSet attrs,
        int defStyleAttr) {
    this(context, attrs, defStyleAttr, defStyleAttr);
}
public TimePreference(Context context, AttributeSet attrs,
        int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    // Do custom stuff here
    // ...
    // read attributes etc.
}

:第二个构造函数中的0替换成R.attr.dialogPreferenceStyle(对于DialogPreference)或者R.attr.preferenceStyle(For any other preference)并不会出现什么问题。感谢Ivan Soriano

然后我们需要两个方法。一个用于把时间保存到SharedPreference,另一个用于读取当前的数据。一会儿我们要从dialog中调用这些方法。在TimePreference类中添加下面的代码:

public int getTime() {
    return mTime;
}
public void setTime(int time) {
    mTime = time;
    // Save to Shared Preferences
    persistInt(time);
}

现在我们需要重写一些方法。首先我们需要一个方法读取默认的值(我们可以在 xml/app_preferences.xml中使用android:defaultValue属性来定义默认值)。第二个方法从SharedPreference读取存储的值并保存到mTime变量,在TimePreference类中添加下面的代码:

@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
    // Default value from attribute. Fallback value is set to 0.
    return a.getInt(index, 0);
}
@Override
protected void onSetInitialValue(boolean restorePersistedValue,
        Object defaultValue) {
    // Read the value. Use the default value if it is not possible.
    setTime(restorePersistedValue ?
        getPersistedInt(mTime) : (int) defaultValue);
}

最后要做的一件事就是为dialog设置layout resource。为此重写getDialogLayoutResource方法。在TimePreference类中添加下面的代码:

@Override
public int getDialogLayoutResource() {
    return mDialogLayoutResId;
}

Building the Dialog

下面的图片是为了提醒你我们要达成的效果。以防在经过这么多代码的解释之后你都忘了长啥样了。

1-FQYSvF0GgACtrdFb4uGdvQ (2).png

现在让我们来创建一个图片中的dialog

如果你仔细阅读就应该知道 所有的 preference dialog都继承自一个名为PreferenceDialogFragmentCompat的类。所以我们创建一个继承它的名为TimePreferenceFragmentCompat的类。

import android.support.v7.preference.PreferenceDialogFragmentCompat;
public class TimePreferenceDialogFragmentCompat
        extends PreferenceDialogFragmentCompat {
   ...
}

我们并不需要一个专门的构造方法,但是需要一个创建TimePreferenceFragmentCompat实例的静态方法。为了知道对话框属于哪个preference,我们在方法中添加了一个String参数表示preference的key,然后通过Bundle传递给dialog。后面我们将用到这个方法。在TimePreferenceFragmentCompat类中添加如下代码:

public static TimePreferenceDialogFragmentCompat newInstance(
        String key) {
    final TimePreferenceDialogFragmentCompat
            fragment = new TimePreferenceDialogFragmentCompat();
    final Bundle b = new Bundle(1);
    b.putString(ARG_KEY, key);
    fragment.setArguments(b);
    return fragment;
}

现在我们需要处理TimePicker。我们希望它显示的总是存储在SharedPreference中的时间。在onBindDialogView方法中通过布局得到TimePicker。然后用getPreference方法得到打开对话框的preference:

@Override
protected void onBindDialogView(View view) {
    super.onBindDialogView(view);
    mTimePicker = (TimePicker) view.findViewById(R.id.edit);
    // Exception when there is no TimePicker
    if (mTimePicker == null) {
        throw new IllegalStateException("Dialog view must contain" +
                " a TimePicker with id 'edit'");
    }
    // Get the time from the related Preference
    Integer minutesAfterMidnight = null;
    DialogPreference preference = getPreference();
    if (preference instanceof TimePreference) {
        minutesAfterMidnight =
                ((TimePreference) preference).getTime();
    }
    // Set the time to the TimePicker
    if (minutesAfterMidnight != null) {
        int hours = minutesAfterMidnight / 60;
        int minutes = minutesAfterMidnight % 60;
        boolean is24hour = DateFormat.is24HourFormat(getContext());
        mTimePicker.setIs24HourView(is24hour);
        mTimePicker.setCurrentHour(hours);
        mTimePicker.setCurrentMinute(minutes);
    }
}

对话框还需要做的最后一件事是点击ok按钮保存选择的时间。为此我们重写onDialogClosed方法。先计算想保存的分钟数,然后得到相关的preference并调用定义在它里面的setTime方法。在TimePreferenceFragmentCompat中添加下面的代码:

@Override
public void onDialogClosed(boolean positiveResult) {
    if (positiveResult) {
        // generate value to save
        int hours = mTimePicker.getCurrentHour();
        int minutes = mTimePicker.getCurrentMinute();
        int minutesAfterMidnight = (hours * 60) + minutes;
        // Get the related Preference and save the value
        DialogPreference preference = getPreference();
        if (preference instanceof TimePreference) {
            TimePreference timePreference =
                    ((TimePreference) preference);
            // This allows the client to ignore the user value.
            if (timePreference.callChangeListener(
                    minutesAfterMidnight)) {
                // Save the value
                timePreference.setTime(minutesAfterMidnight);
            }
        }
    }
}

对话框总算完成了。

打开对话框

离正常工作只剩最后一件事了。如果你读了第一部分,你就该知道必须在某个地方明确的调用对话框。这个地方就是PreferenceFragmentCompat的onDisplayPreferenceDialog方法。现在到我们的SettingsFragment类(它继承了PreferenceFragmentCompat)中。我首先判断想打开对话框的Preference是不是我们自定义的preference之一。如果是,我们创建一个相关的对话框(并传入preference key )并打开之。如果不是,我们直接调用父类的这个方法,这样就能处理预定义的DialogPreference。

@Override
public void onDisplayPreferenceDialog(Preference preference) {
    // Try if the preference is one of our custom Preferences
    DialogFragment dialogFragment = null;
    if (preference instanceof TimePreference) {
        // Create a new instance of TimePreferenceDialogFragment with the key of the related
        // Preference
        dialogFragment = TimePreferenceDialogFragmentCompat
                .newInstance(preference.getKey());
    }
    // If it was one of our cutom Preferences, show its dialog
    if (dialogFragment != null) {
        dialogFragment.setTargetFragment(this, 0);
        dialogFragment.show(this.getFragmentManager(),
                "android.support.v7.preference" +
                ".PreferenceFragment.DIALOG");
    }
    // Could not be handled here. Try with the super method.
    else {
        super.onDisplayPreferenceDialog(preference);
    }
}

把它添加到Settings Screen

现在我们终于有了自己的preference了。你可以把它添加到xml/app_preferences.xml,如下:

<your.package.TimePreference
    android:key="key4"
    android:title="Time Preference"
    android:summary="Time Summary"
    android:defaultValue="90" />

打开之后是这样的...

1-KCNdY9_8t_FZ3HeQrqdTlQ.png

等等,什么情况?为什么应用了part 1 和 part 2 中所提到的修复之后还是存在问题?

修复Layout 和 Design

幸运的是,解决办法相当简单-因为有我告诉你嘛。

在styles.xml 中添加两个新的style。第一个AppPreference将把Settings界面上的preference的布局变成material design的。第二个AppPreference.DialogPreference继承前一个,并定义对话框上的按钮的文字。

<!-- Style for an Preference Entry -->
<style name="AppPreference">
    <item name="android:layout">@layout/preference_material</item>
</style>
<!-- Style for a DialogPreference Entry -->
<style name="AppPreference.DialogPreference">
    <item name="positiveButtonText">@android:string/ok</item>
    <item name="negativeButtonText">@android:string/cancel</item>
</style>

完了之后,你就可以把上面的style添加到你自定义的preference中了。对于继承了DialogPreference的自定义preference可以把style设置为AppPreference.DialogPreference。对于其他preference可以使用AppPreference样式。然后问题基本就解决了。

<your.package.TimePreference
    android:key="key4"
    android:title="Time Preference"
    android:summary="Time Summary"
    android:defaultValue="90"
    style="@style/AppPreference.DialogPreference" />

现在的效果是这样的:

1-JjknpvGZAZfNxjGo6Ywv9Q (1).png

现在你应该知道如何自定义preference了,希望这部分的内容没犯什么错误。

我推荐你常看 Android Developers 网站 这里, 以及 v7.preference library的源代码,这里

你可以在GitHub上看到这个项目。

来自:设置(Settings)