创建 Android 设置界面 (第三部分)
原文:Building an Android Settings Screen (Part 3)
创建 Android 设置界面 (第三部分)
本教程的第一部分 我们已经探讨了Setting的创建和主题,第二部分我们修复了dialog的布局和主题会存在的问题。现在我们继续v7.preference library,学习如何自定义一个preference。
理解这个库是如何工作的
鉴于v7.preference库只提供了4个基本的preference(如果包含了v14.preference库的话是5个),你很可能需要一个自定义的preference。但是在写代码之前你需要知道该做些什么。所以我们先看看 v7.preference 库的结构,以了解它的工作原理。你应该仔细阅读下面的内容,它阐述了后面会用到的基础知识。(如果你想自己去研究这些知识,可以 在这里 找到源码)。我将只关注重要的问题。
这个库的结构是怎样的
如图所示,有四个主要的类:
-
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为例。它将打开一个选择时间的对话框。
当我们想创建的自定义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
下面的图片是为了提醒你我们要达成的效果。以防在经过这么多代码的解释之后你都忘了长啥样了。
现在让我们来创建一个图片中的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" />
打开之后是这样的...
等等,什么情况?为什么应用了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" />
现在的效果是这样的:
现在你应该知道如何自定义preference了,希望这部分的内容没犯什么错误。
我推荐你常看 Android Developers 网站 这里, 以及 v7.preference library的源代码,这里。
你可以在GitHub上看到这个项目。