了解Android中的Preference结构的设计与实现

article/2025/11/4 22:21:09

本文不会涉及这些Preference的使用方式,比如如何定义XML文件、如何使用PreferenceActivity和PreferenceFragment加载设置,这些都可以在Android Developer的官方指南里了解到详情。本文主要通过分析源代码来分享Preference的设计和实现方式,让开发者们在今后更加顺手地使用和扩展Preference类,或者在设计其他类似的界面和功能时可以提供参考帮助。

Preference概览

Android的设置界面本质上就是ListView:PreferenceActivity是继承了ListActivity;而3.0以后推荐使用的PreferenceFragment虽然没有继承ListFragment,但也定义了ListView字段。


跟ListView搭配使用的就是实现了ListAdapter接口的对象,其中的关键又是在 getView (int position, View convertView, ViewGroup parent)。一开始我猜想Preference实现相当于一个自定义的View,所以它可能继承于View或者ViewGroup,这样扩展类就直接往里面加需要的额外的view就好了。结果发现Preference什么也没有继承,只是实现了一个Compareble接口用来比较两个对象,以便可以按序显示内容。不过Preference也不是什么都没有包含,它存储了相应Preference的布局信息以及当前状态。所以Preference相对于整个设置列表来说可以算是子项目(item),而非子视图(view)。

Preference不是View,但也包含View

对于Preference的状态信息,通过setEnabled (boolean enabled)、setTitle (CharSequence title)这些API方法都可以比较直接得了解到,所以这里更关心用来控制布局的两个变量:

private int mLayoutResId = com.android.internal.R.layout.preference;
private int mWidgetLayoutResId;

对于mLayoutResId的默认值,翻看源代码就可以看到它所对应的XML结构如下(不包含Copyright注释):

<?xml version="1.0" encoding="utf-8"?>
<!-- Layout for a Preference in a PreferenceActivity. ThePreference is able to place a specific widget for its particulartype in the "widget_frame" layout. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"android:layout_height="wrap_content"android:minHeight="?android:attr/listPreferredItemHeight"android:gravity="center_vertical"android:paddingEnd="?android:attr/scrollbarSize"android:background="?android:attr/selectableItemBackground" ><ImageView
        android:id="@+android:id/icon"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"/><RelativeLayout
        android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginStart="15dip"android:layout_marginEnd="6dip"android:layout_marginTop="6dip"android:layout_marginBottom="6dip"android:layout_weight="1"><TextView android:id="@+android:id/title"android:layout_width="wrap_content"android:layout_height="wrap_content"android:singleLine="true"android:textAppearance="?android:attr/textAppearanceLarge"android:ellipsize="marquee"android:fadingEdge="horizontal" /><TextView android:id="@+android:id/summary"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_below="@android:id/title"android:layout_alignStart="@android:id/title"android:textAppearance="?android:attr/textAppearanceSmall"android:textColor="?android:attr/textColorSecondary"android:maxLines="4" /></RelativeLayout><!-- Preference should place its actual preference widget here. --><LinearLayout android:id="@+android:id/widget_frame"android:layout_width="wrap_content"android:layout_height="match_parent"android:gravity="center_vertical"android:orientation="vertical" /></LinearLayout>

通过源代码和上边的注释也能了解到@+android:id/widget_frame其实为mWidgetLayoutResId所对应的布局预留了空间。

protected View onCreateView(ViewGroup parent) {final LayoutInflater layoutInflater =(LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);final View layout = layoutInflater.inflate(mLayoutResId, parent, false); final ViewGroup widgetFrame = (ViewGroup) layout.findViewById(com.android.internal.R.id.widget_frame);if (widgetFrame != null) {if (mWidgetLayoutResId != 0) {layoutInflater.inflate(mWidgetLayoutResId, widgetFrame);} else {widgetFrame.setVisibility(View.GONE);}}return layout;
}

这解释了为什么设置上的所有项目看起来都是一样的:图标、标题、子标题;然后右侧额外的可交互控件,比如checkbox、switchbutton。再查看Android系统配置的style可以看到只有PreferenceCategory用的是不同的布局preference_category,其XML文件的内容也就只有一个TextView:

