Android筑基——自定义属性详解

1. 前言

对于自定义属性,大家想必都用过吧。因此,本文打算说明一些在开发中使用自定义属性的痛点问题:

  • 如何合理声明自定义属性的 format,并根据 format 选择对应的解析方法?
  • TypedArray 到底比 AttributeSet 强在什么地方了?
  • 如何在不声明 declare-styleable 的情况下,也可以获取到自定义属性?
  • 你知道 ktx 对 TypedArray 提供了哪些扩展支持吗?

2. 正文

2.1 声明与使用自定义属性的简单例子

创建自定义 View 类

class SimpleCustomAttributeView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}

定义自定义属性

res/values/ 文件夹下新建 attrs.xml 文件(习惯上属性文件的名字是 attrs.xml):

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SimpleCustomAttributeView">
        <attr name="name" format="string" />
        <attr name="age" format="integer" />
        <attr name="gender" format="boolean" />
    </declare-styleable>
</resources>

声明了一个 name 的值为 SimpleCustomAttributeViewdeclare-styleable 标签,按照惯例,这个 name 的值和自定义 View 的类名是相同的。如果不遵循此惯例,IDE 将无法提供相关的语句补全功能。

在这个标签内部,声明了三个自定义属性(attr 是 attribute 的缩写,意思是属性):name 表示属性的名字,format 表示属性的格式或者说类型。

在布局文件中使用自定义 View 和自定义属性

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.example.customattributesstudy.SimpleCustomAttributeView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:name="willwaywang6"
        app:age="18"
        app:gender="true" />

</LinearLayout>

这里面需要注意的有:

  • 我们自己定义的自定义属性不可以使用 android 命名空间,而是需要使用 app 命名空间。在这个 xml 里面还有一个 tools 命名空间。它们三个的区别是:android 命名空间是供系统使用的,app 命名空间是供非系统使用的,tools 命名空间是供 IDE 使用的。这里的 xmlns:app="http://schemas.android.com/apk/res-auto"并不用记忆,只需要输入 appNs 回车就可以很快生成了。

  • 把自定义 View 类的完整类名作为标签名声明在 xml 里面。如果自定义 View 类是一个内部类,则必须使用外部类的名称做进一步的限定,否则会导致运行时报错。

    <view class="com.example.customattributesstudy.SimpleCustomAttributeView$InnerView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    
  • 使用 app 命名空间开头的自定义属性,来声明对应的值。

解析自定义属性

class SimpleCustomAttributeView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    init {
        val typedArray: TypedArray =
            context.obtainStyledAttributes(attrs, R.styleable.SimpleCustomAttributeView)
        val name = typedArray.getString(R.styleable.SimpleCustomAttributeView_name)
        val age = typedArray.getInt(R.styleable.SimpleCustomAttributeView_age, 1)
        val gender = typedArray.getBoolean(R.styleable.SimpleCustomAttributeView_gender, true)
        Log.d(TAG, "name=$name,age=$age,gender=$gender")
        typedArray.recycle()
    }

    companion object {
        private const val TAG = "SimpleCustomAttribute"
    }
}

init 初始化代码块中,

  • 通过 context.obtainStyledAttributes(attrs, R.styleable.SimpleCustomAttributeView) 创建出 TypedArray 对象;
  • R.styleable.SimpleCustomAttributeView_XXXX传入 TypedArray 对象的getXXX 方法,获取对应的属性值;
  • typedArray.recycle() 回收 TypedArray

运行程序,查看日志:

D/SimpleCustomAttribute: name=willwaywang6,age=18,gender=true

到这里,简单的例子已经介绍完毕了。但是,本文可没有结束,而是刚刚正式开始。

2.2 合理地声明和解析自定义属性的 format

自定义属性的 format 有 10 种取值:

在这里插入图片描述

而且可以存在取值的组合。因此,有必要了解每一种取值的含义以及对应的解析方法,这样在实际开发中才可以得心应手。

这里采用的思路是查看 \android-31\data\res\values\attrs.xml 下系统提供的自定义属性声明,然后再去看系统在代码里面是如何解析使用的。

我们选取 <declare-styleable name="View"> 的自定义属性来作为参考无疑是最为合适的,大家对它的属性想必也是用的最多的了。

这里我们另外创建了一个 CustomAttributeView 的自定义 View,来做代码演示。

reference

