目录
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
的值为 SimpleCustomAttributeView
的 declare-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" />
在代码中分别使用 getInt
和 getInteger
来解析:
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
可以看到getInt
和 getInteger
的区别了。
所以,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
getDimension
,getDimensionPixelOffset
和 getDimensionPixelSize
的区别见下表:
方法名 | 返回值 | 含义 |
---|---|---|
getDimension | Float | 将值转为 px |
getDimensionPixelOffset | Int | 将值转为 px,但只保留整数部分。 |
getDimensionPixelSize | Int | 将值转为 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_width
和app: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)
就会只保留自定义的那些属性了,而把系统的属性都过滤掉了。 -
不应用样式
这点是在官方文档上看到的,网上很少有说到这点的。
-
创建一个新的
StyleCustomAttributeView
:class StyleCustomAttributeView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { }
-
定义它的自定义属性,这里和
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>
-
声明一个自定义属性,用于引用后面会引用到的 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) { }
-
在
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>
-
在
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>
-
在 xml 中引用
StyleCustomAttributeView
:<com.example.customattributesstudy.StyleCustomAttributeView android:layout_width="wrap_content" android:layout_height="wrap_content" />
是不是有人会说:这里怎么没有写自定义属性啊?是不是写错了啊?
没有的。就是这样的。
-
在
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
开源库中的 RecyclerView
和 DividerItemDecoration
:
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. 最后
本文从一个简单的自定义属性的例子开始,接着介绍了如何合理地声明和解析自定义属性,然后较为深入了探讨了 AttributeSet
,TypedArray
的作用,declared-styleable
的本质,最后简单地介绍了 ktx 对 TypedArray
的扩展支持。
代码已经上传到github,欢迎大家点赞学习。