本文主要通过分析源代码来分享Preference的设计和实现方式,让开发者们在今后更加顺手地使用和扩展Preference类,或者在设计其他类似的界面和功能时可以提供参考帮助。

Preference概览

Android的设置界面本质上就是ListViewPreferenceActivity是继承了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方法都可以比较直接得了解到,所以这里更关心用来控制布局的两个变量:

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

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

 1     <?xml version="1.0" encoding="utf-8"?>
2 <!-- Layout for a Preference in a PreferenceActivity. The
3 Preference is able to place a specific widget for its particular
4 type in the "widget_frame" layout. -->
5 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
6 android:layout_width="match_parent"
7 android:layout_height="wrap_content"
8 android:minHeight="?android:attr/listPreferredItemHeight"
9 android:gravity="center_vertical"
10 android:paddingEnd="?android:attr/scrollbarSize"
11 android:background="?android:attr/selectableItemBackground" >
12
13 <ImageView
14 android:id="@+android:id/icon"
15 android:layout_width="wrap_content"
16 android:layout_height="wrap_content"
17 android:layout_gravity="center"
18 />
19
20 <RelativeLayout
21 android:layout_width="wrap_content"
22 android:layout_height="wrap_content"
23 android:layout_marginStart="15dip"
24 android:layout_marginEnd="6dip"
25 android:layout_marginTop="6dip"
26 android:layout_marginBottom="6dip"
27 android:layout_weight="1">
28
29 <TextView android:id="@+android:id/title"
30 android:layout_width="wrap_content"
31 android:layout_height="wrap_content"
32 android:singleLine="true"
33 android:textAppearance="?android:attr/textAppearanceLarge"
34 android:ellipsize="marquee"
35 android:fadingEdge="horizontal" />
36
37 <TextView android:id="@+android:id/summary"
38 android:layout_width="wrap_content"
39 android:layout_height="wrap_content"
40 android:layout_below="@android:id/title"
41 android:layout_alignStart="@android:id/title"
42 android:textAppearance="?android:attr/textAppearanceSmall"
43 android:textColor="?android:attr/textColorSecondary"
44 android:maxLines="4" />
45
46 </RelativeLayout>
47
48 <!-- Preference should place its actual preference widget here. -->
49 <LinearLayout android:id="@+android:id/widget_frame"
50 android:layout_width="wrap_content"
51 android:layout_height="match_parent"
52 android:gravity="center_vertical"
53 android:orientation="vertical" />
54
55 </LinearLayout>

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

 1 protected View onCreateView(ViewGroup parent) {
2 final LayoutInflater layoutInflater =
3 (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
4
5 final View layout = layoutInflater.inflate(mLayoutResId, parent, false);
6
7 final ViewGroup widgetFrame = (ViewGroup) layout
8 .findViewById(com.android.internal.R.id.widget_frame);
9 if (widgetFrame != null) {
10 if (mWidgetLayoutResId != 0) {
11 layoutInflater.inflate(mWidgetLayoutResId, widgetFrame);
12 } else {
13 widgetFrame.setVisibility(View.GONE);
14 }
15 }
16 return layout;
17 }

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

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

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)。另外onCreateViewonBindViewprotected的方法且没有被定义为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。

 1     private static class PreferenceLayout implements Comparable<PreferenceLayout> {
2 private int resId;
3 private int widgetResId;
4 private String name;
5
6 public int compareTo(PreferenceLayout other) {
7 int compareNames = name.compareTo(other.name);
8 if (compareNames == 0) {
9 if (resId == other.resId) {
10 if (widgetResId == other.widgetResId) {
11 return 0;
12 } else {
13 return widgetResId - other.widgetResId;
14 }
15 } else {
16 return resId - other.resId;
17 }
18 } else {
19 return compareNames;
20 }
21 }
22 }

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

 1     private ArrayList<PreferenceLayout> mPreferenceLayouts;
2
3
4 private void addPreferenceClassName(Preference preference) {
5 final PreferenceLayout pl = createPreferenceLayout(preference, null);
6 int insertPos = Collections.binarySearch(mPreferenceLayouts, pl);
7
8 // Only insert if it doesn't exist (when it is negative).
9 if (insertPos < 0) {
10 // Convert to insert index
11 insertPos = insertPos * -1 - 1;
12 mPreferenceLayouts.add(insertPos, pl);
13 }
14 }
15
16 public int getItemViewType(int position) {
17 if (!mHasReturnedViewTypeCount) {
18 mHasReturnedViewTypeCount = true;
19 }
20
21 final Preference preference = this.getItem(position);
22 if (!preference.canRecycleLayout()) {
23 return IGNORE_ITEM_VIEW_TYPE;
24 }
25
26 mTempPreferenceLayout = createPreferenceLayout(preference, mTempPreferenceLayout);
27
28 int viewType = Collections.binarySearch(mPreferenceLayouts, mTempPreferenceLayout);
29 if (viewType < 0) {
30 // This is a class that was seen after we returned the count, so
31 // don't recycle it.
32 return IGNORE_ITEM_VIEW_TYPE;
33 } else {
34 return viewType;
35 }
36 }

对于这个的实现方式我比较奇怪为什么要用ArrayList不直接使用HashMap,毕竟将类名和两个整数拼接作为主键应该不算太坏。虽然ListAdapter要求getItemViewType(int position)返回的值要在0getViewTypeCount()-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结构能在屏幕上显示出来。

1     public void bind(ListView listView) {
2 listView.setOnItemClickListener(this);
3 listView.setAdapter(getRootAdapter());
4
5 onAttachedToActivity();
6 }