参考属性:

<attr name="id" format="reference" />

声明一个自定义属性:

<declare-styleable name="CustomAttributeView">
    <attr name="cav_id" format="reference" />
</declare-styleable>

res/values/ 下添加一个 ids.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item type="id" name="cav" />
</resources>

在 xml 中使用自定义属性:

<com.example.customattributesstudy.CustomAttributeView
    app:cav_id="@id/cav"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

在代码中解析自定义属性的值:

class CustomAttributeView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    init {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomAttributeView)
        val id = typedArray.getResourceId(R.styleable.CustomAttributeView_cav_id, NO_ID)
        Log.d(TAG, "id = $id, hex: ${id.toString(16)}")
        typedArray.recycle()
    }

    companion object {
        private const val TAG = "CustomAttributeView"
    }
}

打印日志:

D/CustomAttributeView: id = 2131231208, hex: 7f0801e8

这个打印值是什么含义呢?就是 R.id.cav 对应的整数值。这点可以通过查看 R.txt 中的内容来验证。

R.txt 的路径在 \app\build\intermediates\runtime_symbol_list\debug\R.txt,在这里面搜索 7f0801e8,可以看到:

在这里插入图片描述

reference 的含义是允许填入所有引用类型的资源,如引用类型的字符串(strings.xml),引用类型的尺寸(dimens.xml),引用类型的布尔值,引用类型的图片资源,引用类型的布局资源,引用类型的动画资源,引用类型的整型值资源,引用类型的style和theme资源等等。

integer

参考属性:

<attr name="maxLines" format="integer" min="0" />

声明两个自定义属性:

<attr name="cav_maxLines" format="integer" min="0" />
<attr name="cav_minLines" format="integer" min="0" />

res/values/ 下添加一个 integers.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="max_lines_value">1000</integer>
</resources>

在 xml 中使用自定义属性:

<com.example.customattributesstudy.CustomAttributeView
    app:cav_maxLines="@integer/max_lines_value"
    app:cav_minLines="1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

在代码中解析自定义属性的值:

val maxLines = typedArray.getInt(R.styleable.CustomAttributeView_cav_maxLines, -1)
Log.d(TAG, "maxLines = $maxLines")
val minLines = typedArray.getInt(R.styleable.CustomAttributeView_cav_minLines, -1)
Log.d(TAG, "minLines = $minLines")

打印日志:

D/CustomAttributeView: maxLines = 1000
D/CustomAttributeView: minLines = 1

另外,还有一个 getInteger 的方法,它们的区别是:

getInt:如果属性值不是一个整型,会尝试使用 Integer.decode(String) 方法把它强制转换为整型。

getInteger:如果属性值不是一个整型,就会直接抛出异常。

这里进行演示说明:

在 strings.xml 下添加:

<string name="lines100">100</string>

在 xml 中使用:

<com.example.customattributesstudy.CustomAttributeView
    app:cav_maxLines="@string/lines100"
    app:cav_minLines="@string/lines100"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

在代码中分别使用 getIntgetInteger 来解析:

val maxLines = typedArray.getInt(R.styleable.CustomAttributeView_cav_maxLines, -1)
Log.d(TAG, "maxLines = $maxLines")
val minLines = typedArray.getInteger(R.styleable.CustomAttributeView_cav_minLines, -1)
Log.d(TAG, "minLines = $minLines")

运行日志如下:

D/CustomAttributeView: maxLines = 100
Caused by: java.lang.UnsupportedOperationException: Can't convert value at index 2 to integer: type=0x3
    at android.content.res.TypedArray.getInteger(TypedArray.java:644)
    at com.example.customattributesstudy.CustomAttributeView.<init>(CustomAttributeView.kt:20)
       ... 29 more

可以看到getIntgetInteger 的区别了。

所以,integer 的含义是允许填入硬编码的整型数值,引用类型的整型资源以及可以转换为整型的字符串资源。

float

参考属性:

<attr name="rotation" format="float" />

声明两个自定义属性:

<attr name="cav_rotationX" format="float" />
<attr name="cav_rotationY" format="float" />

res/values/dimens.xml 下添加:

<item name="rotateY_value" format="float" type="dimen">89.5</item>

在 xml 中使用自定义属性:

<com.example.customattributesstudy.CustomAttributeView
    app:cav_rotationX="90.5"
    app:cav_rotationY="@dimen/rotateY_value"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

