- 示例工程
- 教程
【免费下载链接】cw-omnibus
Source code to omnibus edition of _The Busy Coder's Guide to Android Development_
PreferenceScreen是 Android 设置界面的核心载体,而"设置界面里再嵌一层设置界面"(嵌套 PreferenceScreen)则是构建多层级设置页的经典手法。本文以 cw-omnibus 仓库中的 Prefs/NestedScreens 示例为主线,完整剖析嵌套设置项的 XML 声明、Fragment 加载机制、宿主 Activity 的兼容处理与数据读取方式,并给出可直接复用的实现要点。
示例概述:一个嵌套了设置页的偏好设置应用
Prefs/NestedScreens/README.markdown 将本示例定位为:演示在一个PreferenceScreen内部再放另一个PreferenceScreen。该示例来自The Busy Coder's Guide to Android Development中关于进阶偏好设置技巧(advanced preference tips)一章。
整个应用由两个页面组成:
- 主页面(
FragmentsDemo):通过选项菜单中的 "Settings" 入口打开偏好设置界面; - 偏好设置页(
EditPreferences):第一层设置项包括 CheckBox、Ringtone、EditText、List 四种常用偏好控件,以及一个嵌套的二级设置页 "Moar Settings!",点击后进入第二层设置页,其中包含另一个复选框。
用户修改任一设置后,回到主页面即可在PreferenceContentsFragment中实时看到全部偏好的当前值,从而直观验证嵌套设置页中的数据与普通设置项一样被统一持久化。
项目结构与关键文件
Prefs/NestedScreens/ ├── README.markdown ├── build.gradle、settings.gradle、proguard.cfg、project.properties └── app/ ├── build.gradle └── src/main/ ├── AndroidManifest.xml ├── java/com/commonsware/android/preffragsbc/ │ ├── EditPreferences.java // 偏好设置宿主 Activity │ ├── PreferenceContentsFragment.java // 展示偏好当前值的 Fragment │ └── FragmentsDemo.java // 主页面 Activity └── res/ ├── xml/preferences.xml // 一级设置项(含嵌套声明) ├── xml/preferences2.xml // 二级设置项 ├── layout/main.xml、content.xml └── menu/actions.xml // "Settings" 菜单项从源码结构看,本项目采用了"宿主 Activity + 内部静态 PreferenceFragment"的经典组合,是 Android 4.0 之后(本项目minSdkVersion 14)推荐的偏好设置实现方式。
核心 XML:如何声明一个嵌套的 PreferenceScreen
嵌套设置界面的关键在 res/xml/preferences.xml。该文件是"第一层"设置页,包含四项普通偏好:
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <CheckBoxPreference android:key="checkbox" android:summary="@string/pref1summary" android:title="@string/pref1title"/> <RingtonePreference android:key="ringtone" android:showDefault="true" android:showSilent="true" android:summary="@string/pref2summary" android:title="@string/pref2title"/> <EditTextPreference android:dialogTitle="@string/dialogtitle" android:key="text" android:summary="@string/pref3summary" android:title="@string/pref3title"/> <ListPreference android:dialogTitle="@string/listdialogtitle" android:entries="@array/cities" android:entryValues="@array/airport_codes" android:key="list" android:summary="@string/pref4summary" android:title="@string/pref4title"/> <PreferenceScreen android:fragment="com.commonsware.android.preffragsbc.EditPreferences$Second" android:key="unused" android:title="@string/nested_title"/> </PreferenceScreen>要点拆解:
- 嵌套声明:
<PreferenceScreen>本身既可以作为整个设置页的根元素,也可以作为其中一个条目出现。上例中第二处<PreferenceScreen>就是嵌套层级的入口条目,其android:title="@string/nested_title"对应字符串Moar Settings!,在 UI 上表现为一个可点击、带箭头指示的设置项。 android:fragment属性:这是"嵌套 + Fragment 化"的关键。它指定点击该条目后要启动的PreferenceFragment的完整类名,这里指向EditPreferences$Second(EditPreferences的内部静态类Second)。注意必须使用全限定类名,内部类用$分隔。android:key为何是 "unused":由于该嵌套条目本身不存储任何偏好值,仅作为导航入口,其 key 只需保证唯一即可。从命名上也能推断:它不参与实际数据存储。- 常见偏好属性速查:
CheckBoxPreference:key(存储键)、title、summary;RingtonePreference:额外支持showDefault="true"(显示"默认铃声"选项)与showSilent="true"(显示"静音"选项);EditTextPreference:通过dialogTitle定制输入对话框标题;ListPreference:通过entries(显示文本数组)与entryValues(实际存储值数组)成对配置,两个数组按索引一一对应。
对应的显示文本定义在 res/values/strings.xml,其中nested_title的值为Moar Settings!;ListPreference使用的城市与机场代码数组定义在 res/values/arrays.xml(9 个宾夕法尼亚州城市与其机场代码一一对应)。
二级设置页:preferences2.xml
第二层设置页由 res/xml/preferences2.xml 定义,内容非常精简:
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <CheckBoxPreference android:key="checkbox2" android:summary="@string/pref5summary" android:title="@string/pref5title"/> </PreferenceScreen>它只包含一个key为checkbox2的复选框。这个 key 很关键:无论偏好控件位于哪一层嵌套页面,最终都会写入同一个默认SharedPreferences文件,因此主页面读取checkbox2就能拿到二级页面里的设置值。这正是嵌套 PreferenceScreen 在数据层上的核心特征——UI 层级可以嵌套,数据存储保持扁平。
宿主 Activity:PreferenceActivity + 内部 PreferenceFragment
EditPreferences.java 继承PreferenceActivity,内部定义了First与Second两个静态PreferenceFragment:
public class EditPreferences extends PreferenceActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getIntent().getStringExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT)==null) { if (getFragmentManager().findFragmentById(android.R.id.content)==null) { getFragmentManager().beginTransaction() .add(android.R.id.content, new First()).commit(); } } } public static class First extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); } } public static class Second extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences2); } } }这段代码有两个值得深究的点:
EXTRA_SHOW_FRAGMENT守卫判断:当用户从第一层点击嵌套的 "Moar Settings!" 条目时,框架会携带PreferenceActivity.EXTRA_SHOW_FRAGMENT这个 extra 重新启动/驱动该 Activity,并自动把目标 Fragment(EditPreferences$Second)填入内容区。因此onCreate中必须先用getStringExtra(EXTRA_SHOW_FRAGMENT)==null判断:只有当不是由嵌套条目触发的首次启动时,才手动添加FirstFragment,否则会覆盖框架已加载的Second,导致嵌套页面加载失败。findFragmentById(android.R.id.content)幂等保护:在onCreate被系统重建(如旋转屏幕)时,Fragment 可能已由 FragmentManager 恢复,此时不应重复添加,否则会出现 Fragment 叠加。用这一判断保证 Activity 重建后设置页状态不丢失。
两个 Fragment 通过各自的addPreferencesFromResource(...)分别加载R.xml.preferences与R.xml.preferences2,实现"一个 Activity 承载多个设置页"。
注:本项目仍使用
android.preference.PreferenceFragment体系(API 14+),若在较新项目中使用 AndroidX 的PreferenceFragmentCompat,上述嵌套与EXTRA_SHOW_FRAGMENT的处理思路仍然一致,只是包名与加载 API(setPreferencesFromResource)不同。
数据验证:读取嵌套页中的偏好值
主页面 FragmentsDemo.java 通过 res/layout/main.xml 直接以<fragment>标签加载PreferenceContentsFragment。该 Fragment 在onResume()中读取全部偏好值并显示在表格布局中:
SharedPreferences prefs= PreferenceManager.getDefaultSharedPreferences(getActivity()); checkbox.setText(Boolean.valueOf(prefs.getBoolean("checkbox", false)).toString()); ringtone.setText(prefs.getString("ringtone", "<unset>")); checkbox2.setText(Boolean.valueOf(prefs.getBoolean("checkbox2", false)).toString()); text.setText(prefs.getString("text", "<unset>")); list.setText(prefs.getString("list", "<unset>"));对应的界面布局在 res/layout/content.xml,是一个TableLayout,五行分别对应 Checkbox、Ringtone、Checkbox #2、Text、List 的标签与取值。
从中可以得到两个实用结论:
- 统一存储:无论设置项处于嵌套的第几层,
PreferenceManager.getDefaultSharedPreferences()拿到的都是同一个默认偏好文件,key全局唯一即可正确读写。这里checkbox(一级)与checkbox2(二级)共存于同一文件。 - 类型对应关系:
CheckBoxPreference用getBoolean读取、RingtonePreference/EditTextPreference/ListPreference用getString读取;示例为每个读取都提供了默认值(如"<unset>"),避免首次运行(尚未设置)时返回 null 造成 UI 显示异常。 onResume时机:把读取放在onResume而非onCreate,可以保证用户从设置页返回主页面时,界面上的值能够即时刷新。
菜单入口与 Manifest 配置
FragmentsDemo.java 在onCreateOptionsMenu中加载 res/menu/actions.xml,其 "Settings" 菜单项使用系统内置图标@android:drawable/ic_menu_preferences;点击后通过startActivity(new Intent(this, EditPreferences.class))打开设置页。
AndroidManifest.xml 中注册了两个 Activity:
<activity android:name=".FragmentsDemo" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".EditPreferences" android:label="@string/app_name"/>FragmentsDemo是启动入口,EditPreferences作为普通 Activity 被显式调用,无需 intent-filter。应用主题在 values/themes.xml 中基于 Holo Light 定义,并在 values-v21/styles.xml 中为 Android 5.0+ 覆盖为 Material Light 主题并设置主色、深色主色与强调色(定义于 values/colors.xml)。
构建配置与 ProGuard 注意事项
项目构建配置如下:
- app/build.gradle:
compileSdkVersion 21、buildToolsVersion '26.0.2',minSdkVersion 14、targetSdkVersion 17,applicationId为com.commonsware.android.prefs.nested; - build.gradle:使用 Android Gradle 插件
3.1.4,仓库为google()与jcenter(); - gradle/wrapper/gradle-wrapper.properties:Gradle 4.4;
- project.properties:
target=android-19(供旧式 Ant 构建使用)。
若启用 ProGuard(proguard.cfg),务必注意一条关键规则:
-keep public class * extends android.preference.Preference因为 XML 中的android:fragment通过反射按类名字符串加载 Fragment 类,混淆后类名会被改写,导致嵌套设置页无法找到目标 Fragment。上述 keep 规则确保所有Preference子类(含EditPreferences$Second这类继承PreferenceFragment的类)不被改名。
嵌套设置页的设计要点小结
结合本示例,使用嵌套PreferenceScreen时建议注意:
- 层级与 Fragment 一一对应:每一层设置页对应一个
PreferenceFragment,用android:fragment全限定类名建立导航关系;不要让一个 Fragment 承载多层页面。 - 宿主 Activity 做好兼容判断:
EXTRA_SHOW_FRAGMENT的守卫判断是嵌套流程正确工作的前提,否则二级页面会因 Fragment 被覆盖而闪退或空白。 - key 全局唯一:虽然 UI 有层级,但所有偏好写入同一个
SharedPreferences,key 冲突会相互覆盖。 - 扁平读取:无论嵌套多深,业务代码都可以用同一个
PreferenceManager.getDefaultSharedPreferences()统一读取,无需感知页面层级。 - 混淆保护:发布混淆包时保留
android.preference.Preference子类,避免反射加载失败。
如果需要进一步对比单层PreferenceFragment的用法,可参考同目录下的 Prefs/Fragment 示例;需要了解基于兼容包PreferenceFragmentCompat的实现时,可参考 Prefs/FragmentsBC 与 Prefs/CustomSubtitle 等相邻示例。
- 示例工程
- 教程
【免费下载链接】cw-omnibus
Source code to omnibus edition of _The Busy Coder's Guide to Android Development_
相关推荐
国家中小学智慧教育平台电子课本下载终极指南:让教学资源管理变得简单高效
国家中小学智慧教育平台电子课本下载终极指南:让教学资源管理变得简单高效 在数字化教育蓬勃发展的今天,教师们面临着一个看似简单却极其困扰的问题:如何将在线教育资源
开发工具网页爬虫cw-omnibus HttpClient 示例深度解析:在 Android 中用 Apache HttpClient 拉取并展示网络数据
cw omnibus HttpClient 示例深度解析:在 Android 中用 Apache HttpClient 拉取并展示网络数据 cw omnibus
示例工程教程Android Snackbar 带操作按钮实战:cw-omnibus SnackbarAction 示例深度解析
Android Snackbar 带操作按钮实战:cw omnibus SnackbarAction 示例深度解析 本篇技术指南基于 cw omnibus 仓库
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考