深入Android Layout XML属性
前面我们的XmlPullParser解析xml的简要教程中, 我们对于Android是如何解析Layout XML的过程有了直观的理解, 我们也分析了inflate的详细过程. 另外我们还开始研究控件的构造过程,大家对于AttributeSet, TypedArray等结构也有了一些了解. 不过有同学反映还是隔靴搔痒,还是缺少足够深入的理解. 所以我们继续做一个从摇篮到坟墓的教程.
XmlPullParser读取属性快餐教程
前面我们学习了XmlPullParser读取XML的过程, 在获取了TAG事件后,比如在处理XmlPullParser.START_TAG事件的时候,我们就可以将属性也一起处理掉了.
XmlPullParser接口属性支持相关的方法
主要用下面4个方法:
- int getAttributeCount(); 获取一个有多少个属性
- String getAttributeName (int index); 获取第index属性的名字
- String getAttributeValue(int index); 获取第index属性的值
- String getAttributeValue(String namespace,String name); 根据属性的名字获取值
但是XmlPullParser的API不足在于:一是还需要做类型转换,二是还跟资源相关的还需要另行处理.
于是Android就新引入了一个AttributeSet接口,专门处理属性相关的功能.
我们下面写一个小例子遍历这个标签的所有属性的代码来看一下:
public static void logAttribute(XmlPullParser xmlPullParser) {
AttributeSet a = Xml.asAttributeSet(xmlPullParser);
logAttribute(a);
}
public static void logAttribute(AttributeSet a) {
int attrCount = a.getAttributeCount();
Log.d(TAG, "[Xulun]Attribute count=" + attrCount);
if (attrCount > 0) {
for (int i = 0; i < attrCount; i++) {
Log.d(TAG, "[Xulun]Attribute[" + i + "]name=" + a.getAttributeName(i));
Log.d(TAG, "[Xulun]Attribute[" + i + "]value=" + a.getAttributeValue(i));
}
}
}
上面的代码我分成两个方法的原因在于正好分别说明要用到的两个步骤.
Xml.asAttributeSet
这个方法在前面我们曾经简要介绍过, 这里再重温一下:
175 public static AttributeSet asAttributeSet(XmlPullParser parser) {
176 return (parser instanceof AttributeSet)
177 ? (AttributeSet) parser
178 : new XmlPullAttributes(parser);
179 }
前面之所以没有展开讲, 是因为我们说过, 解析资源所用的是XmlResourceParser,同时继承了XmlPullParser和AttributeSet, 所以走177行那一支, 直接转换一下接口类型就可以用了. 而我们这次讲一下第二个分支, 通过XmlPullAttributes类来作为桥,转换一下.
XmlPullAttributes
我们选取一些关键点看一下,其余的以此类推.
构造
28class XmlPullAttributes implements AttributeSet {
29 public XmlPullAttributes(XmlPullParser parser) {
30 mParser = parser;
31 }
因为就是做一个中间转换的工具,其核心还是一个XmlPullParser接口的对象.
方法的封装
上面我们介绍的4个方法,AttributeSet是同样的接口,所以原封不动地调用就好了.
33 public int getAttributeCount() {
34 return mParser.getAttributeCount();
35 }
36
37 public String getAttributeName(int index) {
38 return mParser.getAttributeName(index);
39 }
40
41 public String getAttributeValue(int index) {
42 return mParser.getAttributeValue(index);
43 }
44
45 public String getAttributeValue(String namespace, String name) {
46 return mParser.getAttributeValue(namespace, name);
47 }
另外还有一个有用的方法,是在出错时可以看到是在xml的哪一行:
49 public String getPositionDescription() {
50 return mParser.getPositionDescription();
51 }
带类型转换的方法
先把值读出来,然后再通过XmlUtils或者其他方法实现类型的转换. 这样可以方便调用者,省得像直接用XmlPullParser接口,还得自己转一次.
例:
102 public boolean getAttributeBooleanValue(int index, boolean defaultValue) {
103 return XmlUtils.convertValueToBoolean(
104 getAttributeValue(index), defaultValue);
105 }
XmlUtils是一个Android的内部类,类型转换这些没什么值得多说的,例行公事.
74 public static final boolean
75 convertValueToBoolean(CharSequence value, boolean defaultValue)
76 {
77 boolean result = false;
78
79 if (null == value)
80 return defaultValue;
81
82 if (value.equals("1")
83 || value.equals("true")
84 || value.equals("TRUE"))
85 result = true;
86
87 return result;
88 }
资源相关的方法
由于id, class, style三种属性被用得太多了, 于是专门为它们定义了命名的方法.
130 public String getIdAttribute() {
131 return getAttributeValue(null, "id");
132 }
133
134 public String getClassAttribute() {
135 return getAttributeValue(null, "class");
136 }
137
138 public int getIdAttributeResourceValue(int defaultValue) {
139 return getAttributeResourceValue(null, "id", defaultValue);
140 }
141
142 public int getStyleAttribute() {
143 return getAttributeResourceValue(null, "style", 0);
144 }
后面两个方法是对getAttributeResourceValue方法的封装,也只是将资源ID转换成int型.
69 public int getAttributeResourceValue(String namespace, String attribute,
70 int defaultValue) {
71 return XmlUtils.convertValueToInt(
72 getAttributeValue(namespace, attribute), defaultValue);
73 }
最后还有一个方法通过XmlPullParser无法实现的, 这个要求的是编译后的属性对应的属性名字. 这个没办法,不是通用接口可以处理的问题, 需要去查询ResXMLTree了.
53 public int getAttributeNameResource(int index) {
54 return 0;
55 }
56
TypedArray
资源值存储到TypedArray中
下面我们回到真实的资源世界,再重温一下我们之前看过的View的构造时对AttributeSet的使用:
3897 public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
3898 this(context);
3899
3900 final TypedArray a = context.obtainStyledAttributes(
3901 attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
3902
...
3949 final int N = a.getIndexCount();
3950 for (int i = 0; i < N; i++) {
3951 int attr = a.getIndex(i);
3952 switch (attr) {
3953 case com.android.internal.R.styleable.View_background:
3954 background = a.getDrawable(attr);
3955 break;
...
Context.obtainStyledAttributes我们上一节已经分析过了, 复习一下,它会调用Themer的obtainStyledAttributes. 我们看看针对编译后的资源,这些属性是如何被使用的:
1593 public TypedArray obtainStyledAttributes(AttributeSet set,
1594 @StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
1595 final int len = attrs.length;
1596 final TypedArray array = TypedArray.obtain(Resources.this, len);
被编译之后,XML文件已经成为一些二进制的块, 我们先把这些属性复制到TypedArray对象中. len是这个标签所对应的属性有多少项,我们不关心都只什么,知道有多少项就交给TypedArray的obtain方法去构造一个TypedArray对象.
需要特别注意的是, TypedArray对象是从对象池中申请的, 用完一定要释放掉,方法是通过TypedArray对象的recycle()方法.
一句话描述,obtain这一步仅仅是分配一个空的TypedArray对象.
43 static TypedArray obtain(Resources res, int len) {
44 final TypedArray attrs = res.mTypedArrayPool.acquire();
45 if (attrs != null) {
46 attrs.mLength = len;
47 attrs.mRecycled = false;
48
49 final int fullLen = len * AssetManager.STYLE_NUM_ENTRIES;
50 if (attrs.mData.length >= fullLen) {
51 return attrs;
52 }
53
54 attrs.mData = new int[fullLen];
55 attrs.mIndices = new int[1 + len];
56 return attrs;
57 }
58
59 return new TypedArray(res,
60 new int[len*AssetManager.STYLE_NUM_ENTRIES],
61 new int[1+len], len);
62 }
我们回到obtainStyledAttributes中, 现在开始填充这个TypedArray对象的值了,返回值填充到array.mData和array.mIndices中.
...
1602 final XmlBlock.Parser parser = (XmlBlock.Parser)set;
1603 AssetManager.applyStyle(mTheme, defStyleAttr, defStyleRes,
1604 parser != null ? parser.mParseState : 0, attrs, array.mData, array.mIndices);
1605
1606 array.mTheme = this;
1607 array.mXml = parser;
...
不出意外地,这是个native方法, 又要去读ResXMLTree了.
2136 { "applyStyle","(JIIJ[I[I[I)Z",
2137 (void*) android_content_AssetManager_applyStyle },
这个240多行的大函数我们后面再详细分析.
1265static jboolean android_content_AssetManager_applyStyle(JNIEnv* env, jobject clazz,
1266 jlong themeToken,
1267 jint defStyleAttr,
1268 jint defStyleRes,
1269 jlong xmlParserToken,
1270 jintArray attrs,
1271 jintArray outValues,
1272 jintArray outIndices)
到这一步为止,值就填充完毕了.
使用TypedArray
我们还是回到前面不只一次看到的View类的初始化:
a是TypedArray对象.
...
3949 final int N = a.getIndexCount();
3950 for (int i = 0; i < N; i++) {
3951 int attr = a.getIndex(i);
3952 switch (attr) {
3953 case com.android.internal.R.styleable.View_background:
3954 background = a.getDrawable(attr);
3955 break;
...
TypedArray是容器, 通过getIndexCount()进行遍历. 下面就看业务需求了, 比如我们需要ID,就去读ID:
4038 case com.android.internal.R.styleable.View_id:
4039 mID = a.getResourceId(attr, NO_ID);
4040 break;