在代码中解析自定义属性的值:

val rotationX = typedArray.getFloat(R.styleable.CustomAttributeView_cav_rotationX,0f)
Log.d(TAG, "rotationX = $rotationX")
val rotationY = typedArray.getFloat(R.styleable.CustomAttributeView_cav_rotationY,0f)
Log.d(TAG, "rotationY = $rotationY")

打印日志如下:

D/CustomAttributeView: rotationX = 90.5
D/CustomAttributeView: rotationY = 89.5

对于 getFloat 方法来说,如果属性值不是浮点型或者整型,会尝试使用 Float.parseFloat(String)强制转换为浮点型。

需要注意的是在 xml 中的浮点数不可以加 f 或者 F 的后缀,否则会报错。

boolean

参考属性:

<attr name="clickable" format="boolean" />

声明两个自定义属性:

<attr name="cav_clickable" format="boolean" />
<attr name="cav_longClickable" format="boolean" />

res/values/ 下添加一个 bools.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <bool name="yes">true</bool>
    <bool name="no">false</bool>
</resources>

在 xml 中使用自定义属性:

<com.example.customattributesstudy.CustomAttributeView
    app:cav_clickable="@bool/yes"
    app:cav_longClickable="false"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

在代码中解析自定义属性的值:

val clickable = typedArray.getBoolean(R.styleable.CustomAttributeView_cav_clickable, false)
Log.d(TAG, "clickable = $clickable")
val longClickable = typedArray.getBoolean(R.styleable.CustomAttributeView_cav_longClickable, false)
Log.d(TAG, "longClickable = $longClickable")

打印日志如下:

D/CustomAttributeView: clickable = true
D/CustomAttributeView: longClickable = false

getBoolean 方法:如果属性值是一个整型值,那么这个整型值大于 0,返回 true,反之返回 false;如果属性值不是整型也不是布尔型,那么这个方法会使用 Integer.decode(String)把它强制转换为整型后,再做处理。

bool 可以接受布尔类型的字面值,引用类型的布尔型资源,引用类型的整型资源,可以转换为整型的引用类型的字符串资源。实际开发中,不建议使用后面两种资源,非常不清晰。

fraction

参考属性:

<declare-styleable name="RotateDrawable">
    <attr name="pivotX" format="float|fraction" />
    <attr name="pivotY" format="float|fraction" />
</declare-styleable>

声明两个自定义属性:

<attr name="cav_pivotX" format="fraction" />
<attr name="cav_pivotY" format="fraction" />

res/values/ 下添加一个 dimens.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="current_pivotX" type="fraction">50%</item>
    <fraction name="current_pivotY">50%</fraction>
</resources>

在 xml 中使用自定义属性:

<com.example.customattributesstudy.CustomAttributeView
    app:cav_pivotX="100%"
    app:cav_pivotY="@fraction/current_pivotY"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

在代码中解析自定义属性的值:

val pivotX = typedArray.getFraction(R.styleable.CustomAttributeView_cav_pivotX, 1, 1, -1f)
Log.d(TAG, "pivotX = $pivotX")
val pivotY = typedArray.getFraction(R.styleable.CustomAttributeView_cav_pivotY, 1, 1, -1f)
Log.d(TAG, "pivotY = $pivotY")

打印日志如下:

D/CustomAttributeView: pivotX = 1.0
D/CustomAttributeView: pivotY = 0.5

这个属性,实际开发中用的比较少,就不做过多介绍了。

dimension

参考属性:

<attr name="padding" format="dimension" />

声明两个自定义属性:

<attr name="cav_paddingLeft" format="dimension" />
<attr name="cav_paddingRight" format="dimension" />

res/values/ 下添加一个 dimens.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    ...
    <item name="image_padding" type="dimen">24dp</item>
    <dimen name="title_padding">16dp</dimen>
</resources>

在 xml 中使用自定义属性:

<com.example.customattributesstudy.CustomAttributeView
    app:cav_paddingLeft="16dp"
    app:cav_paddingRight="@dimen/title_padding"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

在代码中解析自定义属性的值:

