☰
在 Android 中实现嵌套 PreferenceScreen:cw-omnibus NestedScreens 示例深度解析
2026/10/16 2:29:27 网站建设 项目流程
  • 示例工程
  • 教程

【免费下载链接】cw-omnibus

Source code to omnibus edition of _The Busy Coder's Guide to Android Development_

项目地址:https://gitcode.com/gh_mirrors/cw/cw-omnibus
点击查看免费下载

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); } } }

这段代码有两个值得深究的点:

  1. EXTRA_SHOW_FRAGMENT守卫判断:当用户从第一层点击嵌套的 "Moar Settings!" 条目时,框架会携带PreferenceActivity.EXTRA_SHOW_FRAGMENT这个 extra 重新启动/驱动该 Activity,并自动把目标 Fragment(EditPreferences$Second)填入内容区。因此onCreate中必须先用getStringExtra(EXTRA_SHOW_FRAGMENT)==null判断:只有当不是由嵌套条目触发的首次启动时,才手动添加FirstFragment,否则会覆盖框架已加载的Second,导致嵌套页面加载失败。
  2. 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时建议注意:

  1. 层级与 Fragment 一一对应:每一层设置页对应一个PreferenceFragment,用android:fragment全限定类名建立导航关系;不要让一个 Fragment 承载多层页面。
  2. 宿主 Activity 做好兼容判断:EXTRA_SHOW_FRAGMENT的守卫判断是嵌套流程正确工作的前提,否则二级页面会因 Fragment 被覆盖而闪退或空白。
  3. key 全局唯一:虽然 UI 有层级,但所有偏好写入同一个SharedPreferences,key 冲突会相互覆盖。
  4. 扁平读取:无论嵌套多深,业务代码都可以用同一个PreferenceManager.getDefaultSharedPreferences()统一读取,无需感知页面层级。
  5. 混淆保护:发布混淆包时保留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_

项目地址:https://gitcode.com/gh_mirrors/cw/cw-omnibus
点击查看免费下载

相关推荐

上一篇:终极免费MP4视频修复指南:3分钟拯救损坏的视频文件
下一篇:如何快速解锁Wand专业功能:完整免费游戏增强工具指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询