ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用 PreferenceFragment 构建分组设置界面:解读 cw-omnibus 中的 FragmentsBC 示例

用 PreferenceFragment 构建分组设置界面:解读 cw-omnibus 中的 FragmentsBC 示例
  • 示例工程
  • 教程

【免费下载链接】cw-omnibus

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

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

本篇技术指南以 cw-omnibus 仓库(The Busy Coder's Guide to Android Development配套示例源码集)中 Prefs/FragmentsBC/README.markdown 所述示例为核心,系统讲解如何借助PreferenceFragment与PreferenceActivity的 header 机制,把设置项拆分成多个分组页面。读完本文,你将掌握loadHeadersFromResource()加载 header、addPreferencesFromResource()挂载偏好资源、isValidFragment()安全校验,以及如何在普通 Fragment 中通过SharedPreferences回读偏好值这一整套实战方案。

一、示例要解决的问题:一个 Activity 管理多组设置

Android 3.0(API 11)引入PreferenceFragment之后,设置界面不再只能由PreferenceActivity单独承载 XML 资源,而是可以把每一组设置放进一个 Fragment。FragmentsBC 示例即演示了这种"一个设置页 + 多个偏好分组"的组织形态:

  • 主界面FragmentsDemo是一个普通 Activity,仅通过菜单项进入设置页;
  • 设置页EditPreferences继承PreferenceActivity,重写onBuildHeaders()用 header 列表把两组设置串起来;
  • 两个PreferenceFragment(内部类First、Second)分别承载两组不同的偏好选项;
  • 返回主界面后,PreferenceContentsFragment从默认SharedPreferences中读取所有设置结果并展示。

该示例的配套书籍章节为《using preferences(使用偏好设置)》,在仓库中的完整实现见 Prefs/FragmentsBC 目录。

二、整体架构与入口流程

2.1 三个 Java 类的分工

类文件职责
FragmentsDemoFragmentsDemo.java主界面,展示当前偏好值,菜单触发设置页
EditPreferencesEditPreferences.javaPreferenceActivity子类,负责 header 路由与 Fragment 白名单
PreferenceContentsFragmentPreferenceContentsFragment.java普通 Fragment,在onResume()中读取并刷新偏好显示

2.2 从主界面到设置页

主界面 FragmentsDemo.java 在onCreate()中加载 main.xml,后者通过静态<fragment>标签直接嵌入了PreferenceContentsFragment:

<fragment xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:name="com.commonsware.android.preffragsbc.PreferenceContentsFragment"/>

菜单方面,actions.xml 定义了一个settings菜单项(图标使用系统自带的@android:drawable/ic_menu_preferences,并设置showAsAction="never"使其始终出现在溢出菜单)。点击后经onOptionsItemSelected()启动EditPreferences:

@Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.settings: startActivity(new Intent(this, EditPreferences.class)); return(true); } return(super.onOptionsItemSelected(item)); }

对应地,AndroidManifest.xml 中同时注册了主 Activity 与设置 Activity,设置页无需LAUNCHER过滤器:

<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"/>

三、核心实现:PreferenceActivity + Header 路由

3.1 onBuildHeaders 与 loadHeadersFromResource

