设置(Settings)
应用程序通常包括允许用户修改应用程序的特性和行为的设置功能。例如,一些应用程序允许用户指定通知是否启用或指定多久使用云同步数据。如果你想要 为你的应用程序提供设置,你应该使用Android的Preference APIs来构建统一的接口。本章的主角就是Preference,下面先让我们看一下图5-1:
:
图5-1 这是android短信息应用程序的设置界面截图。它使用就是就是Preference
5.1 概述
相比使用View对象来构建用户接main,设置是构建Preference的子类。一个Preference对象是构建一个单一设置的一个部分。 每一个Preference作为一个item在list并为用户修改设置提供了适当的界面。例如,一个CheckBoxPreference创建一个用于 显示checkbox的list item,ListPreference创建一个选择列表来显示一个对话框的item。每一个Preference其实都以键值对的形式保存在你应用程序 的SharedPreferences文件中。当用户改变设置时,系统会更新SharedPreferences文件中的键值对。我们只需要读取文件中的 设置数据即可。SharedPreferences支持以下数据类型的保存:
Boolean
Float
Int
Long
String
String Set
因为你的应用程序设置界面是使用Preference对象构建的而不是view,你需要使用Activity或Fragment的子类来显示设置列表:
◆ 如果你的应用程序支持android 3.0以下版本,你必须使用PreferenceActivity类来构建。
◆ 如果高于或等于android 3.0版本,你可以使用PreferenceFragment。当然你屏幕如果足够大的话你还是可以使用PreferenceActivity创建双面板布局来显示多组设置
5.1.1 Preference
你应用中的每一个设置都代表一个Preference对象。每一个Preference的子类包含一组核心的属性,如允许你指定设置的标题和默认值 这样的属性。每一个子类也提供自己的属性和用户界面。就想上面图5-1那样,每一个设置都是List View中的一个item,也是一个Preference对象。常见的Preference如下:
CheckBoxPreference
用checkbox显示一个item的设置是否为打开或关闭。他保存的是boolean值,true表示选中
ListPreference
以单选按钮列表的形式打开一个对话框,保存的值能支持任意类型
EditTextPreference
使用EditText打开一个对话框。保存的值为一个String。
5.2 在XML中定义Preferences
虽然你可以在运行时实例化新的Preference对象,但你也可以在XML中用Preference层级对象来定义。使用XML定义设置是首选, 因为XML文件结构更容易阅读的并且更新也很简单。此外,你的应用程序的设置通常是预先确定的,但你仍然可以在运行时修改它们。每一个 Preference子类都能使用XML节点来匹配声明。如<CheckBoxPreference>。你必须在项目的res/xml目录下 保存这种XML文件。尽管你可以任意命名你的文件名字,但建议使用preferences.xml,方便以后识别自己写的东西。注意如果你想要为你的设置 创建多面板布局,那你需要为每一个fragment创建单独的XML文件。
根节点的XML文件必须是一个< PreferenceScreen >元素。在这个元素中你可以添加每个Preference。每个你添加在< PreferenceScreen >元素下的子节点显示为单一列表项的设置。如代码清单5-1所示:
代码清单5-1
在上面的例子中有一个CheckBoxPreference和一个ListPreference。这两个items包含以下三个属性:
◆android:key
这个属性是必须的,对于一个preferences 来说是一个持久的数据值。当在SharedPreferences中保存这个Setting值时这个指定唯一的key(一个字符串)是被系统使用的。但某 些特殊情况,如preferences是一个PreferenceCategory或 PreferenceScreen,或者是一个XML 中<Intent>调用时,又或者是一个Fragment显示时(用android:fragment属性),以上这些特殊情况下,这个 key就不是必须的了。
◆android:title
为设置提供了一个用户可见的名称。
◆android:defaultValue
这指定初始值,系统应该建立在SharedPreferences文件。你应该为所有设置提供一个默认值。
关于其他更多属性,请直接查看Preference文档。
图 5-2 根据title的设置分类1. 通过指定<PreferenceCategory>节点的分类 2. 通过使用android:title指定title分类.
当你的列表设置超过大约10项,您可能想通过添加标题定义分组设置或在一个单独的屏幕显示这些组。
5.2.1创建设置组(groups)
如果你列出10个或更多的设置,用户可能会有些头疼。这样我们就可以使用分组。以下有两种分组方法:
◆使用titles
◆使用subscreens
1. 使用titles
如果你想要根据标题来提供分界线,请使用PreferenceCategory如代码清单5-2所示:
...
代码清单5-2
2. 使用subscreens
如果你想要放置设置组到一个subscreen中,请使用PreferenceScreen如图5-3和代码清单5-3:
图 5-3
... ... ...
代码清单5-3
5.2.2使用Intents
某些情况下, 你可能想要一个preference item来打开不同的activity而不是设置屏幕,就像一个web浏览器来查看一个web页面。当用户选择一个preference item时可以调用Intent来启动。方法就是添加一个<intent>节点到<Preference>节点中。如代码清单 5-4所示:
代码清单5-4
你能使用以下属性创建隐式和显式的intents:
◆android:action
如同setAction()方法一样设置action
◆android:data
如同setData()方法一样设置data
◆android:mimeType
如同setType()方法一样设置MIME类型
◆android:targetClass
如同setComponent()方法一样设置组件类名
◆android:targetPackage
如同setComponent()方法一样设置组件包名
5.3 创建一个Preference Activity
为了在Acitivity中显示你的设置,你可以继承PreferenceActivity类。这是扩展于传统Activity的一个类,它基于 Preference对象层级来显示一个设置列表。当用户做出一个改变时PreferenceActivity能自动保存与每一个Preference相 关的设置。注意:如果在3.0或以上系统版本中,你应该使用PreferenceFragment。最重要的是要记住,你在onCreate()回调期间 没有加载一个视图的布局。而是调用addPreferencesFromResource()来添加你定义的XML文件。例如代码清单5-5所示:
public class SettingsActivity extends PreferenceActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); }}
代码清单5-5
只要用户修改preference,系统将改变保存到一个默认的SharedPreferences文件。
5.4 使用Preference Fragments
如果你在android3.0或更高版本上开发,你应该使用PreferenceFragment来显示Preference 对象列表。你不应该在使用PreferenceActivity了。因为Fragments提供更为灵活的应用程序结构如代码清单5-6所示:
public static class SettingsFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); } ...}
代码清单5-6
然后你能吧这个fragment添加到Activity,如代码清单5-7所示:
public class SettingsActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getFragmentManager().beginTransaction() .replace(android.R.id.content, new SettingsFragment()) .commit(); }}
代码清单5-7
注意:一个PreferenceFragment没有它自己的Context对象。如果你需要一个Content对象,你能调用 getActivity()方法。然而,当fragment没有附加到activity中或者activity声明周期结束时分离后,你使用 getActivity()返回的将是null。
5.5 Setting的默认值
你创建preferences可能是为你的应用程序定义一些重要的行为,所以当用户第一次打开你的应用程序时,为每一个Preference相关的 SharedPreferences 文件初始化默认值是必要的。首先你必须为每一个Preference对象指定一个默认值,你可以在XML文件中使用 android:defaultValue属性。例如代码清单5-8所示:
代码清单5-8
然后,在Main Activity里的onCreate()方法中调用一次setDefaultValues(),如代码清单5-9所示:
PreferenceManager.setDefaultValues(this, R.xml.advanced_preferences, false);
代码清单5-9
在onCreate()方法的最开始就可以执行此方法,因为可能你的界面需要依据默认值来设置一个属性。这个方法中有三个参数:
1.应用程序的Context
2.Preference XML资源ID
3.这个boolean表示是否多次设置默认值,当然大部分情况下默认值一般我们只需要设置一次,就传false即可
5.6 使用Preference Headers
在少数情况下,如用首次屏幕显示的时候,你可能想要让用户先设置一些配置属性。在android3.0或更高版本系统下,你可以使用新的“headers”功能来代替以前的subscreens的嵌套。使用headers步骤如下:
1. 单独的每组设置作为独立PreferenceFragment的实例。即,每组设置需要一个单独的XML文件。
2. 创建一个XML头文件,其中列出了每个设置组和声明这fragment包含相应的设置列表。
3. 扩展PreferenceActivity类来托管您的设置。
4. 实现onBuildHeaders()回调用来指定头文件。
一个很棒的好处是,PreferenceActivity使用这个设计自动给出了双栏布局如图5-4大屏幕上运行时。
即使你的应用程序支持Android 3.0以上的版本,你也可以使用PreferenceFragment来构建应用程序用于较新的设备
图 5-4 使用headers的双面板布局
1. headers使用一个xml heanders文件定义
2.每一组设置通过PreferenceFragment来定义,并且在<header>节点中指定
图 5-5 这是一个手机设备,当一个item选中时候,会调用PreferenceFragment
5.6.1创建headers文件
每一组设置你都可以在<preference-headers>跟节点中指定一个<header>节点,如代码清单5-10所示:
代码清单5-10
使用android:fragment属性,每一个header声明一个PreferenceFragment实例,当用户选择这个header时 就会打开这个PreferenceFragment。<extras>节点允许你通过键值对的形式传参,一般是使用Bundle。 Fragment通过调用getArguments()来得到参数。关于参数的用途比较常见的就是为每一个组重用相同的 PreferenceFragment子类并且使用参数还是制定你将要载入哪一个preferences XML文件。例如,下面是一个fragment,它被多个设置组重用,下面代码清单5-11中在XML中使用了<extra>节点,key为 “settings”:
public static class SettingsFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String settings = getArguments().getString("settings"); if ("notifications".equals(settings)) { addPreferencesFromResource(R.xml.settings_wifi); } else if ("sync".equals(settings)) { addPreferencesFromResource(R.xml.settings_sync); } }}
代码清单5-11
5.6.2显示headers
为了显示preference headers, 你必须实现onBuildHeaders()回调方法并且调用loadHeadersFromResource()如代码清单5-12所示:
public class SettingsActivity extends PreferenceActivity { @Override public void onBuildHeaders(Listtarget) { loadHeadersFromResource(R.xml.preference_headers, target); }}
代码清单5-12
当用户从headers列表中选择一个item时,系统会打开相关的PreferenceFragment。注意当使用preference headers时候,你的PreferenceActivity 子类不需要onCreate()方法,因为这个activity的任务仅仅是载入 headers而已。
5.6.3老版本中支持Preference header
如果你的应用程序既支持3.0以下的版本,也支持3.0以上的版本,要么3.0以上我们使用headers能提供双面板布局。低于3.0的版本我们 就可以添加额外的preferences XML但里面不是使用<header>而是使用<Preference>节点了。但每一个<Preference> 都发送一个intent到PreferenceActivity。例如让我们先看下3.0或以上版本的代码清单5-13中(res/xml /preference_headers.xml):
代码清单5-13
然后让我们再看下3.0以下版本的代码清单5-14中(res/xml/preference_headers_legacy.xml):
代码清单5-14
因为android3.0中支持<preference-headers>,仅在android3.0或更高版本中系统会在 PreferenceActivity中调用onBuildHeaders()方法。当然如果用户的系统不是3.0(HONEYCOMB)的你就必须调用 preference_headers_legacy.xml,然后调用addPreferencesFromResource()来载入xml文件。例 如代码清单5-15所示:
@Overridepublic void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { //低于3.0版本载入legacy preferences headers addPreferencesFromResource(R.xml.preference_headers_legacy); }} // 高于或等于3.0版本会调用此方法@Overridepublic void onBuildHeaders(Listtarget) { loadHeadersFromResource(R.xml.preference_headers, target);}
代码清单5-15
剩下的3.0以下版本就是通过处理Intent来识别哪个preference文件要被加载到Activity中来。所以需要检索intent的 action并与在preference XML<intent>下已知action字符串比较,如代码清单5-16所示:
final static String ACTION_PREFS_ONE = "com.example.prefs.PREFS_ONE";... @Overridepublic void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String action = getIntent().getAction(); if (action != null && action.equals(ACTION_PREFS_ONE)) { addPreferencesFromResource(R.xml.preferences); } ... else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { // 载入3.0以下版本的legacy preferences文件 addPreferencesFromResource(R.xml.preference_headers_legacy); }}
代码清单5-16
注意连续调用addPreferencesFromResource()会堆叠所有的preferences到一个单独的列表中,所以确保他只调用一次,把它写到else-if的条件分支下。
5.7 读取Preferences
默认的,所有你应用中的preferences会保存到一个文件中,你可以调用静态方法 PreferenceManager.getDefaultSharedPreferences()来获得你保存的preferences。它将返回一个 SharedPreferences对象包含所有你在PreferenceActivity中使用的Preference对象的键值对。例如,下面代码清 单5-17教你如何读取:
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);String syncConnPref = sharedPref.getString(SettingsActivity.KEY_PREF_SYNC_CONN, "");
代码清单5-17
5.7.1监听Preference的改变
有些情况下你可能想只要一个preferences改变你就想得到通知。当任意一个preferences发生改变时,为了取得一个回调,我们可以 实现SharedPreference.OnSharedPreferenceChangeListener这个接口并通过 SharedPreferences.registerOnSharedPreferenceChangeListener()来注册监听。这个接口只有 一个回调方法,就是onSharedPreferenceChanged()你很容易就找到。如代码清单5-18所示:
public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener { public static final String KEY_PREF_SYNC_CONN = "pref_syncConnectionType"; ... public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals(KEY_PREF_SYNC_CONN)) { Preference connectionPref = findPreference(key); // 为选中的值设置用户描述摘要。 connectionPref.setSummary(sharedPreferences.getString(key, "")); } }}
代码清单5-18
这个例子中,onSharedPreferenceChanged()方法会检测改变的设置是否为一个已知的preference key。如果是就会调用findPreference()来获得Preference对象,并且这是改变后的对象你可以做你想做的事情,这里我们设置了一 个摘要用于当用户选中时给出提示信息。其实这是一个比较好的方法,特别是多个被选中时,你可以通过现有的API让用户知道他们做了些什么并得到反馈。还有 请注意记得在Activity声明周期中的onPause()和 onResume()方法中注册于注销你的监听,如代码清单5-19所示:
@Overrideprotected void onResume() { super.onResume(); getPreferenceScreen().getSharedPreferences() .registerOnSharedPreferenceChangeListener(this);} @Overrideprotected void onPause() { super.onPause(); getPreferenceScreen().getSharedPreferences() .unregisterOnSharedPreferenceChangeListener(this);}
代码清单5-19
5.8 管理网络的使用
从Android4.0开始,系统的设置应用程序允许用户能看到他们的应用程序在前台和后台使用了多少网络数据。用户对于个别Apps能关闭使用后 台数据。为了避免用户关闭你的程序从后台访问数据的功能,你应该使用数据连接有效并允许用户通过你应用程序的设置来完善你应用程序的数据使用。例如你可能 允许用户控制你的APP多久同步一次数据,是否你的app仅在Wifi情况下才更新和下载,漫游情况下如何处理等。这样的好处是给用户更精准的控制你的程 序使用多少数据,有这样的精准控制,用户就不会在系统设置中直接把你的应用访问数据的功能给关掉。一旦你在PreferenceActivity 中添加 了必要的preferences来控制你App的数据,并养成了这种写程序的习惯,那接下来我很乐意给你说明一下,你应该在manifest文件中添加一 个intent filter名字为ACTION_MANAGE_NETWORK_USAGE,如代码清单5-20所示:
代码清单5-20
这个intent filter表明了这个activity告诉系统我能控制这个应用程序的数据使用。因此,当用户在设置应用中检查你app使用了多少数据时,一个App设 置按钮便可用了,你点击它会启动PreferenceActivity然后让用户在精确控制你的app数据使用情况。
5.9 构建自定义的Preference
Android框架包含各种各样的Preference子类允许你构建自己的UI。然而你可能发现一个设置没有好的内置方案,如一个number picker或date picker。在这种情况下你需要创建自定义的preference,你需要继承Preference类。当然扩展Preference类后有一些重要的 事情要做:
◆当用户选择设置的时候指定用户接口
◆在适当的情况下保存setting的值
◆当进入我们的View时,使用当前值或默认值初始化Preference
◆当被系统请求时,提供默认值
◆如果Preference提供它自己的UI(如一个对话框),保存和恢复状态并处理生命周期的改变 。
5.9.1指定用户界面
如果你直接扩展Preference类,当用户选择一个item时,你需要实现onClick()用来定义action。其实大部分情况下就是直接 继承的DialogPreference显示对话框的形式,这样简化的程序。如果你继承了DialogPreference,你必须在类的构造函数中调用 setDialogLayoutResourcs()来指定布局。如代码清单5-21所示:
public class NumberPickerPreference extends DialogPreference { public NumberPickerPreference(Context context, AttributeSet attrs) { super(context, attrs); setDialogLayoutResource(R.layout.numberpicker_dialog); setPositiveButtonText(android.R.string.ok); setNegativeButtonText(android.R.string.cancel); setDialogIcon(null); } ...}
代码清单5-21
5.9.2保存设置的值
你可以在任意时刻调用Preference类的persist*()方法来保存一个值,如设置的值为int,那么就使用persistInt()。 这个方法用在对话框关闭的时候调用比较好,它会给用户一个提示。当点击positive按钮时,你就可以保存新的值。如代码清单5-22所示:
@Overrideprotected void onDialogClosed(boolean positiveResult) { // 当用户选择OK时,保存新的值 if (positiveResult) { persistInt(mNewValue); }}
代码清单5-22
在上面这个例子中,mNewValue是一个类成员变量,并且是int型的。
5.9.3初始化当前值
当系统添加你的Preference 到屏幕时,它会调用onSetInitialValue() 来通知你的值是否是已经存在的值。如果不存在, 这个调用会提供一个默认值。onSetInitialValue()方法通过一个boolean值来表明一个值是否已经被存储了。如果为true,你应该 把存储的值给取出来,你可以使用getPersistedInt()这样类似的方法取值。如果restorePersistedValue这个参数的值为 false,那么你就可以使用第二个默认值参数了,如代码清单5-23所示:
@Overrideprotected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { if (restorePersistedValue) { // 恢复状态 mCurrentValue = this.getPersistedInt(DEFAULT_VALUE); } else { // 从xml属性中设置默认状态 mCurrentValue = (Integer) defaultValue; persistInt(mCurrentValue); }}
代码清单5-23
请注意这里当restorePersistedValue为true时不能使用参数自带的defaultValue,因为它的值为null,只有当restorePersistedValue为false时才能使用。
5.9.4提供一个默认值
如果Preference的实例指定一个默认值(使用android:defaultValue属性),那么当Preference实例化对象时为 了取得默认值,系统会调用onGetDefaultValue()方法。你必须实现这个方法,这样在系统保存默认值到SharedPreferences 的时候才能正确处理。例如代码清单5-24所示:
@Overrideprotected Object onGetDefaultValue(TypedArray a, int index) { return a.getInteger(index, DEFAULT_VALUE);}
代码清单5-24
方法参数提供你需要的一切:数组属性和你需要检索的android:defaultValue的索引位置,原因你必须实现这个方法来提取默认值的属性,因为您必须指定一个本地属性的默认值,以防值是未定义的。
5.9.5保存和恢复Preference的状态
就像一个在布局中的View,你的Preference子类负责保存和恢复它的状态,以防止activity和fragment被重新启动。妥善保 存和恢复你Preference类的状态,你必须实现生命周期中的onSaveInstanceState()和 onRestoreInstanceState()回调。Preference的状态可以通过Parcelable接口实现。Android框架提供这样 一个对象,你可以作为入口点定义你对象的状态,比如Preference.BaseSavedState类。定义你Preference保存状态,你应该 继承Preference.BaseSavedState类。你需要重写一些方法来定义CREATOR对象。对于大部分应用程序,你可以直接复制一下实现 并做一些简单的改变即可,如代码清单5-25所示:
private static class SavedState extends BaseSavedState { int value; public SavedState(Parcelable superState) { super(superState); } public SavedState(Parcel source) { super(source); // 获得当前preference的值 value = source.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); // 写入preference的值 dest.writeInt(value); } public static final Parcelable.CreatorCREATOR = new Parcelable.Creator () { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } };}
代码清单5-25
下面是具体运用在onSaveInstanceState()和andonRestoreInstanceState()中的过程,如代码清单5-26所示:
@Overrideprotected Parcelable onSaveInstanceState() { final Parcelable superState = super.onSaveInstanceState(); // 检查Preference是否被保存过 if (isPersistent()) { //不需要保存实例状态,因为它是持久化的,使用父类状态 return superState; } // 创建自定义的BaseSavedState实例 final SavedState myState = new SavedState(superState); // 使用类成员变量赋值 myState.value = mNewValue; return myState;} @Overrideprotected void onRestoreInstanceState(Parcelable state) { //检查我们在onSaveInstanceState中是否保存过状态 if (state == null || !state.getClass().equals(SavedState.class)) { // 没有保存状态,调用父类的方法 super.onRestoreInstanceState(state); return; } // 强制转换到自定义的BaseSavedState SavedState myState = (SavedState) state; super.onRestoreInstanceState(myState.getSuperState()); // 应用它的值到UI,以恢复UI状态 mNumberPicker.setValue(myState.value);}
代码清单5-26
<Linker : >