之前的文章讲到Preference和Setting在本质上是相似的,并相信介绍了SharedPreferences的基本使用方式和其代码设计。但SharedPreferences只能算是设备背后的不可见的存储那一部分,而可见的界面其实对于用户来说更加重要,而这一部分就是本文要讲的那些在android.preference包下的各种Preference。
不过本文不会涉及这些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方法都可以比较直接得了解到,所以这里更关心用来控制布局的两个变量:
[codesyntax lang=”java” lines=”normal”]
private int mLayoutResId = com.android.internal.R.layout.preference; private int mWidgetLayoutResId;
[/codesyntax]
对于mLayoutResId
的默认值,翻看源代码就可以看到它所对应的XML结构如下(不包含Copyright注释):
[codesyntax lang=”xml” lines=”normal”]
<?xml version="1.0" encoding="utf-8"?> <!-- Layout for a Preference in a PreferenceActivity. The Preference is able to place a specific widget for its particular type 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>
[/codesyntax]
通过源代码和上边的注释也能了解到@+android:id/widget_frame
其实为mWidgetLayoutResId
所对应的布局预留了空间。
[codesyntax lang=”java”]
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; }
[/codesyntax]
这解释了为什么设置上的所有项目看起来都是一样的:图标、标题、子标题;然后右侧额外的可交互控件,比如checkbox、switchbutton。再查看Android系统配置的style可以看到只有PreferenceCategory用的是不同的布局preference_category
,其XML文件的内容也就只有一个TextView:
[codesyntax lang=”xml” lines=”normal”]
<?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" />
[/codesyntax]
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。
[codesyntax lang=”java” lines=”normal”]
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; } } }
[/codesyntax]
PreferenceGroupAdapter将PreferenceLayout存储在ArrayList中,然后通过二分查找,来确认是否需要添加新的成员以及其在数组中的位置用以指示其类型。
[codesyntax lang=”java” lines=”normal”]
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 index insertPos = 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; } }
[/codesyntax]
对于这个的实现方式我比较奇怪为什么要用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
- DialogPreference
(*YesNoPreference是唯一定义在com.android.internal.preference
包下的类,因为无法被外部使用所以不清楚其具体作用。)
这些Preference的子类(不算抽象类)我可以将他们分成两类:Group其下的Category、Screen算为组织型,因为他们主要是定义Preference的层次结构;其他的则是应用型,因为它们是真正与用户交互来获得偏好信息。
这里只来讨论PreferenceScreen
,因为在定义XML的时候PreferenceScreen是根元素,PreferenceGroupAdapter的对象实例也是存放在PreferenceScreen之中,所以在整个Preference结构设计中它起着相当关键的作用。我觉得其中一个重要的方法就是bind(ListView listView)
,它让整个Preference结构能在屏幕上显示出来。
[codesyntax lang=”java” lines=”normal”]
public void bind(ListView listView) { listView.setOnItemClickListener(this); listView.setAdapter(getRootAdapter()); onAttachedToActivity(); }
[/codesyntax]
这个方法会被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提供的一个预定义的名字:
[codesyntax lang=”java” lines=”normal”]
private void init(Context context) { mContext = context; setSharedPreferencesName(getDefaultSharedPreferencesName(context)); }
[/codesyntax]
而这个预定的是程序的包名加上后缀”_preferences
“,所以在使用自定义名字的时候尽量避开这种形式以免存储冲突。当然如果是想读取设置信息来做执行的判定条件那是应该使用PreferenceManager上的getDefaultSharedPreferences (Context context)
。
[codesyntax lang=”java” lines=”normal”]
private static String getDefaultSharedPreferencesName(Context context) { return context.getPackageName() + "_preferences"; }
[/codesyntax]
不过的PreferenceManager初始化时只是设定了名字参数,真正的SharedPreferences是在Preference首次读或写键值对时才被创建,因此如果希望设置的参数存储在不同的文件名下,可以在PreferenceActivity或PreferenceFragment的onCreat()
方法里调用PreferenceManager的setSharedPreferencesName (String sharedPreferencesName)
来完成自定义化。
[codesyntax lang=”java” lines=”normal”]
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); }
[/codesyntax]
从Preference到其它
了解Preference的设计和实现可以为今后的开发和架构提供一定的参考。比如在布局的设计上,为了保持相对得统一可以固定整体然后留出局部的占位区间做差异化;实现ListAdapter的时候不一定要使用switch...case
的结构来决定需要用返回哪种View,将它留给项目类则可以大大增加扩展性。SharedPreferences中也体会到读取和写入被分成两个类的好处,而它又与Preference行程了界面与存储的分离,再通过PreferenceManager衔接,对于这样的设计,完全可以再实现出这几个继承类,让内容比其它格式存储,比如XML、SQLite。
总之,Android的开源性让开发者能够方便地学习到其中的设计理念,虽然它的整体设计上经过了那么多的版本可能依然有许多不足(比如让我困惑的在PreferenceGroupAdapter里使用二分查找),但还是可以学习到不少的开发思想。