val paddingLeft: Int = typedArray.getDimensionPixelSize(R.styleable.CustomAttributeView_cav_paddingLeft, 0)
Log.d(TAG, "paddingLeft = $paddingLeft")
val paddingRight: Float = typedArray.getDimension(R.styleable.CustomAttributeView_cav_paddingRight, 0f)
Log.d(TAG, "paddingRight = $paddingRight")
val paddingRight2: Int = typedArray.getDimensionPixelOffset(R.styleable.CustomAttributeView_cav_paddingRight, 0)
Log.d(TAG, "paddingRight2 = $paddingRight2")

打印日志如下:

D/CustomAttributeView: paddingLeft = 44
D/CustomAttributeView: paddingRight = 44.0
D/CustomAttributeView: paddingRight2 = 44

getDimensiongetDimensionPixelOffsetgetDimensionPixelSize 的区别见下表:

方法名返回值含义
getDimensionFloat将值转为 px
getDimensionPixelOffsetInt将值转为 px,但只保留整数部分。
getDimensionPixelSizeInt将值转为 px,但只保留四舍五入后的整数部分,并且一个非零基值至少表示一个px。

string

参考属性:

<attr name="tag" format="string" />

声明两个自定义属性:

<attr name="cav_tag1" format="string" />
<attr name="cav_tag2" format="string" />

在 xml 中使用自定义属性:

<com.example.customattributesstudy.CustomAttributeView
    app:cav_tag1="willwaywang6"
    app:cav_tag2="@string/app_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

在代码中解析自定义属性的值:

val tag1 = typedArray.getString(R.styleable.CustomAttributeView_cav_tag1)
Log.d(TAG, "tag1 = $tag1")
val tag2 = typedArray.getString(R.styleable.CustomAttributeView_cav_tag2)
Log.d(TAG, "tag2 = $tag2")

打印日志如下:

D/CustomAttributeView: tag1 = willwaywang6
D/CustomAttributeView: tag2 = CustomAttributesStudy

另外,还有 getText 方法,它和 getString 的区别是什么呢?

getString 方法的返回值是 String 类型,返回的是不带样式的字符串;

getText 方法的返回值是 CharSequence 类型,返回的是带样式的字符串。

color

参考属性:

<attr name="backgroundTint" format="color" />

声明两个自定义属性:

<attr name="cav_backgroundTint" format="color" />
<attr name="cav_foregroundTint" format="color" />

在 xml 中使用自定义属性:

<com.example.customattributesstudy.CustomAttributeView
    app:cav_backgroundTint="@color/white"
    app:cav_foregroundTint="#FF0000"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

在代码中解析自定义属性的值:

val backgroundTint = typedArray.getColor(R.styleable.CustomAttributeView_cav_backgroundTint, 0)
Log.d(TAG, "backgroundTint = ${backgroundTint.toHexString()}")
val foregroundTint = typedArray.getColor(R.styleable.CustomAttributeView_cav_foregroundTint, 0)
Log.d(TAG, "foregroundTint = ${foregroundTint.toHexString()}")

打印日志如下:

D/CustomAttributeView: backgroundTint = #FFFFFFFF
D/CustomAttributeView: foregroundTint = #FFFF0000

另外,还有一个 getColorStateList 方法,与 getColor 方法的区别是:

它们都可以用来解析硬编码的颜色值,引用类型的颜色资源,引用类型的颜色选择器资源。

getColor 的返回值是整型,也就是一个颜色。如果 getColor 解析的是颜色选择器资源,那么返回的只是颜色选择器里面的默认颜色而已。

getColorStateList 的返回值是 ColorStateList 类型,也就是一个颜色选择器对象。

enum

参考属性:

<attr name="visibility">
    <enum name="visible" value="0" />
    <enum name="invisible" value="1" />
    <enum name="gone" value="2" />
</attr>

声明自定义属性:

<declare-styleable name="CustomAttributeView">
    <attr name="cav_visibility">
        <enum name="visible" value="0" />
        <enum name="invisible" value="1" />
        <enum name="gone" value="2" />
    </attr>
</declare-styleable>

在 xml 中使用自定义属性:

<com.example.customattributesstudy.CustomAttributeView
    app:cav_visibility="gone"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

在代码中解析自定义属性的值:

val visibility = typedArray.getInt(R.styleable.CustomAttributeView_cav_visibility, 0)
Log.d(TAG, "visibility = $visibility")

打印日志如下:

D/CustomAttributeView: visibility = 2

flag

参考属性:

