应用程序呢,通常会有一些设置,用户可以修改app的特性和行为。比如,一些app,用户可以指定是否可以推送通知,或者多长时间与云数据发送同步。
要为app提供这类的设置功能呢,就需要使用android的Preference API来构建一个与其他android应用程序(包括系统的设置)一致的界面。
这篇文章,就介绍一下怎样通过Preference API构建自己的app的设置。
一、简述设置界面的设计理念
详情查看http://developer.android.com/design/patterns/settings.html
这是短信应用程序的设置,选择一项打开一个新界面来更改设置。
二、概述
SharedPreferences能保存的数据类型只有以下几种:
Boolean
Float
Int
Long
String
String Set
由于应用程序的设置界面是用Preference对象构建的,而不是View对象,所以我们需要使用专门的Activity或者Fragment的子类,来展示设置列表。
1、如果app支持的版本低于3.0(API 10),那么就必须构建一个继承PreferenceActivity类的activity。
2、android 3.0之后的版本,就可以用PreferenceFragment了,而不用传统的Activity。当然屏幕够大的话,也可用PreferenceActivity来创建两个容器layout展示双重设置组(两个设置界面联动)。
具体怎么用,将在本文下面介绍。
三、在XML文件中定义preference
这里的XML文件必须保存在res/xml/目录下,系统也会自动生成xml目录。一般呢,会将其命名为preferences.xml,当然可以随意命名。一般只需要这一个文件(当然也有特殊情况,当用到<header>的时候就需要使用分离的xml文件,下面介绍),因为preference的一些子类都是嵌套在preferenceScreen节点里面的,例如:
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<CheckBoxPreference
android:key="pref_sync"
android:title="@string/pref_sync"
android:summary="@string/pref_sync_summ"
android:defaultValue="true" />
<ListPreference
android:dependency="pref_sync"
android:key="pref_syncConnectionType"
android:title="@string/pref_syncConnectionType"
android:dialogTitle="@string/pref_syncConnectionType"
android:entries="@array/pref_syncConnectionTypes_entries"
android:entryValues="@array/pref_syncConnectionTypes_values"
android:defaultValue="@string/pref_syncConnectionTypes_default" />
</PreferenceScreen>
在这个例子中,有一个checkboxpreference和一个listpreference,这两个item都有以下三个属性:
android:key
指定在sharedpreferences中存储的唯一性的key(一个字符串类型),系统用这个key来存储响应的设置的值。
这个属性有些情况不必写:
<PreferenceScreen/>
<PreferenceCategory/>
Preference指定了intent或者fragment。
<Preference
android:title="@string/prefs_category_one"
android:summary="@string/prefs_summ_category_one" >
<intent
android:targetPackage="com.example.prefs"
android:targetClass="com.example.prefs.SettingsActivity"
android:action="com.example.prefs.PREFS_ONE" />
</Preference>
-----------------------------------------------------------------------
<header
android:fragment="com.example.prefs.SettingsFragmentOne"
android:title="@string/prefs_category_one"
android:summary="@string/prefs_summ_category_one" />
andorid:title
如果不指定summary,那么就是这个item的显示文本。
如果指定了summary,那么summary将在title下面显示。
android:defaultValue
建议每个setting设置defaultValue。
四、创建分组
当设置项超过10个时,就应该考虑分组了。分组有两种实现方式:
1、使用titles
2、使用子屏幕
使用titles就像下面这样:
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="@string/pref_sms_storage_title"
android:key="pref_key_storage_settings">
<CheckBoxPreference
android:key="pref_key_auto_delete"
android:summary="@string/pref_summary_auto_delete"
android:title="@string/pref_title_auto_delete"
android:defaultValue="false"... />
<Preference
android:key="pref_key_sms_delete_limit"
android:dependency="pref_key_auto_delete"
android:summary="@string/pref_summary_delete_limit"
android:title="@string/pref_title_sms_delete"... />
<Preference
android:key="pref_key_mms_delete_limit"
android:dependency="pref_key_auto_delete"
android:summary="@string/pref_summary_delete_limit"
android:title="@string/pref_title_mms_delete" ... />
</PreferenceCategory>
...
</PreferenceScreen>
使用子屏幕,就像下面这样:
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<!-- opens a subscreen of settings -->
<PreferenceScreen
android:key="button_voicemail_category_key"
android:title="@string/voicemail"
android:persistent="false">
<ListPreference
android:key="button_voicemail_provider_key"
android:title="@string/voicemail_provider" ... />
<!-- opens another nested subscreen -->
<PreferenceScreen
android:key="button_voicemail_setting_key"
android:title="@string/voicemail_settings"
android:persistent="false">
...
</PreferenceScreen>
<RingtonePreference
android:key="button_voicemail_ringtone_key"
android:title="@string/voicemail_ringtone_title"
android:ringtoneType="notification" ... />
...
</PreferenceScreen>
...
</PreferenceScreen>
ps.android 3.0版本之后,就不用嵌套的PreferenceScreen了,改用header了,下面会介绍。
另外,还可以使用intent。 因为有的时候,想通过设置项跳转到其他的activity,比如说打开一个网页,那就可以用intent
<Preference android:title="@string/prefs_web_page" >
<intent android:action="android.intent.action.VIEW"
android:data="http://www.example.com" />
</Preference>
intent标签还包含以下属性:
android:action
The action to assign, as per the setAction()
method.
android:data
The data to assign, as per the setData()
method.
android:mimeType
The MIME type to assign, as per the setType()
method.
android:targetClass
The class part of the component name, as per the setComponent()
method.
android:targetPackage
The package part of the component name, as per the setComponent()
method.
五、创建preferenceActivity
android版本在3.0之前,用到的preferenceActivity的创建,一段代码可以概括:
public class SettingsActivity extends PreferenceActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
}
}
六、使用preferenceFragment
android版本3.0之后,就建议使用fragment替代activity了,因为fragment是更灵活的层级结构,灵活体现在任何一个activity都可以随意的装载该fragment。
同样使用两段代码也能说明问题:
public static class SettingsFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences);
}
...
}
public class SettingsActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Display the fragment as the main content.
getFragmentManager().beginTransaction()
.replace(android.R.id.content, new SettingsFragment())
.commit();
}
}
七、设置默认值
XML文件中设置默认值:
<!-- default value is a boolean -->
<CheckBoxPreference
android:defaultValue="true"
... />
<!-- default value is a string -->
<ListPreference
android:defaultValue="@string/pref_syncConnectionTypes_default"
... />
代码中动态设置默认值,就需要在onCreate()方法中添加以下代码:
PreferenceManager.setDefaultValues(this, R.xml.advanced_preferences, false);
关于这个方法的三个参数重点说说第三个参数:
第三个参数:是一个boolean值,指示默认值是否可以被设置多次。
如果是false,那么默认值只会在第一次调用时设置,以后即使调用也不再设置了。
如果是true,那么默认值需要在activity每次获取值之前先设置默认值。
所以,安全的做法是:false.
八、使用Preference Header
在一些比较少的情况下,可能希望提一个设置界面只单纯得显示列表,具体的设置,要到另一个界面去设置。
android版本3.0之前“子屏幕”用嵌套的preferenceScreen,3.0之后,就引入了header。
要使用header,就得做以下改变:
1、将每个“设置组”分离到单独一个preferenceFragment实例中,当然这样就伴随着,需要将xml按组分离成多个xml文件,分配到每个preferenceFragment实例中。
2、创建一个xml header文件,列出每个设置组,并关联响应的fragment。
3、继承PreferenceActivity类,来掌管设置。
4、实现onBuildHeader()方法,来指定header的xml文件。
1. The headers are defined with an XML headers file.
2. Each group of settings is defined by a PreferenceFragment
that's specified by a <header>
element in the headers file.
A handset device with setting headers. When an item is selected, the associated PreferenceFragment
replaces the headers.
九、创建headers文件
用一段代码说明一下:
<?xml version="1.0" encoding="utf-8"?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header
android:fragment="com.example.prefs.SettingsActivity$SettingsFragmentOne"
android:title="@string/prefs_category_one"
android:summary="@string/prefs_summ_category_one" />
<header
android:fragment="com.example.prefs.SettingsActivity$SettingsFragmentTwo"
android:title="@string/prefs_category_two"
android:summary="@string/prefs_summ_category_two" >
<!-- key/value pairs can be included as arguments for the fragment. -->
<extra android:name="someKey" android:value="someHeaderValue" />
</header>
</preference-headers>
根节点变了,不再是preferenceScreen了,变成了preference-header。
支节点也不再是preference的子类了,而是header。
<extra />元素可以向fragment传递Bundle类型的键值对。fragment呢,可以通过getArgument()方法重新获取传递值。
十、显示header
要显示header呢,就必须实现onBuildHeader()方法,并且调用loadHeadersFromResource()方法。
public class SettingsActivity extends PreferenceActivity {
@Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.preference_headers, target);
}
}
ps.使用preference header 上面继承preferenceActivity的SettingsActivity就不必实现onCreate()方法了。
十一、读取preference值
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
String syncConnPref = sharedPref.getString(SettingsActivity.KEY_PREF_SYNC_CONN, "");
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);
// Set summary to be the user-description for the selected value
connectionPref.setSummary(sharedPreferences.getString(key, ""));
}
}
}
@Override
protected void onResume() {
super.onResume();
getPreferenceScreen().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this);
}
@Override
protected void onPause() {
super.onPause();
getPreferenceScreen().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this);
}
十三、自定义preference
自定义Preference:
如果继承Preference,那需要实现onClick事件。然而一般继承DialogPreference,可以简化程序。
首先需要一个构造函数(系统会提示,必须要实现的),在构造函数里面需要调用setDialogLayoutResourse方法来设置布局文件。同时,可以setPositiveText或者setNagativeText
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);
}
...
}
另外一点是,保存数据。
每个preference只能保存一种数据类型。所以保存数据时要指定保存的数据类型像:persistInt()、persistBoolen();
什么时候保存数据,也有讲究,必须继承自DialogPreference,那么就应该在dialog关闭的时候保存,进一步说是,点击ok按钮的时候保存。
@Override
protected void onDialogClosed(boolean positiveResult) {
// When the user selects "OK", persist the new value
if (positiveResult) {
persistInt(mNewValue);
}
}
persistInt()
方法将自动保存到SharedPreferences文件中,key是在xml布局中指定的那个。
也就是说省去了edit.put和commit。
还有一点是,初始化当前值。
Preference
中有个onSetInitialValue()方法。
系统添加Preference到屏幕显示时,系统会调用这个方法。这个方法可判断该settings时候已经有值了,若没有值,那么就设置为默认值(objectdefaultValue)。
这个方法有个参数:boolen restorePersistedValue.这个参数指示,此settings是否已经有值了。
具体细节看下段代码解释:
@Override
protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
if (restorePersistedValue) {
// Restore existing state
//protected int getPersistedInt (int defaultReturnValue)
//defaultReturnValue The default value to return if either this Preference is not persistent or this Preference is not in the SharedPreferences.
// You cannot use the defaultValue as the default value, because its value is always null when restorePersistedValue is true.
mCurrentValue = this.getPersistedInt(DEFAULT_VALUE);
} else {
// Set default state from the XML attribute, in other words, Object defaultValue need to set in the xml file by android:defaultValue="";
mCurrentValue = (Integer) defaultValue;
persistInt(mCurrentValue);
}
}
再者,提供默认值。
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return a.getInteger(index, DEFAULT_VALUE);
}
最后,保存和恢复Preference状态。
万一,用户旋屏,activity或者fragment restart()了,那就要考虑到保存和恢复状态的问题了。