EditPreferences的关键在于重写onBuildHeaders(),把 header 资源喂给系统(EditPreferences.java#L22-L26):

public class EditPreferences extends PreferenceActivity { @Override public void onBuildHeaders(List<Header> target) { loadHeadersFromResource(R.xml.preference_headers, target); } ... }

preference_headers.xml定义了两个分组入口,每个<header>通过android:fragment指向要展示的PreferenceFragment全限定类名(preference_headers.xml):

<preference-headers xmlns:android="http://schemas.android.com/apk/res/android"> <header android:fragment="com.commonsware.android.preffragsbc.EditPreferences$First" android:summary="@string/header1summary" android:title="@string/header1title"> </header> <header android:fragment="com.commonsware.android.preffragsbc.EditPreferences$Second" android:summary="@string/header2summary" android:title="@string/header2title"> </header> </preference-headers>

注意类名的写法:内部类使用$分隔,即EditPreferences$First。title与summary分别来自 strings.xml(header1title="Some Settings"、header1summary="A handful of values for you to tailor"、header2title="More Settings" 等)。

3.2 isValidFragment:Android 4.0+ 的强制安全闸门

从 API 19 开始,凡是使用 header 加载 Fragment 的PreferenceActivity,系统会调用isValidFragment()校验传入的 Fragment 类名,防止外部注入恶意 Fragment。示例中通过白名单方式放行自带的两个内部类(EditPreferences.java#L29-L36):

@Override protected boolean isValidFragment(String fragmentName) { if (First.class.getName().equals(fragmentName) || Second.class.getName().equals(fragmentName)) { return(true); } return(false); }

从源码结构看,这是基于 fragment 的偏好页面必不可少的配套代码:一旦 header 引用的 Fragment 类名不在此白名单内,isValidFragment()返回false,系统将拒绝加载该 Fragment。

3.3 两个 PreferenceFragment 内部类

两个内部类都继承android.preference.PreferenceFragment,在onCreate()里通过addPreferencesFromResource()各自加载一份偏好 XML(EditPreferences.java#L38-L54):

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

First挂载的是 preferences.xml,Second挂载的是 preferences2.xml。这样一个PreferenceActivity就能承载两个互相独立的设置分组,用户通过 header 列表切换。

四、偏好资源详解:四类常用 Preference

preferences.xml 聚合了 Android 平台四种最常见的偏好控件,是理解PreferenceScreen子节点用法的绝佳范本:

<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>

各控件要点:

  • CheckBoxPreference:开关型偏好,android:key="checkbox"即写入SharedPreferences的键名;
  • RingtonePreference:铃声选择器,android:showDefault="true"让列表中出现"默认铃声"项,android:showSilent="true"让列表中出现"静音"项;
  • EditTextPreference:弹窗文本输入,android:dialogTitle指定弹窗标题;
  • ListPreference:弹窗单选列表,android:entries是展示给用户的文本数组,android:entryValues是实际写入存储的值数组,两者通过数组下标一一对应。

第二个分组 preferences2.xml 只包含一个CheckBoxPreference(android:key="checkbox2"),用于验证"多个 Fragment 分组各自独立维护 XML"的模式。

4.1 配套的数组资源

arrays.xml 为ListPreference提供显示名与存储值的成对数据——以宾夕法尼亚州城市为显示名、机场三字码为存储值:

<string-array name="cities"> <item>Philadelphia</item> <item>Pittsburgh</item> <item>Allentown/Bethlehem</item> ... </string-array> <string-array name="airport_codes"> <item>PHL</item> <item>PIT</item> <item>ABE</item> ... </string-array>

4.2 文本资源的组织

strings.xml 统一维护了 header 标题/摘要、偏好标题/摘要、弹窗标题等文案(如pref1title="Checkbox Preference"、pref2summary="Pick a tone, any tone"、listdialogtitle="Choose a Pennsylvania city")。所有 XML 中均通过@string/...引用,避免了魔法字符串。

五、回读偏好值:PreferenceContentsFragment 的 onResume 刷新

设置页修改的值保存在应用默认的SharedPreferences中。主界面的 PreferenceContentsFragment.java 在onResume()中统一读取——之所以放在onResume()而不是onCreateView(),是为了保证用户从设置页返回时能拿到最新值:

@Override public void onResume() { super.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>")); }

要点:

  • PreferenceManager.getDefaultSharedPreferences()返回的是与 Preference UI 默认关联的那份偏好文件,key 与 XML 中的android:key一一对应;
  • 每个读取都提供了默认值(如false、"<unset>"),避免首次启动时空指针;
  • ringtone键存储的是铃声 URI 字符串;checkbox、checkbox2用Boolean.valueOf(...)转为字符串展示。

界面侧,content.xml 用TableLayout+TableRow布局,每行由一个加粗标签 TextView 和一个 30sp 的取值 TextView 组成,标签与取值样式定义在 styles.xml(label样式继承value样式并追加bold与 5dip 右内边距)。

六、工程配置与运行前提

6.1 构建配置

  • app/build.gradle:com.android.application插件,compileSdkVersion 21、buildToolsVersion '26.0.2'、targetSdkVersion 17;
  • 根 build.gradle:Android Gradle 插件版本3.1.4,仓库源为google()与jcenter();
  • settings.gradle:仅包含:app模块;
  • AndroidManifest.xml:minSdkVersion="14"(API 14,即 Android 4.0),targetSdkVersion="14"。

6.2 主题适配

主题在 themes.xml 中基于 Holo 系定义(Theme.Holo.Light.DarkActionBar),同时在 values-v21/styles.xml 中为 Android 5.0+ 提供了 Material 变体,并指定colorPrimary、colorPrimaryDark、colorAccent三个主题色。运行该示例需要 API 14 以上的设备或模拟器。

七、与同目录姊妹示例的横向对照

在仓库 Prefs 目录下,cw-omnibus 围绕偏好设置提供了多组渐进式示例,方便对比不同组织方式:

示例特点
Prefs/Fragment基础版:单个PreferenceFragment,不使用 header
Prefs/FragmentsBC(本文)多分组:PreferenceActivity+ header + 多个PreferenceFragment
Prefs/NestedScreens进阶:在一个PreferenceScreen内嵌套另一个PreferenceScreen
Prefs/CustomSubtitle自定义 Fragment 副标题
Prefs/DataStore自定义偏好存储后端
Prefs/SingleHeader 与 Prefs/SingleHeader2单 header 场景,后者额外处理大屏布局

对照可见:FragmentsBC 的核心价值在于"header 驱动的多分组路由",它是 NestedScreens(单页面内嵌套)之外的另一条组织路径——把不同类别的设置拆到不同页面,更适合设置项较多、需要按模块划分的应用。

八、实战要点小结

  • 三层结构:PreferenceActivity(路由层)→ header 资源(分组层)→PreferenceFragment(内容层),每层职责单一;
  • 安全校验不能省:API 19+ 必须重写isValidFragment()并返回白名单判断,否则基于 header 的 Fragment 无法加载;
  • key 即存储键:偏好 XML 中的android:key直接决定SharedPreferences中的键名,回读时需保持一致;
  • 回读时机选 onResume:需要实时反映用户修改时,把PreferenceManager.getDefaultSharedPreferences()的读取逻辑放在onResume();
  • 数组配对:ListPreference的entries与entryValues按下标一一对应,前者面向用户、后者面向存储。

通过本文的源码级拆解,你可以直接参考 Prefs/FragmentsBC 的完整实现,在自己的应用中快速落地"一个设置入口、多组 Fragment 化偏好页面"的经典架构。

  • 示例工程
  • 教程

【免费下载链接】cw-omnibus

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

项目地址:https://gitcode.com/gh_mirrors/cw/cw-omnibus
点击查看免费下载
上一篇:手机跑大模型:30分钟用koboldcpp在Android上跑通本地AI推理(终极实战指南)
下一篇:【亲测免费】 layui-soul-table 项目常见问题解决方案

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进