<attr name="textStyle">
    <flag name="normal" value="0" />
    <flag name="bold" value="1" />
    <flag name="italic" value="2" />
</attr>

声明自定义属性:

<attr name="cav_textStyle">
    <flag name="normal" value="0" />
    <flag name="bold" value="1" />
    <flag name="italic" value="2" />
</attr>

在 xml 中使用自定义属性:

<com.example.customattributesstudy.CustomAttributeView
    app:cav_textStyle="bold|italic"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

在代码中解析自定义属性的值:

val textStyle = typedArray.getInt(R.styleable.CustomAttributeView_cav_textStyle, 0)
Log.d(TAG, "textStyle = $textStyle")

打印日志如下:

D/CustomAttributeView: textStyle = 3

flag 与 enum 的区别:flag 可以进行与位运算,而 enum 不可以;enum 的 value 值一般是从 0 开始的连续整数值,而 flag 的 value 值则比较灵活,只要是整数值就可以了。

format 还可以组合使用

参考属性:

<attr name="background" format="reference|color" />

它的含义是不仅可以引用颜色资源,还可以引用图片资源。

对应的解析方式:

background = TypedArray.getDrawable(com.android.internal.R.styleable.View_background);

组合使用时,有时使用一种解析方式是无法完成解析的,如 RotateDrawable

<declare-styleable name="RotateDrawable">
    <attr name="pivotX" format="float|fraction" />
</declare-styleable>

对应的解析方式:

if (a.hasValue(R.styleable.RotateDrawable_pivotX)) {
    final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotX);
    // 先获取到类型
    state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION;
    // 根据类型,使用不同的解析方式
    state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
}

2.3 AttributeSet vs TypedArray

CustomAttributeView目前的属性太多了,不便于分析,让我们回过头去看 SimpleCustomAttributeView 的例子吧。

init 代码块中添加代码:

if (attrs != null) {
    for (index in 0 until attrs.attributeCount) {
        val attrName = attrs.getAttributeName(index)
        val attrValue = attrs.getAttributeValue(index)
        Log.d(TAG, "attr name: $attrName, attr value: $attrValue")
    }
}

打印日志如下:

D/SimpleCustomAttribute: attr name: layout_width, attr value: -2
D/SimpleCustomAttribute: attr name: layout_height, attr value: -2
D/SimpleCustomAttribute: attr name: age, attr value: 18
D/SimpleCustomAttribute: attr name: gender, attr value: true
D/SimpleCustomAttribute: attr name: name, attr value: willwaywang6

这说明通过 AttributeSet,可以获得布局文件中定义的所有属性的属性名和属性值。

那是不是说在实际开发中,我们可以直接使用 AttributeSet,而不必再去通过 Context.obtainStyledAttributes 方法创建 TypedArray 对象了呢?

肯定不是的。

