深入理解 Android 中的自定义属性
本文深入解析了 Android 自定义属性的工作原理。核心在于,系统通过
AttributeSet向 View 传递布局中定义的原始属性键值对,而TypedArray作为其封装和增强工具,专门负责简化资源引用(如@dimen/xxx)的解析与类型转换。文中最关键的洞见是:declare-styleable标签并非语法必需,它实质上是辅助 aapt 工具自动生成属性资源 ID 数组和索引常量的“语法糖”,开发者完全可以绕过它,手动管理属性 ID 来实现相同的功能。理解这套机制有助于开发者更灵活、高效地使用和定义 View 属性。
博主博客
1. 引言
对于自定义属性,大家肯定都不陌生。通常我们遵循以下几个步骤来实现:
- 自定义一个 CustomView(继承自 View)类
- 编写 values/attrs.xml,在其中编写
styleable和attr等标签元素 - 在布局文件中使用自定义属性(注意命名空间)
- 在 CustomView 的构造方法中通过
TypedArray获取属性值
提示:如果你对上述几个步骤还不熟悉,建议先熟悉它们,再继续阅读本文。
那么,我有几个问题想要探讨:
- 以上步骤是如何奏效的?
styleable的含义是什么?可以不写吗?我自定义属性,只声明属性就好了,为什么一定要写个styleable呢?- 如果系统中已经有了语义比较明确的属性,我可以直接使用吗?
- 构造方法中的参数
AttributeSet(例如:MyTextView(Context context, AttributeSet attrs)),看名字就知道它包含属性的集合,那么我能直接通过它来获取自定义属性吗? TypedArray是什么?为什么要使用它来获取属性?
针对这几个问题,大家可以先思考一下。还是说,你觉得只要记住上述 4 个步骤就够了?
2. 常见的例子
接下来我们通过一个例子来逐一解答上述问题,回答顺序可能有所不同。
首先,我们来看一个常见的例子,将上述步骤具体实现。
2.1 自定义属性的声明文件(attrs.xml)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="test">
<attr name="text" format="string" />
<attr name="testAttr" format="integer" />
</declare-styleable>
</resources>
2.2 自定义 View 类(MyTextView.java)
package com.example.test;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
public class MyTextView extends View {
private static final String TAG = MyTextView.class.getSimpleName();
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
// 使用 TypedArray 获取属性值
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.test);
String text = ta.getString(R.styleable.test_text);
int textAttr = ta.getInteger(R.styleable.test_testAttr, -1);
Log.e(TAG, "text = " + text + " , textAttr = " + textAttr);
ta.recycle();
}
}
2.3 在布局文件中使用
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:zhy="http://schemas.android.com/apk/res/com.example.test"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.test.MyTextView
android:layout_width="100dp"
android:layout_height="200dp"
zhy:testAttr="520"
zhy:text="helloworld" />
</RelativeLayout>
大家花几秒钟看一下代码,运行结果为:
MyTextView: text = helloworld , textAttr = 520
应该都不意外吧。注意,这里 styleable 的 name 写的是 test,说明它并不要求必须是自定义 View 的名字。
3. AttributeSet 与 TypedArray
现在来思考这个问题:
构造方法中的
AttributeSet参数包含的是属性的集合,那么我能直接通过它来获取自定义属性吗?
首先,AttributeSet 中确实保存了该 View 声明的所有属性,我们当然可以通过它来获取(包括自定义)属性。具体怎么做呢?看看 AttributeSet 的方法就明白了。
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
// 直接使用 AttributeSet 获取所有属性
int count = attrs.getAttributeCount();
for (int i = 0; i < count; i++) {
String attrName = attrs.getAttributeName(i);
String attrVal = attrs.getAttributeValue(i);
Log.e(TAG, "attrName = " + attrName + " , attrVal = " + attrVal);
}
// ... 之后使用 TypedArray
}
输出:
MyTextView(4136): attrName = layout_width , attrVal = 100.0dip
MyTextView(4136): attrName = layout_height , attrVal = 200.0dip
MyTextView(4136): attrName = text , attrVal = helloworld
MyTextView(4136): attrName = testAttr , attrVal = 520
结合上面的布局文件,你会发现:通过 AttributeSet 确实可以获得布局文件中定义的所有属性的 key 和 value(还有一些其他方法,可以自己尝试)。那么,是不是 TypedArray 就可以抛弃了呢?答案是:NO。
现在关注下一个问题:
TypedArray 是什么?为什么要使用它?
我们稍微修改一下布局文件中 MyTextView 的属性:
<com.example.test.MyTextView
android:layout_width="@dimen/dp100"
android:layout_height="@dimen/dp200"
zhy:testAttr="520"
zhy:text="@string/hello_world" />
现在再次运行,结果是:
MyTextView(4692): attrName = layout_width , attrVal = @2131165234
MyTextView(4692): attrName = layout_height , attrVal = @2131165235
MyTextView(4692): attrName = text , attrVal = @2131361809
MyTextView(4692): attrName = testAttr , attrVal = 520
>>use typedarray
MyTextView(4692): text = Hello world! , textAttr = 520
发现了什么?通过 AttributeSet 获取的值,如果是对资源的引用(如 @dimen/dp100),会变成 @+数字 的字符串。这种格式你能直接看懂吗?再看最后一行使用 TypedArray 获取的值,是不是瞬间明白了。
TypedArray 是用来简化我们工作的。比如上例中,如果布局文件中的属性值是引用类型(如 @dimen/dp100),使用 AttributeSet 获取最终像素值需要两步:首先拿到资源 ID,然后再去解析这个 ID。而 TypedArray 正是帮我们简化了这个过程。
贴一下使用 AttributeSet 获取最终像素值的过程:
int widthDimensionId = attrs.getAttributeResourceValue(0, -1);
Log.e(TAG, "layout_width= " + getResources().getDimension(widthDimensionId));
现在,如果别人问你 TypedArray 存在的意义,你就可以告诉他了。
4. 重用系统属性
我们已经解决了两个问题。接下来,看看布局文件,我们有一个属性叫 zhy:text。众所周知,系统提供了一个属性叫 android:text。如果能直接使用 android:text,会更符合习惯。那么,考虑这个问题:
如果系统中已经有了语义比较明确的属性,我可以直接使用吗?
答案是可以的。怎么做呢?直接在 attrs.xml 中使用 android:text 属性:
<declare-styleable name="test">
<attr name="android:text" />
<attr name="testAttr" format="integer" />
</declare-styleable>
注意,当使用已定义好的属性时,不需要添加 format 属性(注意“声明”和“使用”的区别,差别就是有没有 format)。然后在代码中这样获取:ta.getString(R.styleable.test_android_text);。布局文件中直接使用 android:text="@string/hello_world" 即可。
这里提一下,系统中定义的属性,其实和我们自定义属性的方式类似。你可以在 sdk/platforms/android-xx/data/res/values 目录下找到系统定义的属性。然后,你可以在系统提供的 View(例如 TextView)的构造方法中找到使用 TypedArray 获取属性的代码(可以自己去查看一下)。
5. declare-styleable 的作用
接下来,既然 declare-styleable 标签的 name 可以随便写,这么随意的话,那么考虑问题:
styleable 的含义是什么?可以不写吗?我自定义属性,只声明属性就好了,为什么一定要写个 styleable 呢?
其实,不写 declare-styleable 标签也是可以的。怎么做呢?
5.1 首先,删除 declare-styleable 标签
那么现在的 attrs.xml 为:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="testAttr" format="integer" />
</resources>
很好,清爽多了!
5.2 修改 MyTextView 的实现
package com.example.test;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
public class MyTextView extends View {
private static final String TAG = MyTextView.class.getSimpleName();
// 手动声明一个数组,包含我们想要获取的 attr 的资源 ID
private static final int[] mAttr = { android.R.attr.text, R.attr.testAttr };
private static final int ATTR_ANDROID_TEXT = 0;
private static final int ATTR_TESTATTR = 1;
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, mAttr);
String text = ta.getString(ATTR_ANDROID_TEXT);
int textAttr = ta.getInteger(ATTR_TESTATTR, -1);
// 输出:text = Hello world! , textAttr = 520
Log.e(TAG, "text = " + text + " , textAttr = " + textAttr);
ta.recycle();
}
}
这里我们手动声明了一个 int 数组 mAttr,数组中的元素就是我们想要获取的 attr 的资源 ID。同时,根据元素在数组中的位置,我们定义了一些整型常量代表其下标,然后通过 TypedArray 获取值。
可以看到,我们做了如下映射:
R.styleable.test→mAttrR.styleable.test_text→ATTR_ANDROID_TEXT(0)R.styleable.test_testAttr→ATTR_TESTATTR(1)
那么,Android 内部其实也是这么做的。按照传统的写法(使用 declare-styleable),它会在 R.java 中生成如下代码:
public static final class attr {
public static final int testAttr = 0x7f0100a9;
}
public static final class styleable {
public static final int test_android_text = 0;
public static final int test_testAttr = 1;
public static final int[] test = {
0x0101014f, // 这是 android.R.attr.text
0x7f0100a9 // 这是 R.attr.testAttr
};
}
现在,你应该明白 styleable 的作用了。它的出现让系统可以为我们自动生成很多常量(int[] 数组、下标常量等),从而简化开发工作(想象一下,如果有一堆属性,全部手动编写常量,代码会是什么样子)。大家肯定还注意到,declare-styleable 的 name 属性,一般情况下我们写的都是自定义 View 的类名。这主要是为了直观地表达:该 declare-styleable 所包含的属性,都是这个 View 所使用的。
6. 总结
我们已经回答了四个问题,对于第一个问题:
自定义属性的几个步骤是如何奏效的?
上述内容已经基本涵盖了答案,这里总结一下:
- 声明属性:在
attrs.xml中声明自定义属性,系统(aapt 工具)会将其编译到R.java中,生成资源 ID。 - 使用属性:在布局文件中通过自定义命名空间使用这些属性。
- 解析属性:在自定义 View 的构造方法中,系统会传入
AttributeSet参数,它包含了所有在布局文件中声明的原始键值对(包括系统属性和自定义属性)。 - 简化解析:为了更方便地处理属性值(特别是资源引用和类型转换),我们可以通过
context.obtainStyledAttributes()方法获取TypedArray对象。这个方法内部会处理AttributeSet,并根据我们提供的属性 ID 数组(无论是手动创建还是由R.styleable.xxx自动生成)返回一个封装好的TypedArray,让我们可以轻松地获取各种类型的值。 - styleable 的角色:
declare-styleable标签是一个可选的组织工具。它的主要作用是让 aapt 工具自动生成一个属性 ID 数组和对应的索引常量,从而简化我们在代码中获取属性值的操作。本质上,我们可以完全不使用它,而是手动管理属性 ID 数组。
核心要点总结:
attrs.xml中的declare-styleable和attr,Android 会根据其在R.java中生成常量方便我们使用(由 aapt 工具完成)。本质上,我们可以不声明declare-styleable,仅仅声明所需的属性即可。- 在 View 的构造方法中,可以通过
AttributeSet获得自定义属性的原始值,但处理起来比较麻烦(尤其是资源引用)。TypedArray则提供了便捷的方法来获取经过解析的、类型正确的值。 - 在自定义 View 时,可以使用系统已经定义的属性,只需在
attr声明中引用它(如<attr name="android:text" />),而无需再次指定format。 TypedArray是属性值解析的助手,它封装了从AttributeSet中根据资源 ID 查找并转换类型值的复杂逻辑。- 自定义属性机制的本质是:声明 -> 使用 -> 在运行时解析。
styleable只是一种语法糖,用于自动生成辅助的元数据(数组和索引)。
深入理解 Android 中的自定义属性
https://blog.uso6.com/archives/shen-ru-li-jie-android-zhong-de-zi-ding-yi-shu-xing
评论