<?xml version="1.0" encoding="utf-8"?>
<!-- Layout used for PreferenceCategory in a PreferenceActivity. -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"style="?android:attr/listSeparatorTextViewStyle"android:id="@+android:id/title"
/>

Preference不是View,但能创建View

有ListView,就一定会有ListAdapter的子类出现,在Preference里这个子类就是PreferenceGroupAdapter。可惜它被定义成包可见,所以在API文档中无法找到对它的介绍。就像之前已说到的ListAdapter最关键的还是看getView (int position, View convertView, ViewGroup parent)是怎么创建出View来。不过通过源码一看,PreferenceGroupAdapter并不像传统的如ArrayAdapter、SimpleAdatapter那样在其内部实现View的创建和数据绑定工序,还是将该流程转给了Preference去完成。这也就是之前看到的那段onCreateView(ViewGroup parent),以及getView (View convertView, ViewGroup parent)和onBindView (View view)。另外onCreateView和onBindView是protected的方法且没有被定义为final,所以自定义Preference子类的时候可以覆盖这两个方法来返回不同View,这样就大大增强了扩展性,不用去修改ListAdapter的实现了。

ListAdapter为了能够合理地复用View所以要求对每个View的提供指示其类型的整数:getItemViewType(int position)这样对于相同类型的View就可以服用在不同的项目上呈现内容了,节省很多内存。既然Android定义了那么多不同的Preference类,PreferenceGroupAdapter自然也需要针对每个Preference提供可靠的类型。考虑到设置界面的项目数目上一般都是固定的而且不会特别长,所以即使不做任何复用对性能上也不会有太大影响。不过Android系统还是对其做了这方面的优化,根据Preference的实际类名以及上边提到两个跟布局相关的字段的值映射到PreferenceLayout上,如果两个Preference对应的PreferenceLayout相同那么就这两个就被认定为类型相同。因此如果自定义Preference是不想用系统提供的布局结构,也要注意通过setLayoutResource (int layoutResId)和setWidgetLayoutResource (int widgetLayoutResId)来覆盖其上的值以防止复用了错误的View。

private static class PreferenceLayout implements Comparable<PreferenceLayout> {private int resId;private int widgetResId;private String name;public int compareTo(PreferenceLayout other) {int compareNames = name.compareTo(other.name);if (compareNames == 0) {if (resId == other.resId) {if (widgetResId == other.widgetResId) {return 0;} else {return widgetResId - other.widgetResId;}} else {return resId - other.resId;}} else {return compareNames;}}
}

PreferenceGroupAdapter将PreferenceLayout存储在ArrayList中,然后通过二分查找,来确认是否需要添加新的成员以及其在数组中的位置用以指示其类型。