这是因为直接从 AttributeSet 读取值存在一些弊端:

  • 解析属性值中的资源引用不方便

    我们通过代码来说明一下,把 android:layout_widthapp:name的属性值改为资源引用

    <com.example.customattributesstudy.SimpleCustomAttributeView
        android:layout_width="@dimen/width"
        android:layout_height="wrap_content"
        app:name="@string/author"
        app:age="18"
        app:gender="true" />
    

    再次运行程序,查看日志:

    D/SimpleCustomAttribute: attr name: layout_width, attr value: @2131100239
    D/SimpleCustomAttribute: attr name: layout_height, attr value: -2
    D/SimpleCustomAttribute: attr name: age, attr value: 18
    D/SimpleCustomAttribute: attr name: gender, attr value: true
    D/SimpleCustomAttribute: attr name: name, attr value: @2131755133
    D/SimpleCustomAttribute: name=willwaywang6,age=18,gender=true
    

    可以看到,通过 AttributeSet 获取的引用资源的值只是@+一个数字的字符串,而 TypedArray 可以直接获取到引用资源的值了。

    这个数字是什么呢?

    我们查看一下 R.txt 中的 author 引用资源的 id:

    int string author 0x7f10007d
    

    把0x7f10007d这个 16 进制转为十进制是:2131755133,不正好就是那个数字。所以@后面的数字就是引用资源的id值。

    如果想获取到引用资源的值,需要这样写:

    val authorId = attrs.getAttributeResourceValue(4, NO_ID) // 4 是 name 属性在 AttributeSet 中的索引号。
    val name = resources.getString(authorId)
    Log.d(TAG, "name = $name") // 打印:willwaywang6
    

    这里可以看到,AttributeSet 获取引用资源的值麻烦(需要两步才可以),也不好用(传入的索引没有地方定义,这在属性个数很多的时候肯定是容易犯错的);AttributeSet 获取的是所有的属性名和属性值,而实际上我们只关心自定义属性的属性名和属性值。

    TypedArray 可以解决上面说的问题:

    通过 TypedArray 只需要一步就可以获取到引用资源的值,而且传入的索引是定义好的含义非常清晰的常量:

    val name = typedArray.getString(R.styleable.SimpleCustomAttributeView_name)
    

    通过 TypedArray 获取的就是自定义属性的属性名和属性值,不多也不少:

    val typedArray: TypedArray =
        context.obtainStyledAttributes(attrs, R.styleable.SimpleCustomAttributeView)
    val arrayLength = typedArray.length()
    Log.d(TAG, "arrayLength = $arrayLength")
    val name = typedArray.getString(R.styleable.SimpleCustomAttributeView_name)
    val age = typedArray.getInt(R.styleable.SimpleCustomAttributeView_age, 1)
    val gender = typedArray.getBoolean(R.styleable.SimpleCustomAttributeView_gender, true)
    Log.d(TAG, "name=$name,age=$age,gender=$gender")
    

    打印:

    D/SimpleCustomAttribute: arrayLength = 3
    D/SimpleCustomAttribute: name=willwaywang6,age=18,gender=true
    

    换句话说,通过 context.obtainStyledAttributes(attrs, R.styleable.SimpleCustomAttributeView) 就会只保留自定义的那些属性了,而把系统的属性都过滤掉了。

  • 不应用样式

    这点是在官方文档上看到的,网上很少有说到这点的。

    1. 创建一个新的 StyleCustomAttributeView

      class StyleCustomAttributeView @JvmOverloads constructor(
          context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
      ) : View(context, attrs, defStyleAttr) {
      }
      
    2. 定义它的自定义属性,这里和 SimpleCustomAttributeView的自定义属性是一样的。所以,我们把这些自定义属性先声明在外部,然后再在 declare-styleable 标签内部使用(这时就不用加 format 属性了)

      <attr name="name" format="string" />
      <attr name="age" format="integer" />
      <attr name="gender" format="boolean" />
      <declare-styleable name="SimpleCustomAttributeView">
          <attr name="name" />
          <attr name="age" />
          <attr name="gender" />
      </declare-styleable>
      <declare-styleable name="StyleCustomAttributeView">
          <attr name="name" />
          <attr name="age" />
          <attr name="gender" />
      </declare-styleable>
      
    3. 声明一个自定义属性,用于引用后面会引用到的 style;并把这个自定义属性的 id 赋值给StyleCustomAttributeView主构造函数的 defStyleAttr 属性:

      <attr name="styleCustomAttributeViewStyle" format="reference" />
      
      class StyleCustomAttributeView @JvmOverloads constructor(
          context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = R.attr.styleCustomAttributeViewStyle
      ) : View(context, attrs, defStyleAttr) {
      }
      
    4. res/values/ 目录下,创建 styles.xml

      <?xml version="1.0" encoding="utf-8"?>
      <resources>
          <style name="StyleCustomAttributeViewStyle">
              <item name="name">jakewharton</item>
              <item name="age">36</item>
              <item name="gender">true</item>
          </style>
      </resources>
      
    5. res/themes.xml 目录下,使用 styleCustomAttributeViewStyle 属性

      <resources xmlns:tools="http://schemas.android.com/tools">
          <!-- Base application theme. -->
          <style name="Theme.CustomAttributesStudy" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
              ...
              <!-- Customize your theme here. -->
              <item name="styleCustomAttributeViewStyle">@style/StyleCustomAttributeViewStyle</item>
          </style>
      </resources>
      
    6. 在 xml 中引用 StyleCustomAttributeView:

      <com.example.customattributesstudy.StyleCustomAttributeView
          android:layout_width="wrap_content"
          android:layout_height="wrap_content" />
      

      是不是有人会说:这里怎么没有写自定义属性啊?是不是写错了啊?

      没有的。就是这样的。

    7. init 代码块中解析自定义属性的值:

      init {
          val typedArray: TypedArray =
              context.obtainStyledAttributes(attrs, R.styleable.StyleCustomAttributeView, defStyleAttr, 0)
          val name = typedArray.getString(R.styleable.StyleCustomAttributeView_name)
          val age = typedArray.getInt(R.styleable.StyleCustomAttributeView_age, 1)
          val gender = typedArray.getBoolean(R.styleable.StyleCustomAttributeView_gender, true)
          Log.d(TAG, "name=$name,age=$age,gender=$gender")
          typedArray.recycle()
      }
      

      需要特别注意的是,这里使用的 obtainStyledAttributes 方法是 4 个参数的方法,而不是之前两个参数的那个方法了。

    运行程序,查看日志:

    D/StyleCustomAttribute: name=jakewharton,age=36,gender=true
    

    可以看到,虽然在引用自定义控件的布局文件中没有写任何自定义属性,但是我们仍然可以获取到在 style 中定义的一套自定义属性的值,这就是样式的应用了。

    如果我们在布局文件中使用自定义属性了:

    <com.example.customattributesstudy.StyleCustomAttributeView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:name="@string/author"
        app:age="18"
        app:gender="true"/>
    

    再次运行程序,可以看到布局文件中的属性值会覆盖掉默认样式里面的属性值的。

    D/StyleCustomAttribute: name=willwaywang6,age=18,gender=true
    

    这种默认样式的应用,通过 AttributeSet 是无法完成的。

