35、自定义属性:declare-styleable、TypedArray获取、在布局中使用自定义属性
自定义属性,说白了就是给咱们的自定义 View 开个「后门」。
你想想看,系统自带的 TextView、Button 都有 android:text、android:layout_width 这些属性。那你自己写的 View 呢?总不能每次都写死吧?
我刚开始做自定义 View 的时候,就犯过这个傻。写了个圆形头像控件,半径直接写死在代码里。结果产品经理说「这个头像大一点,那个小一点」——得,改代码重新编译。后来我才知道,原来 Android 早就给我们准备好了自定义属性的机制。
核心流程:三步走
自定义属性的使用,其实就三步:
- 定义属性:在 res/values/attrs.xml 里用 declare-styleable 声明
- 获取属性:在 View 的构造函数里用 TypedArray 读取
- 使用属性:在布局文件里像普通属性一样写
嗯,就这么简单。但每一步都有坑,我一个个说。
第一步:定义属性(declare-styleable)
在 res/values/ 目录下新建一个 attrs.xml 文件。注意,文件名可以随便起,但习惯上都叫 attrs.xml。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RoundImageView">
<attr name="borderWidth" format="dimension" />
<attr name="borderColor" format="color" />
<attr name="isCircle" format="boolean" />
<attr name="imageScaleType" format="enum">
<enum name="center_crop" value="0" />
<enum name="fit_center" value="1" />
<enum name="center" value="2" />
</attr>
</declare-styleable>
</resources>
这里有几个要点:
- name 要和你的自定义 View 类名一致。虽然不是强制,但这是行业惯例。我见过有人随便起名,结果自己都找不着。
- format 决定了属性值的类型。常用的有:dimension(尺寸)、color(颜色)、boolean(布尔)、integer(整数)、float(浮点)、string(字符串)、enum(枚举)、reference(资源引用)。
- enum 类型比较特殊,它其实是个整数,但你在布局里写的是字符串。比如 app:imageScaleType="center_crop"。
第二步:获取属性(TypedArray)
这一步是核心。在自定义 View 的构造函数里,通过 TypedArray 把属性值读出来。
public class RoundImageView extends AppCompatImageView {
private float borderWidth;
private int borderColor;
private boolean isCircle;
private int imageScaleType;
public RoundImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
// 获取 TypedArray
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
// 读取属性值(第二个参数是默认值)
borderWidth = ta.getDimension(R.styleable.RoundImageView_borderWidth, 0f);
borderColor = ta.getColor(R.styleable.RoundImageView_borderColor, Color.TRANSPARENT);
isCircle = ta.getBoolean(R.styleable.RoundImageView_isCircle, false);
imageScaleType = ta.getInteger(R.styleable.RoundImageView_imageScaleType, 0);
// 重要!一定要回收
ta.recycle();
}
}
这里有个细节:属性名是 R.styleable.RoundImageView_borderWidth,注意是「类名_属性名」的格式。这是 Android 自动生成的,别写错了。
第三步:在布局中使用
在布局文件里使用自定义属性前,需要先声明命名空间。通常用 xmlns:app="http://schemas.android.com/apk/res-auto"。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.widget.RoundImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/avatar"
app:borderWidth="2dp"
app:borderColor="#FF0000"
app:isCircle="true"
app:imageScaleType="center_crop" />
</LinearLayout>
注意看,系统属性用 android: 前缀,自定义属性用 app: 前缀。这个 app 就是上面声明的命名空间。
知识体系结构图
下面这张图帮你理清自定义属性的完整流程:
进阶技巧:属性分组与复用
有时候多个自定义 View 需要共用一些属性。比如 RoundImageView 和 RoundTextView 都需要 borderWidth 和 borderColor。这时候可以把公共属性抽出来:
<resources>
<!-- 公共属性 -->
<attr name="borderWidth" format="dimension" />
<attr name="borderColor" format="color" />
<declare-styleable name="RoundImageView">
<attr name="borderWidth" />
<attr name="borderColor" />
<attr name="isCircle" format="boolean" />
</declare-styleable>
<declare-styleable name="RoundTextView">
<attr name="borderWidth" />
<attr name="borderColor" />
<attr name="textSize" format="dimension" />
</declare-styleable>
</resources>
注意看,borderWidth 和 borderColor 只定义了一次,然后在两个 declare-styleable 里引用。这样既避免了重复,又保持了统一。
避坑指南
我这些年用自定义属性,踩过的坑不少。挑几个典型的说说:
- TypedArray 没回收:这个前面说过了,会导致内存泄漏和属性读取异常。我的习惯是在 finally 块里回收,确保万无一失。
- 属性名冲突:如果你在多个 declare-styleable 里定义了同名但不同类型的属性,编译不会报错,但运行时会出问题。所以命名要规范,最好加上类名前缀。
- 默认值不合理:比如 borderWidth 默认给 0,结果用户没设置时边框就没了。我一般给个合理的默认值,比如 1dp。
- 构造函数重载:自定义 View 通常有多个构造函数,记得在每个构造函数里都调用 init 方法。我见过有人只在两个参数的构造函数里初始化,结果在代码里 new 的时候属性全丢了。
核心要点总结:
- declare-styleable 的 name 要和 View 类名一致
- TypedArray 用完必须 recycle()
- 布局中使用 app: 命名空间
- 属性默认值要合理,避免空指针
- 公共属性可以复用,减少重复定义
自定义属性这个机制,说白了就是让 View 变得更灵活。你想想看,没有自定义属性,每个 View 都是「死」的。有了它,你的 View 才能像系统控件一样,在布局文件里随意配置。嗯,这就是 Android 框架设计的精妙之处。