private ArrayList<PreferenceLayout> mPreferenceLayouts;private void addPreferenceClassName(Preference preference) {final PreferenceLayout pl = createPreferenceLayout(preference, null);int insertPos = Collections.binarySearch(mPreferenceLayouts, pl);// Only insert if it doesn't exist (when it is negative).if (insertPos < 0) {// Convert to insert indexinsertPos = insertPos * -1 - 1;mPreferenceLayouts.add(insertPos, pl);}
}public int getItemViewType(int position) {if (!mHasReturnedViewTypeCount) {mHasReturnedViewTypeCount = true;}final Preference preference = this.getItem(position);if (!preference.canRecycleLayout()) {return IGNORE_ITEM_VIEW_TYPE;}mTempPreferenceLayout = createPreferenceLayout(preference, mTempPreferenceLayout);int viewType = Collections.binarySearch(mPreferenceLayouts, mTempPreferenceLayout);if (viewType < 0) {// This is a class that was seen after we returned the count, so// don't recycle it.return IGNORE_ITEM_VIEW_TYPE;} else {return viewType;}
}

对于这个的实现方式我比较奇怪为什么要用ArrayList不直接使用HashMap,毕竟将类名和两个整数拼接作为主键应该不算太坏。虽然ListAdapter要求getItemViewType(int position)返回的值要在0到getViewTypeCount()-1,使用ArrayList会有保障。但即使是用HashMap也可以用一个从0开始的自增量,每当添加了新的PreferenceLayout就将自增量的值作为值然后自增。毕竟PreferenceLayout的顺序关系应该对Preference的呈现没有什么影响,只是为了二分查找才要保持其顺序。不过之前也提到设置页面上的东西不会特别多,所以二分查找的效率也接近HashMap的O(1)。

Preference的组织结构

Preference为整个设置界面的结构提供了基本内容和操作,PreferenceGroupAdapter也打通了与ListView的连接。另一方面Android系统也提供了很多预定义的Preference类型,下边就是目前可供选择的全部类型和继承关系。

  • Preference
    • DialogPreference
      • EditTextPreference
      • ListPreference
      • MultiCheckPreference
      • MultiSelectListPreference
      • SeekBarDialogPreference
        • VolumePreference
      • YesNoPreference*
    • PreferenceGroup
      • PreferenceCategory
      • PreferenceScreen
    • RingtonePreference
    • SeekBarPreference
    • TwoStatePreference
      • CheckBoxPreference
      • SwitchPreference

(*YesNoPreference是唯一定义在com.android.internal.preference包下的类,因为无法被外部使用所以不清楚其具体作用。)

这些Preference的子类(不算抽象类)我可以将他们分成两类:Group其下的Category、Screen算为组织型,因为他们主要是定义Preference的层次结构;其他的则是应用型,因为它们是真正与用户交互来获得偏好信息。

这里只来讨论PreferenceScreen,因为在定义XML的时候PreferenceScreen是根元素,PreferenceGroupAdapter的对象实例也是存放在PreferenceScreen之中,所以在整个Preference结构设计中它起着相当关键的作用。我觉得其中一个重要的方法就是bind(ListView listView),它让整个Preference结构能在屏幕上显示出来。

public void bind(ListView listView) {listView.setOnItemClickListener(this);listView.setAdapter(getRootAdapter());onAttachedToActivity();
}

这个方法会被PreferenceActivity和PreferenceFragment调用,并用它们存储的ListView对象作为参数,进而获得了所需的PreferenceGroupAdapter。因此PreferenceActivity父类ListActivity上的ListAdapter对象其实从来没被用到过一直是null。这也多少能解释为什么后来设计的PreferenceFragment没有继承ListActivity,只是自己实现的必要的部分。

如果PreferenceScreen是以子项目出现在列表上的话,点击它会呈现出另一个列表,不过这个列表是呈现在Dialog上而非新的Activity或Fragment。同样的新列表也会通过上边的bind(ListView listView)方法来和PreferenceGroupAdapter绑定上。

衔接前后的PreferenceManager

介绍过了在背后存储设置内容的SharedPreferences,了解了在前台展示的界面的Preference,最后再来讲讲衔接两者的PreferenceManager。PreferenceManager在PreferenceActivity或PreferenceFragment中被创建被作为其属性,然后共享给包含的Preference树结构。所以同一个设置类别使用着同一个PreferenceManager对象,而PreferenceManager里则有一个SharedPreferences对象帮助写入偏好到相应的XML文件中。

已经知道SharedPreferences的构造方法需要指定对应XML的文件名,对于Preference中所使用的SharedPreferences的文件名,取的时Android提供的一个预定义的名字:

private void init(Context context) {mContext = context;setSharedPreferencesName(getDefaultSharedPreferencesName(context));
}

而这个预定的是程序的包名加上后缀”_preferences“,所以在使用自定义名字的时候尽量避开这种形式以免存储冲突。当然如果是想读取设置信息来做执行的判定条件那是应该使用PreferenceManager上的getDefaultSharedPreferences (Context context)。

private static String getDefaultSharedPreferencesName(Context context) {return context.getPackageName() + "_preferences";
}

不过的PreferenceManager初始化时只是设定了名字参数,真正的SharedPreferences是在Preference首次读或写键值对时才被创建,因此如果希望设置的参数存储在不同的文件名下,可以在PreferenceActivity或PreferenceFragment的onCreat()方法里调用PreferenceManager的setSharedPreferencesName (String sharedPreferencesName)来完成自定义化。

public static final String pref_file_name = "ider_hacked_preferences";
@Override
public void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);getPreferenceManager().setSharedPreferencesName(pref_file_name);addPreferencesFromResource(R.xml.preferences);
}

