preference的解析与显示
一、preference的xml解析过程
解析触发流程:
在一个继承自PreferenceActivity类的子类中调用addPreferencesFromResource(R.xml.voicemail_settings);后,就开始了xml的解析过程。在方法PreferenceActivity.addPreferencesFromResource中,首先是调用PreferenceManager.inflateFromResource–PreferenceInflater.inflate得到PreferenceScreen实例引用,该PreferenceScreen中保存了从xml中解析到的所有preference元素的对象引用。
xml的解析工作主要由PreferenceInflater类完成,它继承自GenericInflater,
大致流程:
1.解析从PreferenceInflater.inflate方法开始;
2.首先是得到AttributeSet实例,大致过程:
XmlResourceParser parser = getContext().getResources().getXml(resource);
AttributeSet attrs = Xml.asAttributeSet(parser);
3.然后开始逐一解析xml文件的所有元素节点,将xml中配置的所有preference创建出对应的java实例:
3.1 首先得到根元素实例:inflate–createItemFromTag–onCreateItem
3.2 然后开始循环遍历创建每个非根元素实例:
1)inflate–rInflate–onCreateCustomFromTag——解析preference中配置的intent,并调用Preference.setIntent方法将intent保存在preference对象中。
2)inflate–rInflate–createItemFromTag–onCreateItem——这里类似创建根节点元素实例的逻辑,创建好preference对象后,调用PreferenceGroup.addItemFromInflater方法将preference引用传到PreferenceGroup(一般是PreferenceScreen)缓存,
3)rInflate–rInflate,递归调用自己,这个情况属于内嵌了PreferenceScreen或PreferenceCategory的情况,它将继续解析子PreferenceScreen或PreferenceCategory里面的preference元素。
4)最终得到下面的树形数据结构:
PreferenceScreen {
preference,preference,…,
PreferenceScreen {
preference,preference,…,
PreferenceCategory {preference,preference,… }
},
PreferenceCategory {preference,preference,…}
}
4.创建每个节点元素实例的流程:
主要逻辑在方法:
public final T createItem(String name, String prefix, AttributeSet attrs),
参数:
name:各preference类的name
prefix:程序员自定义的preference时,prefix值为null,android系统的preference,prefix值为android.preference.
attrs:这个是创建preference实例时,会传入的参数,即在preference构造方法中会使用到,用来获取xml元素节点中配置的preference的属性值。
主要逻辑代码如下,完整代码参考createItem方法:
Constructor constructor = (Constructor) sConstructorMap.get(name);
try {
if (null == constructor) {
// Class not found in the cache, see if it's real,
// and try to add it
Class clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name);
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
}
Object[] args = mConstructorArgs;
args[1] = attrs;
return (T) constructor.newInstance(args);
} catch (NoSuchMethodException e) {}
使用反射机制去创建preference的实例,首先从sConstructorMap(sConstructorMap缓存了已经使用过的preference构造方法类型实例引用)获取构造方法引用,如果没获取到,则先加载preference对应的class,然后获取具有context和AttributeSet(在上面2中创建的)两个参数的构造方法引用,怎么知道是获取两个参数的构造方法,可看mConstructorSignature的定义:
private static final Class[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};
获取到构造方法类型应用后,再以mConstructorArgs为构造方法的参数值,最终调用constructor.newInstance(args)得到preference实例。mConstructorArgs的值是如何来的,可参考代码,这里就不介绍了。在一个preference的对象创建后,构造方法引用会被缓存在sConstructorMap变量,下次使用直接从缓存获取即可。
创建preference对象还会分程序员自定义的preference或是android原生的preference,逻辑主要在GenericInflater.createItemFromTag方法,判断节点标签的name是否包含.,包含则是用户自定义的preference,不包含,则是使用android系统的preference,代码如下:
if (-1 == name.indexOf('.')) {
item = onCreateItem(name, attrs);
} else {
item = createItem(name, null, attrs);
}
name不包含点符号时,则它不是一个java类全路径,这时需要有系统preference默认的包名,这是在PreferenceInflater的构造方法执行时会调用setDefaultPackage(“android.preference.”);传入了默认的package名,赋值给变量mDefaultPackage。
以上差不多就是将preference从xml解析出来的全过程了。
二、preference显示过程
回到最开始的PreferenceManager.inflateFromResource方法调用,得到了PreferenceScreen引用后,就开始进行显示流程的逻辑调用了。
调用PreferenceActivity.setPreferenceScreen–PreferenceManager.setPreferences,将得到的PreferenceScreen引用保存在PreferenceManager的变量mPreferenceScreen中。当mPreferenceScreen不为null,则会继续调用PreferenceActivity.postBindPreferences–使用handler发送MSG_BIND_PREFERENCES消息,handleMessage方法中进而调用bindPreferences–PreferenceScreen.bind,bind代码如下:
public void bind(ListView listView) {
listView.setOnItemClickListener(this);
listView.setAdapter(getRootAdapter());
onAttachedToActivity();
}
这个方法首先给listview设置了点击事件,然后是将解析xml得到的PreferenceScreen及其包含的各子preference最终显示到listview中,每个preference就会形成listview的一个item。
将解析到的preference变成listview的每个item的数据,这个工作就在getRootAdapter方法之后的逻辑中,调用关系如下:
PreferenceScreen.getRootAdapter–onCreateRootAdapter–new PreferenceGroupAdapter–syncMyPreferences–flattenPreferenceGroup,
在flattenPreferenceGroup方法中,会将根元素PreferenceScreen内包含的preference都add到mPreferenceList中,mPreferenceList用来缓存待显示的preference,对于PreferenceGroup类型的情况,会有如下情况:
1)如果preference是PreferenceGroup类型,但不是PreferenceScreen,那么就会将它内部包含的preference也会add到mPreferenceList中;
2)如果preference是内嵌的子PreferenceScreen类型,它里面包含的子preference则不会add到mPreferenceList,内嵌的PreferenceScreen会以一个独立的界面来显示。
这样的话,flattenPreferenceGroup方法得到了mPreferenceList内容,再回到方法syncMyPreferences,之后会调用notifyDataSetChanged,调用它后,会执行BaseAdapter.notifyDataSetChanged–AbsListView(ListView的父类).AdapterDataSetObserver.onChanged–AdapterView<ListAdapter>.AdapterDataSetObserver.onChanged–requestLayout(),这样就触发了ListView的显示流程,先会执行onMeasure,onLayout等方法,之后执行PreferenceGroupAdapter.getView方法,这个方法将会从mPreferenceList中获取到对应位置的preference,进而调用Preference.getView–onCreateView–onBindView等方法,Preference.getView调用完成后,每个item的内容显示就完成了,最终每个item的内容显示到view中都是在Preference自己的方法中完成的。
这大概就是preference的显示过程了。
三、内嵌子PreferenceScreen的点击事件
首先看下preference的点击事件。
首先上面已经说到每个preference最终是作为ListView的一个item显示的,看下每个item的点击事件监听器代码PreferenceScreen.bind方法:
public void bind(ListView listView) {
listView.setOnItemClickListener(this);
listView.setAdapter(getRootAdapter());
onAttachedToActivity();
}
可以看到是PreferenceScreen自身作为监听器,然后看下onItemClick方法:
public void onItemClick(AdapterView parent, View view, int position, long id) {
// If the list has headers, subtract them from the index.
if (parent instanceof ListView) {
position -= ((ListView) parent).getHeaderViewsCount();
}
Object item = getRootAdapter().getItem(position);
if (!(item instanceof Preference)) return;
final Preference preference = (Preference) item;
preference.performClick(this);
}
最终是调用了Preference.performClick方法,该方法代码如下:
public void performClick(PreferenceScreen preferenceScreen) {
if (!isEnabled()) {
return;
}
onClick();
if (mOnClickListener != null && mOnClickListener.onPreferenceClick(this)) {
return;
}
PreferenceManager preferenceManager = getPreferenceManager();
if (preferenceManager != null) {
PreferenceManager.OnPreferenceTreeClickListener listener = preferenceManager
.getOnPreferenceTreeClickListener();
if (preferenceScreen != null && listener != null
&& listener.onPreferenceTreeClick(preferenceScreen, this)) {
return;
}
}
if (mIntent != null) {
Context context = getContext();
context.startActivity(mIntent);
}
}
从这个方法可看出有5种处理情况,按照先后顺序总结如下:
1.isEnabled方法的值,为false时,则不处理点击,直接return,相反则继续执行后面的逻辑;这个是由setEnabled方法设置的值决定的,或者在xml配置了android:enabled="true/false"决定。
2.onClick()方法被执行;当1未return时,这个方法会无条件执行,稍后看这个方法,Preference.onClick是一个空方法,主要是子类可覆盖该方法,实现有关逻辑调用。
3.mOnClickListener不为null时,则执行mOnClickListener.onPreferenceClick,这个是调用Preference.setOnPreferenceClickListener设置了监听后会执行,如果执行了它,则后面的不执行了。
4.PreferenceManager.OnPreferenceTreeClickListener.onPreferenceTreeClick,当这个执行返回了true,表示消费了该事件,则直接return。为false则继续执行后面的逻辑;这个其实是由PreferenceActivity.onPreferenceTreeClick的返回值来决定的,实际操作中,我们可以通过重写该方法来处理点击事件。
5.当mIntent 不为null,则调用context.startActivity(mIntent),进入到intent找到的界面。这个情况可以是在在xml中定义preference时配置intent,也可以在代码中调用Preference.setIntent来设置intent。
再来看PreferenceScreen,它属于Preference的子类,它重写了onClick方法,代码如下:
@Override
protected void onClick() {
if (getIntent() != null || getFragment() != null || getPreferenceCount() == 0) {
return;
}
showDialog(null);
}
由上面代码可知,当一个内嵌的子PreferenceScreen未指定fragment,也未配置intent,并且还有子preference元素时,将调用showDialog(null);,这个方法会使PreferenceScreen里面的元素以一个Dialog来显示一个新的界面,只不过这个Dialog是全屏的。
showDialog方法比较长,关键的代码如下:
Dialog dialog = mDialog = new Dialog(context, context.getThemeResId());
dialog.setTitle(title);
dialog.setContentView(childPrefScreen);
dialog.setOnDismissListener(this);
// Add the screen to the list of preferences screens opened as dialogs
getPreferenceManager().addPreferencesScreen(dialog);
dialog.show();
可以看到完全就是一个Dialog显示逻辑,这个dialog显示不会有像AlertDialog一样的效果,它显示的效果和activity一样。
这就是内嵌的子PreferenceScreen的显示原理,它不会启动一个新的activity,而是以dialog来展示的。