这个方法会被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提供的一个预定义的名字:

1     private void init(Context context) {
2 mContext = context;
3
4 setSharedPreferencesName(getDefaultSharedPreferencesName(context));
5 }

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

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

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

1     public static final String pref_file_name = "ider_hacked_preferences";
2 @Override
3 public void onCreate(Bundle savedInstanceState) {
4 super.onCreate(savedInstanceState);
5 getPreferenceManager().setSharedPreferencesName(pref_file_name);
6 addPreferencesFromResource(R.xml.preferences);
7 }

从Preference到其它

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

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

Android中的Preference结构的设计与实现的更多相关文章

  1. 浅谈android中的目录结构

    之前在android游戏开发中就遇到本地数据存储的问题:一般情形之下就将动态数据写入SD中存储,在没有SD卡的手机上就需另作处理了;再有在开发android应用的过程中,总要去调试APP,安装时又想去 ...

  2. 在Android中让Preference的宽度占满整个屏幕的宽度

    今天遇到一个问题,需要修改Preference的宽度,让其与屏幕宽度一致.搞了一上午. 终于发现Preference的这个尺寸是在PreferenceFrameLayout中设置的.通过下面这段代码, ...

  3. Android中自定义Preference

    一.需求 开发横屏设备的app时,发现preference显示的都是上下结构,因此需要自定义preference实现横屏显示. 二.layout实现 <?xml version="1. ...

  4. android中的目录结构介绍

      Google Android手机的软件为了安全性和稳定性都是默认安装到手机内存里,但是手机内存有限,所以我们会做app2sd操作,来让我们安装的软件放到sd卡上,这个操作是需要rom的支持的.   ...

  5. 《转载-两篇很好的文章整合》Android中自定义控件

    两篇很好的文章,有相互借鉴的地方,整合到一起收藏 分别转载自:http://blog.csdn.net/xu_fu/article/details/7829721 http://www.cnblogs ...

  6. Android中的树状(tree)列表

    树状列表前端挺常用的,还有人专门写过Ztree,Android中有的时候也需要使用到树状列表,上篇文章写了一下ExpandableListView,ExpandableListView最多支持两级结构 ...

  7. Android中Preference的使用以及监听事件分析

    在Android系统源码中,绝大多数应用程序的UI布局采用了Preference的布局结构,而不是我们平时在模拟器中构建应用程序时使用的View布局结构,例如,Setting模块中布局.当然,凡事都有 ...

  8. Android中UI设计的一些技巧!!!

    出处:http://blog.csdn.net/android_tutor/article/details/5995759 大家好,今天给大家分享的是Android中UI设计的一些技巧,本节内容主要有 ...

  9. Android中UI线程与后台线程交互设计的5种方法

    我想关于这个话题已经有很多前辈讨论过了.今天算是一次学习总结吧. 在android的设计思想中,为了确保用户顺滑的操作体验.一 些耗时的任务不能够在UI线程中运行,像访问网络就属于这类任务.因此我们必 ...

随机推荐

  1. LeetCode-010-正则表达式匹配

    正则表达式匹配 题目描述:给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配. '.' 匹配任意单个字符 '*' 匹配零个或多个前面的那一个元素 所谓匹配 ...

  2. 面试官:Redis中列表的内部实现方式是什么?

    在面试间里等候时,感觉这可真暖和呀,我那冰冷的出租屋还得盖两层被子才能睡着.正要把外套脱下来,我突然听到了门外的脚步声,随即门被打开,一位眉毛弯弯嘴唇红红的小姐姐走了进来,甜甜的香水味立刻钻进了我的鼻 ...

  3. (数据科学学习手札134)pyjanitor:为pandas补充更多功能

    本文示例代码及文件已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 pandas发展了如此多年,所包含的功能已 ...

  4. tp限制访问频率

    作用 通过本中间件可限定用户在一段时间内的访问次数,可用于保护接口防爬防爆破的目的. 安装 composer require topthink/think-throttle 安装后会自动为项目生成 c ...

  5. 表格的td行利用css显示...

      默认超过指定长度以...显示, 鼠标放到文本上显示全 代码如下 .fh{ max-width:220px; word-wrap:break-word; text-overflow:ellipsis ...

  6. Vmware安装Ubuntu16.4的过程及出现问题的解决

    镜像下载.域名解析.时间同步请点击 阿里云开源镜像站 1.下载Ubuntu镜像文件 Ubuntu16.4镜像文件下载地址:https://mirrors.aliyun.com/ubuntu-relea ...

  7. web服务器-nginx反向代理

    web服务器-nginx反向代理 一. 代理介绍 代理是网络中使用比较常见的, 比如我们说的最多的就是FQ软件, 比如ss, 蓝灯等这些大家常用的软件,他们就是能改代理大家访问的国内无法访问的一些国外 ...

  8. MATLAB批量打印输出600PPI的图像且图像不留空白

    一 前言 最近收到审稿人的修改意见,其中有三条:一条为<RC: There were only five images evaluated in the experiment, and I re ...

  9. 羽夏看Win系统内核——消息机制篇

    写在前面   此系列是本人一个字一个字码出来的,包括示例和实验截图.由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新. 如有好的建议,欢迎反馈.码字不易, ...

  10. 22.2.14session和反反爬处理

    22.2.14 session和反反爬处理 1.session: requests库包含session,都是用来对一个url发送请求,区别在于session是一连串的请求,在session请求过程中c ...