从Preference到其它

了解Preference的设计和实现可以为今后的开发和架构提供一定的参考。比如在布局的设计上,为了保持相对得统一可以固定整体然后留出局部的占位区间做差异化;实现ListAdapter的时候不一定要使用switch…case的结构来决定需要用返回哪种View,将它留给项目类则可以大大增加扩展性。SharedPreferences中也体会到读取和写入被分成两个类的好处,而它又与Preference行程了界面与存储的分离,再通过PreferenceManager衔接,对于这样的设计,完全可以再实现出这几个继承类,让内容比其它格式存储,比如XML、SQLite。

总之,Android的开源性让开发者能够方便地学习到其中的设计理念,虽然它的整体设计上经过了那么多的版本可能依然有许多不足(比如让我困惑的在PreferenceGroupAdapter里使用二分查找),但还是可以学习到不少的开发思想。

原地址


http://chatgpt.dhexx.cn/article/uyqdPc0f.shtml

相关文章

Android设置界面_Preference

AndroidPreference 基本上每个应用都有一个设置(首选项)界面, Google其实提供了默认的设置界面实现方式. 介绍下 Preference该类拥有多个直接或间接的子类, 这些子类可以组成不同内容的首选项界面. 和一般界面不同的 关键类: Preference 普通 RingtonePreference 铃声Chec…

Android Preference 卡片圆角风格定制

效果图 实现步骤 在网上查找这块的资料&#xff0c;发现并未找到相关的&#xff0c;大多都是通过修改 Preference style 来设置背景色什么的&#xff0c;和我们预想的 效果不太一样&#xff0c;那就去看看 Preference 源码吧&#xff0c;说不定能有什么收获。 先看下 Prefer…

Preference,PreferenceCategory,PreferenceList,PreferenceCheckBox等控件的简单讲解

有人会纳闷&#xff0c;为什么不使用普通的控件去写settings页面&#xff0c;非得要preference来写&#xff0c;这是有原因的&#xff0c;Preference可以自动保存上一次操作的值&#xff0c;并且preference会自动将自己的值保存在shared Preference里面&#xff0c;而preferenc…

Android中preference标签的使用

现在做公司任务的时候&#xff0c;经常会要去读Settings的源码&#xff0c;然后发现在xml文件中几乎全是用的preferenceScreen和preferenceCategory标签&#xff0c;很少有用布局和控件的&#xff0c;然后我就自己上网看了很多有关的资料&#xff0c;在此总结下。 首先在res目录…

Android 之Preference控件

简述 Preference是Android的控件之一&#xff0c;相对来说我们用的比较少&#xff0c;但在系统应用的Settings设置应用模块中大部分由Preference控件组成。 主要成分 Preference主要角色是子控件&#xff0c;PreferenceCategory相当于LinearLayout和Relative layout&#xff0c…

使用jxls导出报错:Connot load XLS transformer please make sure a Transformer implementation is in classpath

使用jxls导出是报错&#xff1a; java.lang.IllegalStateException: Cannot load XLS transformer. Please make sure a Transformer implementation is in classpath 仔细排查&#xff0c;是因为批注的问题&#xff1a; 是因为批注放到第二个单元格去了&#xff0c;扫描不到区…

XLSTransformer生成excel文件案例

项目结构图&#xff1a; 项目中所用到的jar&#xff0c;可以到http://www.findjar.com/index.x下载 ExcelUtil类源码&#xff1a; package util; import java.io.IOException; import java.net.URL; import java.util.HashMap; import java.util.List; import java.u…

XLSTransformer 导出Excel数据

先上代码 java code: <pre name"code" class"java">package cn.export.util; import java.io.FileInputStream; import java.net.URLEncoder; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.Serv…

Java XLSTransformer生成excel文件

把页面的数据导出excel 数据 然后进入 struts2 action <action name"generateExcel" class"com.xx.emidas.activity.activity.ajax.XLSTransformerGenerateExcelAction"></action> package com.xx.emidas.activity.activity.ajax;import co…

