概述:
通常我们的APP都会包含设置项, 以配置自己的APP的动作行为等. 如果我们想要为自己的APP提供设置项, 那么我们应该使用Android提供的Preference的API来为创建Android统一的用户体验. 栗子:
在创建设置页面的时候, 我们一般使用Preference类的子类来实现, 而不是自己定义这些控件, 这样会显得不专业. 我们可以在XML文件中直接使用Perference.
每个Preference代表设置项中的一个块, 每个Preference作为list中的一个item出现为用户提供修改设置的界面. 比如, 一个CheckBoxPreference对象代表一个显示checkbox的列表项, 一个ListPreference则创建一个可以打开列表对话框的界面.
每个我们添加的Preference对象都包含一个对应的键值对(key-value)可以保存在SharePreference文件中, 作为我们的设置项. 当用户修改设置的时候, Android会自动更新SharePreference中相关的值. 我们只有在需要根据设置项决定APP行为的时候才有必要去读取SharePreference中的值.
保存在SharePreference中的值, 可以取这些类型: Boolean, Float, Int, Long, String, String Set. 而且因为我们使用的是Preference而不是View, 所以我们需要使用特殊的Activity和Fragment的子类来显示设置项:
如果APP需要支持Android3.0以下的版本, 我们必须创建PreferenceActivity的子类作为Activity. 对于Android3.0及以上版本, 则应该使用传统的Activity+PreferenceFragment的组合来显示设置项. 不过还是依然可以使用PreferenceActivity来创建拥有两个页面的针对大屏的设置项.
APP中每个设置项都是一个Preference的子类, 这些类中包含一系列的属性,让我们可以设置该设置项的参数, 比如title, 默认值等. 下面列举一些我们最常用的属性:
CheckBoxPreference: 在设置项中显示一个带有选择框的item, 可以选中或者不选, 保存的值是boolean, true表示选中.
ListPreference: 打开一个带有单选框的列表.保存的值可以为任何可支持的类型.
EditTextPreference: 打开一个带有EditText的对话框, 保存的值是String.
上面是三个最常用的, 更多的子类可以参考这里.
在XML中新建一个Preference:
我们可以在XML文件中创建Preference, 也可以在运行时在代码中添加, 但是前者具有更好的可读性, 也更易管理. 虽然不常用到我们也可以在运行时修改它们. 每个Preference的子类都可以在XML文件中定义, 标签的名字和它们的类名一样, 比如<CheckBoxPreference>.
Preference的XML文件必须被保存在/res/xml目录下, 可以起任何合法的名字, 但是通常我们用preferences.xml,通常我们只需要一个XML文件就够了, 但是当我们需要为大屏幕指定多个layout的时候, 就需要为每个fragment指定单独的XML文件了.
每个XML文件的根标签必须是<PreferenceScreen>, 然后就是每个Preference了, 每个<PreferenceScreen>的子标签都会在设置项中显示为一个item. 栗子:
<?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, 这两个标签中都包含下面三个属性:
android:key: 这个属性是一个必须的属性, 它为Android保存该设置项指定了唯一的key,我们在SharedPreference中读取设置的值的时候, 就需要这个key来读取. 就是key-value中的key!
在大多数的Preference中key都是必须的, 但是这里有一些的例外, 在PreferenceCategory或者PreferenceScreen中. 或者Preference通过<intent>指定了一个intent的时候, 或者需要显示一个Fragment(通过android:fragment指定).
android:title: 提供一个给用户看的设置项的名字.
android:defaultValue: 为应该保存在SharedPreference文件中的设置项指定一个默认值. 我们应该为所有的设置项都指定默认值.
更多属性可以参考这里.
为设置项分组:
当我们的设置项大于10个左右的时候, 用户再去挨个查找就会觉得十分的蛋疼. 我们可以通过为设置项分组来解决这一问题, 分组有两种形式: 使用title和使用子屏幕. 至于使用哪个或者是两个都要用, 我们可以参考这里.
首先是如何使用title:
如果我们想要通过分隔线来为设置项分组, 那么必须把每组的Preference放到同一个PreferenceCategory中. 使用title的效果是这样的:
图中的1是使用<PreferenceCategory>标签的效果, 图中的2是通过android:title为分组指定标题的效果. 代码栗子:
<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>
使用子屏幕(subscreen):
如果我们想要将分组的设置项置于子屏幕中, 那么我们需要将Preference对象放在一个PreferenceScreen中, 效果如图:
当选中一个选项的时候, 会打开一个新的独立的列表. 代码栗子:
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<!-- opens asubscreen 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>
使用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: 指定一个action, 功能相当于setAction()方法.
android:data: 指定一个data, 功能相当于setData()方法.
android:mimeType: 指定mimeType, 相当于setType()方法.
android:targetClass: 指定要启动的组件类名, 相当于setComponent()方法.
android:targetPackage: 指定包名, 相当于setComponent()方法.
创建一个PreferenceActivity:
如果我们的设置项是在Activity中, 那么需要继承PreferenceActivity类. 这是一个从传统的Activity类继承来的类, 用来显示Preference对象. PreferenceActivity会自动持久化那些Preference的修改. 注意, 如果我们使用的Android版本在3.0及以上, 则应该尽量使用PreferenceFragment来代替PreferenceActivity.
使用PreferenceActivity最重要的是, 我们不需要在onCreate()中再为Activity设置layout了. 取而代之的是需要调用addPreferenceFromResource()方法加载Preference的XML文件, 栗子:
public class SettingsActivity extends PreferenceActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
}
}
使用PreferenceFragment:
在Android3.0及以上版本中, 官方推荐我们使用PreferenceFragment代替PreferenceActivity, 这样会更好的降低耦合性. Fragment本身可以提供更好的灵活性. 在PreferenceFragment中初始化并不比在PreferenceActivity中复杂, 方法是一样的, 栗子:
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);
}
...
}
然后我们可以将这个Fragment加入到任何的Activity中, 比如:
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();
}
}
注意, PreferenceFragment本身是个Fragment, 并没有Context对象, 如果我们需要在Fragment中获取Context对象, 就需要调用getActivity()方法, 但是只有在Fragment跟Activity关联的时候该方法才会返回有效值, 否则返回null.
为选择项设置默认值:
大部分的设置项都需要默认值, 我们可以在XML文件中, 对Preference对象设置android:defaultValue属性来为其指定默认值, 默认值的类型跟Preference本身的类型要匹配, 栗子:
<!-- default value is a boolean -->
<CheckBoxPreference
android:defaultValue="true"
... />
<!-- defaultvalue is a string -->
<ListPreference
android:defaultValue="@string/pref_syncConnectionTypes_default"
... />
然后还要在用户进入APP的必经之路(比如MainActivity的onCreate()方法中)上调用setDefaultValues()方法:
PreferenceManager.setDefaultValues(this, R.xml.advanced_preferences, false);
调用该方法以确保APP已经初始化了默认值, 因为我们的APP可能需要读取一些设置项来执行某些操作. 这个方法包含三个参数:
第一个是Context, 第二个是想要设置默认值的Preference的XML文件的资源ID. 第三个是一个boolean值, 表示默认值是否应该被设置一次以上, 如果为false, 只有当过去该方法从未被调用的时候才会设置默认值(就是只有第一次调用生效). 该参数设置为false的时候, 我们可以毫无顾忌的调用该方法而不必担心会覆盖用户已经保存的设置项. 但是如果是true的话, 那么会覆盖用户的选择.使用PreferenceHeaders:
在某些情况下我们可能需要只显示subscreen列表的设置项, 点击任何一个设置项都进入到一个subscreen中. 在Android3.0及以上版本中, 我们应该使用”headers”来代替嵌套的PreferenceScreen标签. 设置headers的步骤:
1. 将每个设置项的组用单独的PreferenceFragment实例来实现, 每个组都需要一个单独的XML文件.
2. 创建一个XML headers文件, 列出每个设置组. 声明每个Fragment应该包含的列表.
3. 继承PreferenceActivity类用来承载我们的设置项.
4. 实现onBuildHeaders()回调方法来指定headers file.
使用这种设计最大的好处是PreferenceActivity在大屏幕上会自动展示成两个界面, 效果如下图:
上图中的1部分是由XML headers文件中定义的. 图中的2部分是一个PreferenceFragment, 由<header>标签指定.
上图是在手机上的表现.
创建Headers文件:
Headers由<header>标签指定, 它应该包含在一个<preference-header>标签内, 栗子:
<?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>
我们通过android:fragment属性来为每个header指定一个PreferenceFragment的实例. 这样当用户打开一个header的时候, 就会直接开启一个Fragment.
<extras>让我们可以指定一个key-value值给Fragment, 它们会以Bundle对象的方式传给Fragment.在Fragment中可以使用getArguments()方法获得这个参数. 使用该标签最合适的时机应该是我们需要重用PreferenceFragment的子类, 但是需要指定Fragment需要加载那个XML文件. 比如, 这里是通过<extras>指定”settings”参数让Fragment可以重用多个XML文件的栗子:
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);
}
}
}
显示Headers:
如果想要显示Headers, 我们必须实现onBuildHeaders()回调方法, 并且在里面调用loadHeadersFromResource(), 栗子:
public class SettingsActivity extends PreferenceActivity {
@Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.preference_headers, target);
}
}
当用户从headers选择一个item的时候, Android将会打开相应的PreferenceFragment. 当使用Preference headers的时候, 我们的PreferenceActivity类的子类不需要实现onCreate()方法, 因为它唯一的任务就是加载headers.
使用Preferenceheaders支持早期的版本:
如果我们的APP需要支持Android3.0以前的版本, 那么我们依然可以使用headers来支持3.0以后的版本, 所有我们需要做的就是创建一个额外的Preference文件, 使用<Preference>标签让它看起来像是一个headers(用于3.0以前的版本). 每个<Preference>标签都需要一个Intent以指定需要打开哪个PreferenceActivity.
这是一个比3.0更新的版本的栗子, 新版本中我们使用header + fragment (res/xml/preference_headers.xml):
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header
android:fragment="com.example.prefs.SettingsFragmentOne"
android:title="@string/prefs_category_one"
android:summary="@string/prefs_summ_category_one" />
<header
android:fragment="com.example.prefs.SettingsFragmentTwo"
android:title="@string/prefs_category_two"
android:summary="@string/prefs_summ_category_two" />
</preference-headers>
这是一个比3.0更旧的版本的栗子, 旧版本中我们使用Preference + Intent (res/xml/preference_headers_legacy.xml):
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<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>
<Preference
android:title="@string/prefs_category_two"
android:summary="@string/prefs_summ_category_two" >
<intent
android:targetPackage="com.example.prefs"
android:targetClass="com.example.prefs.SettingsActivity"
android:action="com.example.prefs.PREFS_TWO" />
</Preference>
</PreferenceScreen>
3.0(HONEYCOMB)以上版本使用onBuildHeaders()来加载<preference-headers>,所以我们必须分辨版本号进行区别对待, 栗子:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
// Load the legacy preferences headers
addPreferencesFromResource(R.xml.preference_headers_legacy);
}
}
// Called onlyon Honeycomb and later
@Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.preference_headers, target);
}
剩下的要做的事情就是接收Intent的activity需要识别加载哪个文件. 我们可以通过识别Intent附带的action来区分:
final static String ACTION_PREFS_ONE = "com.example.prefs.PREFS_ONE";
...
@Override
public 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) {
// Load the legacy preferences headers
addPreferencesFromResource(R.xml.preference_headers_legacy);
}
}
这里需要注意的是, addPreferenceFromResource()方法只能调用一次, 否则会重复加载, 所以请保证只有一个if语句可以执行.
参考: http://developer.android.com/guide/topics/ui/settings.html