2.4 declare-styleable 的本质是什么?

我们在 attrs.xml 中声明了 declare-styleable 标签:

<attr name="name" format="string" />
<attr name="age" format="integer" />
<attr name="gender" format="boolean" />
<declare-styleable name="SimpleCustomAttributeView">
    <attr name="name" />
    <attr name="age" />
    <attr name="gender" />
</declare-styleable>

然后就可以在代码里面使用了:

R.styleable.SimpleCustomAttributeView:整型数组

R.styleable.SimpleCustomAttributeView_name:整型值

R.styleable.SimpleCustomAttributeView_age:整型值

R.styleable.SimpleCustomAttributeView_gender:整型值

不禁想问下,这些都是什么啊?从哪里来的?

通过代码打印它的内容:

// 使用 16 进制打印
Log.d(TAG, "R.styleable.SimpleCustomAttributeView = ${R.styleable.SimpleCustomAttributeView.joinToString(transform = {it.toString(16)})}")
// 使用十进制打印
Log.d(TAG, "R.styleable.SimpleCustomAttributeView = ${R.styleable.SimpleCustomAttributeView.joinToString()}")
Log.d(TAG, "R.styleable.SimpleCustomAttributeView_age=${R.styleable.SimpleCustomAttributeView_age}")
Log.d(TAG, "R.styleable.SimpleCustomAttributeView_gender=${R.styleable.SimpleCustomAttributeView_gender}")
Log.d(TAG, "R.styleable.SimpleCustomAttributeView_name=${R.styleable.SimpleCustomAttributeView_name}")

打印如下:

D/SimpleCustomAttribute: R.styleable.SimpleCustomAttributeView = 7f030027, 7f0301d6, 7f0302f2
D/SimpleCustomAttribute: R.styleable.SimpleCustomAttributeView = 2130903079, 2130903510, 2130903794
D/SimpleCustomAttribute: R.styleable.SimpleCustomAttributeView_age=0
D/SimpleCustomAttribute: R.styleable.SimpleCustomAttributeView_gender=1
D/SimpleCustomAttribute: R.styleable.SimpleCustomAttributeView_name=2

看一下打印结果:从 16 进制打印结果看,这个数组的内容应该是 3 个 id;从 10 进制的打印结果看,这个数组的三个元素是按照升序排列的。

其他三个的打印分别是0,1,2,它们应该就是数组的三个索引位置了吗?

我们再去看看 R.txt,把相关内容拷贝如下:

int[] styleable SimpleCustomAttributeView { 0x7f030027, 0x7f0301d6, 0x7f0302f2 }
int attr age 0x7f030027
int attr gender 0x7f0301d6
int attr name 0x7f0302f2
int styleable SimpleCustomAttributeView_age 0
int styleable SimpleCustomAttributeView_gender 1
int styleable SimpleCustomAttributeView_name 2

注意啊:这些内容不是在一起拷贝出来的,只是有意把它们放在一起了。