XLSTransformer生成excel文件简单示例

项目结构图&#xff1a; 项目中所用到的jar&#xff0c;可以到http://www.findjar.com/index.x下载 ExcelUtil类源码&#xff1a; [java] view plain copy package util; import java.io.IOException; import java.net.URL; import java.util.HashMap; import java.uti…

XLSTransformer生成excel文件简单演示样例

项目结构图&#xff1a; 项目中所用到的jar&#xff0c;能够到http://www.findjar.com/index.x下载 ExcelUtil类源代码&#xff1a; package util;import java.io.IOException; import java.net.URL; import java.util.HashMap; import java.util.List; import java.util.Map;i…

XLSTransformer+模板 导出 Excel

利用excel模板来导出excel文件&#xff0c; 十分方便 RequestMapping(value "/download/vin", method RequestMethod.GET)ResponseBodypublic ResBody exportExcel(HttpServletRequest request) {String path request.getSession().getServletContext().getRealPa…

XLSTransformer.transformWorkbook导出excel不能将列导出完整问题

生产就项目用到jxls加poi导出excel文件 //模拟数据 List data new ArrayList<>(); Map<String,Object> excelMap new HashMap<>();excelMap.put("ro", data ); //项目用到的导出代码 String fileName SystemConst.WEB_ROOT_PATH File.separat…

Jxls异常 Cannot load XLS transformer. Please make sure a Transformer impleme

错误&#xff1a;Cannot load XLS transformer. Please make sure a Transformer implementation is in classpath 原因&#xff1a; 这是 jxls 的版本升级2.4后需要额外导入其他依赖 还要在plugins中加上以下插件 <plugin><groupId>org.apache.maven.plugins</…

Excel之XLSTransformer

前言 利用该工具类可以向excel模板中写入数据&#xff0c;而不用写过多代码画excel 引入依赖 java <dependency> <groupId>net.sf.jxls</groupId> <artifactId>jxls-core</artifactId> <version>1.0.3</version> </dependency>…

使用XLSTransformer生成报表的步骤和流程

使用XLSTransformer生成XLS报表的步骤和流程&#xff1a; 需要引入的jar包&#xff1a; jxls-core-0.9.7.jar jxls-reader-0.9.7.jar poi-3.6.jar commons-jexl-1.1.jar commons-digester-2.0.jar commons-beanutil-core-1.8.3.jar commons-collection.jar 示例代码…

利用模板导出文件(一)之XLSTransformer导出excel文件

由于现在好多公司都在实行办公无纸化操作&#xff0c;所以一般都是使用excel以及word来办公&#xff0c;本文是公司项目中使用excel文件模板生成对应的文件&#xff1a; 首先&#xff0c;需要导入一下几个包&#xff1a; 接下来就是具体的代码&#xff1a; import java.io.Fil…

关于使用XLSTransformer.transformXLS导出Excel表格中遇到的问题

1. 需求&#xff1a;最近拿到的一个任务&#xff0c;是将订单列表导出&#xff0c;按照订单列表导出&#xff08;包括筛选条件&#xff09;。 背景&#xff1a;由于原本的订单列表查询代码太过繁重&#xff0c;里面夹杂的逻辑较多&#xff0c;再有一个是自己想尽快的熟悉公司…

最简单方便的excel导出方式

前言&#xff1a;开发各种统计系统以及报表系统之类的不可避免的就是导出excel功能&#xff0c;传统的poi用过的都知道&#xff0c;每个sheet每个row都需要去定义去美化&#xff0c;繁琐不说还很浪费时间&#xff0c;最近本人发现两种较快速的方法&#xff0c;一种是excel模版方…

Handler、Looper、HandleThread、ActivityThread简介

1. Handler 源码位于&#xff1a; platform/frameworks/base/core/java/android/os/Handler.java Handler允许发送和处理与线程的MessageQueue关联的Message和Runnable对象。每个Handler实例都与单个线程和该线程的消息队列相关联。 当创建一个新的Handler时&#xff0c;它会被…