自定义属性常规套路
自定义View
, 对Android
开发者来说肯定不会陌生.通常在需要用到自定义属性时,我们都会在attrs.xml
中按如下套路操作.
- 以自定义
View
的类名作为name
,声明declare-styleable
标签
<declare-styleable name="CustomButton">
</declare-styleable>
- 在
declare-styleable
标签内,通过attr
标签来定义属性的名称和类型.
<declare-styleable name="CustomButton">
<attr name="buttonType" format="enum">
<enum name="type1" value="1"/>
<enum name="type2" value="2"/>
<enum name="type3" value="3"/>
</attr>
<attr name="buttonTextColor" format="color"/>
</declare-styleable>
- 在自定义View的类中,至少声明1个和2个参数的构造方法.
public class CustomButton extends View {
public CustomButton(Context context) {
this(context, null);
}
public CustomButton(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
}
- 在构造方法中通过
context.obtainStyledAttributes
方法获取TypedArray
,并最终通过TypedArray
的各种getXXX
方法取出自定义属性的值.
public CustomButton(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomButton, 0, 0);
typedArray.getInt(R.styleable.CustomButton_buttonType, -1);
typedArray.getColor(R.styleable.CustomButton_buttonTextColor, Color.RED);
typedArray.recycle();
}
不知道各位看官,尤其是刚入坑的小伙伴们有没有像曾经的我一样,有很多疑问. 为什么要按照这个套路来?declare-styleable
标签中的name
一定要是自定义View的类名吗? 一定要声明两个构造方法吗?obtainStyledAttributes
方法几个参数分别是什么意思,等等..
别着急, 接下来就分享下我曾经的疑惑以及对应的理解.
Q&A
Q1
Q: declare-styleable
标签中的name
一定要与自定义View的类名相同吗?
A: 当然不一定,只是Google约定我们这样做,当不同时并不会影响我们的业务逻辑;我们尝试将declare-styleable
中的name
改为不同,Android Studio
的Lint
会有如下提示:
勉强过CET-4的渣渣翻译下: 根据约定,自定义
View
和declare-styleable
应该有相同的名称,(编辑器的各种功能依赖于此约定).
所以只是编辑器的各种功能依赖于此约定, 具体是什么哪些功能呢,最直接的就是没有代码提示了.
Q2
Q: declare-styleable
标签是干嘛的,attr
一定要声明在declare-styleable
标签内吗?
A: declare-styleable
表示一个包含了很多自定义属性的集合. 编译后会生成如下常量
// R#attrs
public static final int buttonType = 2130903145;
public static final int buttonTextColor = 2130903142;
// R#styleable
public static final int[] CustomButton = new int[]{R.attr.buttonTextColor, R.attr.buttonType};
public static final int CustomButton_buttonTextColor = 0;
public static final int CustomButton_buttonType = 1;
我们在xml
中定义的declare-styleable
和attr
会在R
的静态内部类styleable
和attr
中根据name
生成对应的常量. 而且还会额外生成自定义属性在数组中的索引.
那么attr
标签一定要声明在descale-styleable
中吗?答案也是否定的.
<attr name="buttonType" format="enum">
<enum name="type1" value="1"/>
<enum name="type2" value="2"/>
<enum name="type3" value="3"/>
</attr>
<attr name="buttonTextColor" format="color"/>
<declare-styleable name="CustomButton">
<attr name="buttonType"/>
<attr name="buttonTextColor"/>
</declare-styleable>
我们如上将attr
定义在外部也同样没问题.而且当不同的declare-styleable
中含有相同name
的attr
时,编译是无法通过(Error: Found item Attr/xxxx more than one time
),这时,我们可以把attr
定义在外部,就可以解决这个问题.
<attr name="buttonType" format="enum">
<enum name="type1" value="1"/>
<enum name="type2" value="2"/>
<enum name="type3" value="3"/>
</attr>
<attr name="buttonTextColor" format="color"/>
<attr name="buttonTextSize" format="dimension"/>
<declare-styleable name="CustomButton">
<attr name="buttonType"/>
<attr name="buttonTextColor"/>
</declare-styleable>
<declare-styleable name="CustomLargeButton">
<attr name="buttonType"/>
<attr name="buttonTextColor"/>
</declare-styleable>
Q3
Q: 一定要声明两个构造方法吗?
A: 实际上不是一定要声明参数为(Context context)
和(Context context, @Nullable AttributeSet attrs)
的构造方法. 而是如果自定义View需要在xml中使用是,一定要直接或间接的调用参数为(Context context, @Nullable AttributeSet attrs)
的构造方法.
// 间接调用两个参数的构造方法
public CustomButton(Context context) {
super(context,null);
}
// 直接声明两个参数的构造方法
public CustomButton(Context context) {
super(context,null);
}
public CustomButton(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
否则在xml
中使用时会报Caused by: java.lang.NoSuchMethodException: <init> [class android.content.Context, interface android.util.AttributeSet]
的异常. 因为xml
中的view
最终会通过LayoutInflate
类的createView(String name, String prefix, AttributeSet attrs)
方法利用反射实例化,而调用就是签名为static final Class<?>[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class};
的构造方法.
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
// name就是自定义view的类名
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
// mConstructorSignature 参数签名是Context.class,AttributeSet.class
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
}
Q4
Q: 构造方法中的参数都是什么含义呢?
A: View
构造方法参数最多有四个,签名是(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)
, View的源码对四个参数分别做出了解释, 这里简单的翻译下
- context: View运行环境的上下文,可以通过context获取当前的theme和resource.
- attrs: View在xml中定义的属性集合, 可以通过attrs获取属性名和属性值
- defStyleAttr: 当前theme中的一个属性名称,它是一个引用并指向一个style,style中的属性会应用到当前view
- defStyleRes: 一个style类型的资源id,当defStyleAttr为0或者defStyleAttr不为0,但当前theme中不包含defStyleAttr对应的属性时, 会将defStyleRes中的属性应用到当前view
除了context,其他三个参数,文字描述可能比较抽象.下面分别演示下实例
attrs
<cc.sadhu.neteasecloudmusic.view.CustomButton
android:layout_width="200dp"
app:buttonTextColor="@color/colorPrimary"
app:buttonType="type1"
android:padding="2dp"
android:layout_height="200dp"/>
假如如上在xml
中声明CustomButton
, 在CustomButton
的构造方法中,可以通过attrs.getAttributeName()
,attrs.getAttributeValue()
获取属性名称和值
int attributeCount = attrs.getAttributeCount();
for (int i = 0; i < attributeCount; i++) {
String name = attrs.getAttributeName(i);
String value = attrs.getAttributeValue(i);
Log.d("CustomButton", "name:" + name + ";value:" + value);
}
/* D/CustomButton: name:padding;value:2.0dip
D/CustomButton: name:layout_width;value:200.0dip
D/CustomButton: name:layout_height;value:200.0dip
D/CustomButton: name:buttonTextColor;value:@2131034159
D/CustomButton: name:buttonType;value:1 */
defStyleAttr
View
构造方法的第三个参数, 很多同学可能比较纳闷这个参数到底是干嘛的.
从上面的文字描述,我们知道他代表一个属性, 并且指向一个style.我们来看看到底怎么使用
// 在attrs中定义一个reference类型的属性
<attr name="customButtonStyle" format="reference"/>
// 在styles中定义一个style,该style中有一个buttonType属性
<style name="customStyle">
<item name="buttonType">type2</item>
</style>
// 在我们的主题使用customButtonStyle属性.指向名为customStyle的style
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="customButtonStyle">@style/customStyle</item>
</style>
// 第三个参数就可以传入R.attr.customButtonStyle这个属性了
public CustomButton(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.customButtonStyle);
}
我们可以从Android
已有的View
入手,来看看TextView
,Button
的defStyleAttr
传的是什么值.
public TextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.textViewStyle);
}
public Button(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.buttonStyle);
}
从源码可以看出,他们的defStyleAttr
分别是com.android.internal.R.attr.textViewStyle
和com.android.internal.R.attr.buttonStyle
, 他们也是一个reference
类型的属性,并且
系统的默认主题包含也两个属性.
// attrs.xml
<!-- Default TextView style. -->
<attr name="textViewStyle" format="reference" />
<!-- Normal Button style. -->
<attr name="buttonStyle" format="reference" />
// theme.xml中定义了很多默认属性
<style name="Theme">
<item name="textViewStyle">@style/Widget.TextView</item>
<item name="buttonStyle">@style/Widget.Button</item>
</style>
// style.xml
<style name="Widget.TextView">
<item name="textAppearance">?attr/textAppearanceSmall</item>
<item name="textSelectHandleLeft">?attr/textSelectHandleLeft</item>
<item name="textSelectHandleRight">?attr/textSelectHandleRight</item>
<item name="textSelectHandle">?attr/textSelectHandle</item>
<item name="textEditPasteWindowLayout">?attr/textEditPasteWindowLayout</item>
<item name="textEditNoPasteWindowLayout">?attr/textEditNoPasteWindowLayout</item>
<item name="textEditSidePasteWindowLayout">?attr/textEditSidePasteWindowLayout</item>
<item name="textEditSideNoPasteWindowLayout">?attr/textEditSideNoPasteWindowLayout</item>
<item name="textEditSuggestionItemLayout">?attr/textEditSuggestionItemLayout</item>
<item name="textEditSuggestionContainerLayout">?attr/textEditSuggestionContainerLayout</item>
<item name="textEditSuggestionHighlightStyle">?attr/textEditSuggestionHighlightStyle</item>
<item name="textCursorDrawable">?attr/textCursorDrawable</item>
<item name="breakStrategy">high_quality</item>
<item name="hyphenationFrequency">normal</item>
</style>
<style name="Widget.Button">
<item name="background">@drawable/btn_default</item>
<item name="focusable">true</item>
<item name="clickable">true</item>
<item name="textAppearance">?attr/textAppearanceSmallInverse</item>
<item name="textColor">@color/primary_text_light</item>
<item name="gravity">center_vertical|center_horizontal</item>
</style>
这样结合文字和代码应该就不难理解defStyleAttr
是什么东西, 具体是怎么使用,我们待会再来分析.
defStyleRes
这个属性很少使用,它表示一个style,我们可以在xml
中定义, 然后作为构造方法的第四个参数传入,具体是怎么使用,我们待会再来分析.
.
<style name="defaultStyle">
<item name="buttonType">type3</item>
</style>
public CustomButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, R.style.defaultStyle);
}
obtainStyledAttributes
上面的Q&A
中已经介绍过了View
构造方法四个参数的含义。我们也知道了参数中defStyleAttr
和defStyleRes
代表什么东西,但是我们还不知道这些参数有什么用.
其实他们都是用于给View设置属性,这些属性的值就通过context.obtainStyledAttributes()
方法获取出来.该方法有四个参数,具体使用方式如下:
public CustomButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomButton, defStyleAttr, defStyleRes);
typedArray.recycle();
}
可以看到,除了第二个参数,其他都来自构造方法的传参.不需要过多介绍.第二个参数传入了一个我们之前声明的declare-styleable
.该方法返回类型是TypedArray
.
总结来说, 一个view
包含的属性.声明在declare-styleable
中,这些属性可以在很多地方复制,比如在布局文件:attrs
,在当前主题中:defStyleAttr
, obtainStyledAttributes
就是将这些这些属性对应的值从可能被赋值的地方取出来,并将相关信息放到TypedArray
中, 而且为了扩展性, 还给我们一个兜底的方案:
当布局文件没有声明或者主题中没有声明.我们还可以从defStyleRes
取出默认值.
文字描述大家可能还是觉得很抽象, 我们依然用例子来说明.
我们先在attrs
中定义两个自定义属性buttonTextColor
和buttonType
,通过declare-styleable
声明它们在CustomButton
这个View
中使用.同时定义个自定义属性customButtonStyle
,类型是reference
,表示customButtonStyle
指向一个style
.
<attr name="buttonTextColor" format="color"/>
<attr name="buttonType" format="enum">
<enum name="type1" value="1"/>
<enum name="type2" value="2"/>
<enum name="type3" value="3"/>
<enum name="type4" value="4"/>
<enum name="type5" value="5"/>
</attr>
<attr name="customButtonStyle" format="reference"/>
<declare-styleable name="CustomButton">
<attr name="buttonType"/>
<attr name="buttonTextColor"/>
</declare-styleable>
我们想给CustomButton
的buttonType
属性赋值有多少种方式呢
- 在布局文件中直接设置
buttonType
属性 - 在布局文件中设置
style
, 在style
中设置buttonType
属性 - 在布局文件中设置
theme
, 在theme
指向的style
中设置customButtonStyle
, 在customButtonStyle
指向的style
中设置buttonType
属性 - 在
AndroidManifest
设置当前布局文件对应的Activity
的theme
,在该theme
指向的style
中设置customButtonStyle
,在customButtonStyle
指向的style
中设置buttonType
属性 - 声明一个
style
,在该style
中设置buttonType
属性,并将该参数作为obtainStyledAttributes
方法的第四个参数传入.
有这么多地方可以设置属性值, 肯定也有个优先级吧. 其实上面的顺序就是对应的优先级.
我们验证下, 上代码:
- attrs.xml
<attr name="buttonTextColor" format="color"/>
<attr name="buttonType" format="enum">
<enum name="type1" value="1"/>
<enum name="type2" value="2"/>
<enum name="type3" value="3"/>
<enum name="type4" value="4"/>
<enum name="type5" value="5"/>
</attr>
<attr name="customButtonStyle" format="reference"/>
<declare-styleable name="CustomButton">
<attr name="buttonType"/>
<attr name="buttonTextColor"/>
</declare-styleable>
- styles.xml
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="customButtonStyle">@style/customStyle</item>
</style>
<style name="customStyle">
<item name="buttonType">type4</item>
<item name="buttonTextColor">@color/colorPrimary</item>
</style>
<style name="defaultStyle">
<item name="buttonType">type5</item>
</style>
<style name="layoutTheme">
<item name="customButtonStyle">@style/styleInLayoutTheme</item>
</style>
<style name="layoutStyle">
<item name="buttonType">type2</item>
</style>
<style name="styleInLayoutTheme">
<item name="buttonType">type3</item>
</style>
- AndroidManifest.xml
<activity android:name=".ui.activity.RankListActivity" android:theme="@style/AppTheme"/>
- 布局文件
<?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"
android:orientation="vertical"
android:id="@+id/llContent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<cc.sadhu.neteasecloudmusic.view.CustomButton
android:layout_width="200dp"
app:buttonTextColor="@color/colorPrimary"
app:buttonType="type1"
style="@style/layoutStyle"
android:theme="@style/layoutTheme"
android:padding="2dp"
android:layout_height="200dp"/>
</LinearLayout>
- CusttomButton.java
public class CustomButton extends View {
public CustomButton(Context context) {
super(context, null);
}
public CustomButton(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.customButtonStyle);
}
public CustomButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, R.style.defaultStyle);
}
public CustomButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomButton, defStyleAttr, defStyleRes);
int type = typedArray.getInt(R.styleable.CustomButton_buttonType, -1);
Log.d("CustomButton", "type:" + type);
typedArray.recycle();
}
}
我们直接运行,打印结果如下:
D/CustomButton: type:1
说明上述五种声明方式中,第一种,在布局文件中直接设置buttonType
属性优先级最高.
我们将布局文件夹中的app:buttonType="type1"
删掉, 打印结果如下:
D/CustomButton: type:2
说明第二种方式,style中的优先级次之.
我们继续将布局文件中的style="@style/layoutStyle"
删掉,打印结果如下:
D/CustomButton: type:3
说明第三种方式,theme中的优先级再次之.
我们接着将布局文件中的android:theme="@style/layoutTheme"
删掉,打印结果如下:
D/CustomButton: type:4
说明第三种方式,theme中的优先级再次之.
最后我们把AppTheme
主题中的customButtonStyle
属性删掉, 打印如下结果:
D/CustomButton: type:5
这说明defStyleRes
作为兜底的方案生效了.
以上也验证了我们的优先级结论.
总结
一不小心, 就写了万把来字, 虽然不是很复杂的问题, 结论也很简单. 但是带着问题去思考的话,会让我们理解得更深入.而且在看源码的过程中也会将很多知识点串联起来. 比如布局文件中的view
如何实例化;LayoutInflate
是如何解析布局文件的;设置属性优先级等问题.有机会,我们再来吹吹逼~~
本文由 sadhu 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为:
2019/07/01 13:16