到这里,我们可以知道:

R.styleable.SimpleCustomAttributeView 这个数组里面存放的是自定义属性的 id 值,并且它们是按照升序排列的;

R.styleable.SimpleCustomAttributeView_age=0,R.styleable.SimpleCustomAttributeView_gender=1,R.styleable.SimpleCustomAttributeView_name=2,对应的是数组中的三个索引。

如果觉得看 R.txt 不习惯,可以看 \app\build\intermediates\compile_and_runtime_not_namespaced_r_class_jar\debug\R.jar,这里仍然把相关内容拷贝如下:

package com.example.customattributesstudy;

public final class R {
    public static final class attr {
        public static final int age = 2130903079;
        public static final int gender = 2130903510;
        public static final int name = 2130903794;
    }
    public static final class styleable {      
        public static final int[] SimpleCustomAttributeView = {R.attr.age, R.attr.gender, R.attr.name};
        public static final int SimpleCustomAttributeView_age = 0;
        public static final int SimpleCustomAttributeView_gender = 1;
        public static final int SimpleCustomAttributeView_name = 2;    
    }
}

这下子应该清晰了吧。

declared-styleable 的本质就是 appt 会依据它去默认生成如上所示的一个 R.java 里面的内容。

现在我们知道这个数组的生成规则,完全可以自己手写出来,这样就不必再写declared-styleable了。

定义自定义属性 id 的数组和相应的索引常量:

companion object {
    private val CUSTOM_ATTRS = intArrayOf(R.attr.age, R.attr.gender, R.attr.name)
    private const val SimpleCustomAttributeView_age = 0
    private const val SimpleCustomAttributeView_gender = 1
    private const val SimpleCustomAttributeView_name = 2
}

解析自定义属性的值:

val ta = context.obtainStyledAttributes(attrs, CUSTOM_ATTRS)
val _age = ta.getInt(SimpleCustomAttributeView_age, 1)
val _gender = ta.getBoolean(SimpleCustomAttributeView_gender, true)
val _name = ta.getString(SimpleCustomAttributeView_name)
Log.d(TAG, "_name=$_name,_age=$_age,_gender=$_gender")
ta.recycle()

打印如下:

D/SimpleCustomAttribute: _name=willwaywang6,_age=18,_gender=true

这里进行手写的目的并不是为了替代默认生成的自定义属性 id 数组,而是为了在某些情况下实现精确地获取自定义属性的值。这里可以参考 RecylerView 开源库中的 RecyclerViewDividerItemDecoration

RecyclerView 中:

private static final int[] CLIP_TO_PADDING_ATTR = {android.R.attr.clipToPadding};
// 解析出 clipToPadding 的值
TypedArray a = context.obtainStyledAttributes(attrs, CLIP_TO_PADDING_ATTR, defStyle, 0);
mClipToPadding = a.getBoolean(0, true); // 这里数组里面只有一个元素,直接传入索引为 0 即可。
a.recycle();

DividerItemDecoration 中:

private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };
public DividerItemDecoration(Context context, int orientation) {
    final TypedArray a = context.obtainStyledAttributes(ATTRS);
    mDivider = a.getDrawable(0); // 这里数组里面只有一个元素,直接传入索引为 0 即可。
    ...
    a.recycle();
    setOrientation(orientation);
}

2.5 ktx 对 TypedArray 的扩展支持

TypedArray.use 自动完成 recycle() 调用

public inline fun <R> TypedArray.use(block: (TypedArray) -> R): R {
    return block(this).also {
        recycle()
    }
}

在代码中使用:

context.obtainStyledAttributes(attrs, R.styleable.SimpleCustomAttributeView).use {
    // 在这里面进行属性解析
}

TypedArray.getXXXOrThrow

会先行检查自定义属性是否定义在属性集合里面,如果没有定义,会直接抛出异常:throw IllegalArgumentException("Attribute not defined in set.");如果定义了,会接着调用相应的 getXXX 方法。

3. 最后

本文从一个简单的自定义属性的例子开始,接着介绍了如何合理地声明和解析自定义属性,然后较为深入了探讨了 AttributeSetTypedArray的作用,declared-styleable 的本质,最后简单地介绍了 ktx 对 TypedArray 的扩展支持。

代码已经上传到github,欢迎大家点赞学习。

4. 参考

  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

willwaywang6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值