本文深入解析了 Android 自定义属性的工作原理。核心在于,系统通过 AttributeSet 向 View 传递布局中定义的原始属性键值对,而 TypedArray 作为其封装和增强工具,专门负责简化资源引用(如 @dimen/xxx)的解析与类型转换。文中最关键的洞见是:declare-styleable 标签并非语法必需,它实质上是辅助 aapt 工具自动生成属性资源 ID 数组和索引常量的“语法糖”,开发者完全可以绕过它,手动管理属性 ID 来实现相同的功能。理解这套机制有助于开发者更灵活、高效地使用和定义 View 属性。

博主博客

1. 引言

对于自定义属性,大家肯定都不陌生。通常我们遵循以下几个步骤来实现:

  1. 自定义一个 CustomView(继承自 View)类
  2. 编写 values/attrs.xml,在其中编写 styleableattr 等标签元素
  3. 在布局文件中使用自定义属性(注意命名空间)
  4. 在 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

应该都不意外吧。注意,这里 styleablename 写的是 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.testmAttr
  • R.styleable.test_textATTR_ANDROID_TEXT(0)
  • R.styleable.test_testAttrATTR_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-styleablename 属性,一般情况下我们写的都是自定义 View 的类名。这主要是为了直观地表达:该 declare-styleable 所包含的属性,都是这个 View 所使用的。

6. 总结

我们已经回答了四个问题,对于第一个问题:

自定义属性的几个步骤是如何奏效的?

上述内容已经基本涵盖了答案,这里总结一下:

  1. 声明属性:在 attrs.xml 中声明自定义属性,系统(aapt 工具)会将其编译到 R.java 中,生成资源 ID。
  2. 使用属性:在布局文件中通过自定义命名空间使用这些属性。
  3. 解析属性:在自定义 View 的构造方法中,系统会传入 AttributeSet 参数,它包含了所有在布局文件中声明的原始键值对(包括系统属性和自定义属性)。
  4. 简化解析:为了更方便地处理属性值(特别是资源引用和类型转换),我们可以通过 context.obtainStyledAttributes() 方法获取 TypedArray 对象。这个方法内部会处理 AttributeSet,并根据我们提供的属性 ID 数组(无论是手动创建还是由 R.styleable.xxx 自动生成)返回一个封装好的 TypedArray,让我们可以轻松地获取各种类型的值。
  5. styleable 的角色declare-styleable 标签是一个可选的组织工具。它的主要作用是让 aapt 工具自动生成一个属性 ID 数组和对应的索引常量,从而简化我们在代码中获取属性值的操作。本质上,我们可以完全不使用它,而是手动管理属性 ID 数组。

核心要点总结:

  • attrs.xml 中的 declare-styleableattr,Android 会根据其在 R.java 中生成常量方便我们使用(由 aapt 工具完成)。本质上,我们可以不声明 declare-styleable,仅仅声明所需的属性即可。
  • 在 View 的构造方法中,可以通过 AttributeSet 获得自定义属性的原始值,但处理起来比较麻烦(尤其是资源引用)。TypedArray 则提供了便捷的方法来获取经过解析的、类型正确的值。
  • 在自定义 View 时,可以使用系统已经定义的属性,只需在 attr 声明中引用它(如 <attr name="android:text" />),而无需再次指定 format
  • TypedArray 是属性值解析的助手,它封装了从 AttributeSet 中根据资源 ID 查找并转换类型值的复杂逻辑。
  • 自定义属性机制的本质是:声明 -> 使用 -> 在运行时解析styleable 只是一种语法糖,用于自动生成辅助的元数据(数组和索引)。