自定义View中如果想通过XML文件指定参数,会直接在Res文件下新建的attr.xml
,但是子节点有时候用styleable
,有时候用attr
。以至于对于两个一直有点傻傻分不清,今天搜索研究了几篇博客,算是有了一些眉目,所以在此记录下来,希望对看到博客的人有所帮助。
attr和styleable的关系
<resources>
<attr name="customColor" format="color"/>
<attr name="customText" format="string"/>
</resources>
<resources>
<declare-styleable name="customView">
<attr name="customColor" format="color"/>
<attr name="customText" format="string"/>
</declare-styleable>
</resources>
我们先捋清楚这两个节点的关系,这两个节点,无论使用哪个,所达到的效果都是一样的。attr
不依赖于styleable
,styleable
只是为了方便attr
的使用。
每定义一个attr
,就会在R文件中生成一个id,我们去调用的时候会用R.attr.customAttr
操作。
而通过定义一个styleable
,可以在R文件里自动生成一个int[],数组的每个值对应的就是定义在styleable
中的attr
的id.
由此得知,定义一个
declare-styleable
,在获取属性的时候为我们自动提供了一个属性数组。此外,使用declare-styleable
的方式有利于我们我们把相关的属性组织起来,有一个分组的概念,属性的使用范围更加明确。
为View添加自定义XML属性
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
如上是一个TextView的XML属性,我们可以通过android:text
为TextView的文本赋值。
我们有时在自定义View的时候也需要自定义View的XML属性。
假设我们有一个自定义的View,其类名是com.cxx.demo.widget.MyTextView
,其中com.cxx.demo.widget
是应用程序的包名。
我们想要自定义XML属性,总的来说包括三步:
1. 在XML资源文件中定义需要的attr
,指定attr
的数据类型;
2. 在自定义View的构造函数中解析这些从XML中第一段属性值,将其存放到自定义View对应的成员变量中;
3. 在XML布局文件中为自定义View的XML属性赋值。
- 我们需要在res/values目录下新建名字为
attrs.xml
文件(名字可以任意)。然后在该文件中定义MyTextView的XML属性。该文件的根节点为<resources>
,我们在<resources>
节点下可以添加多个<attr>
子节点,通过name
指定属性名称,通过format
指定属性值的类型。如图所示: - 当我们指定了XML属性的名称和属性值的类型后,还需要先在布局文件中声明命名空间,studio的命名空间一般都指定为
xmlns:app="http://schemas.android.com/apk/res-auto"
,这样定义的命名空间自动指向当前App的命名空间。
在定义app的命名空间后,我们就可以为MyTextView属性赋值了。如果app:customColor
指定的format类型为color,那么对应的XML属性值必须为color类型。如图所示:
format支持的类型有enum、boolean、color、dimension、flag、float、fraction、integer、reference、string。
format值 | attr对应值类型 |
boolean | 布尔类型的值,取值只能是true或false。 |
color | 颜色类型的值,例如#ff0000,也可以使用一个指向Color的资源, 比如@android:color/background_dark,但是不能用0xffff0000这样的值。 |
string | 字符串类型。 |
integer | 整数类型,取值只能是整数,不能是浮点数。 |
float | 浮点数类型,取值只能是浮点数或整数。 |
fraction | 百分数类型,取值只能以%结尾,例如30%、120.5%等。 |
dimension | 尺寸类型,例如取值16px、16dp,也可以使用一个指向 比如 |
reference | 只能指向某一资源的ID,例如取值@id/textView。 |
enum | 枚举类型,在定义enum类型的attr时,可以将attr的format设置为enum, 也可以不用设置attr的format属性,但是必须在attr节点下面添加一个或多个enum节点。 |
flag | bit位标记,flag与enum有相似之处,定义了flag的attr,在设置值时,可以通过 |
<!--enum举例-->
<attr name="customAttr">
<enum name="man" value="0" />
<enum name="woman" value="1" />
</attr>
<!--flag举例-->
<attr name="customAttr">
<flag name="none" value="0" />
<flag name="bold" value="0x1" />
<flag name="italic" value="0x2" />
<flag name="underline" value="0x4" />
</attr>
在<attr>节点下通过定义多个<flag>表示其支持的值,value的值一般是0或者是2的N次方(N为大于等于0的整数),
对于上面的例子我们在实际设置值是可以设置单独的值,如none、bold、italic、underline,也可以通过|设置多个值,
例如app:customAttr="italic|underline"。
自定义View的代码为:
package com.cxx.demo.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import com.cxx.demo.R;
/**
* Created by ZHOU on 2018/1/18.
*/
public class MyTextView extends android.support.v7.widget.AppCompatTextView {
//存储要显示的文本
private String mCustomText;
/*** 存储文本的显示颜色*/
private int mCustomColor = 0xFF000000;
//画笔
private TextPaint mTextPaint;
//字体大小
private float fontSize = getResources().getDimension(R.dimen.textSize);
public MyTextView(Context context) {
this(context,null);
}
public MyTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs, defStyleAttr);
}
private void init(AttributeSet attrs, int defStyleAttr) {
//首先判断attrs是否为null
if (attrs!=null){
//获取AttributeSet中所有的XML属性的数量
int count = attrs.getAttributeCount();
//遍历AttributeSet中的XML属性
for (int i = 0; i < count; i++) {
//获取attr的资源ID
int attrsResId = attrs.getAttributeNameResource(i);
switch (attrsResId){
case R.attr.customColor:
//customColor属性
//如果读取不到对应的颜色值,那么就用黑色作为默认颜色
mCustomColor = attrs.getAttributeIntValue(i,0xff000000);
break;
case R.attr.customText:
//customText属性
mCustomText = attrs.getAttributeValue(i);
break;
default:
break;
}
}
}
//初始化画笔
mTextPaint = new TextPaint();
mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextSize(fontSize);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!TextUtils.isEmpty(mCustomText)){
mTextPaint.setColor(mCustomColor);
//将文本绘制显示出来
canvas.drawText(mCustomText,0,fontSize,mTextPaint);
}
}
}
我们在MyTextView 中定义了两个成员变量mCustomText和mCustomColor。MyTextView无论调用哪个构造函数最终都会调用到init方法,我们重点看一下init方法。
- 传递给init方法的是一个AttributeSet对象,可以把它看成一个索引数组,这个数组里面存储着MyTextView在XML中定义属性的索引,通过索引可以得到XML属性名和属性值。
- 通过调用AttributeSet的getAttributeCount()方法可以获得XML属性的数量,然后我们就可以在for循环中通过索引遍历AttributeSet的属性名和属性值。AttributeSet中有很多getXXX方法,一般必须的参数都是索引号,说几个常用的方法:
方法 | 解释 |
getAttributeName (int index) | 得到对应索引的XML属性名 |
getAttributeNameResource (int index) | 得到对应索引的XML属性在R.attr中的资源ID,例如R.attr.customText、R.attr.customColor。 |
getAttributeValue (int index) | 得到对应索引的XML属性值 |
如果index对应的XML属性的format是string,那么通过AttributeSet的
String getAttributeValue (int index)
方法,可以得到对应索引的XML属性的值,该方法返回的是String。除此之外,AttributeSet还有getAttributeIntValue、getAttributeFloatValue、getAttributeListValue等方法,返回不同类型的属性值。
- 这样就可以将XML中定义的属性获取并赋值给成员变量以供使用了。
使用<declare-styleable>
和obtainStyledAttributes方法
我们上面定义的customText和customColor这两个<attr>
属性都是直接在<resources>
节点下定义的,这样定义<attr>
属性存在一个问题:不能通过style或theme设置这两个属性的值。
要想能够通过style或theme设置XML属性的值,需要在<resources>
节点下添加<declare-styleable>
节点,并在<declare-styleable>
节点下定义<attr>
,如下所示:
<resources>
<declare-styleable name="MyTextView">
<attr name="customText" format="string" />
<attr name="customColor" format="color" />
</declare-styleable>
</resources>
<!-- 需要给<declare-styleable>设置name属性,一般name设置为自定义View的名字,我们此处设置为MyTextView。-->
R.styleable.MyTextView
是一个int数组,也就是R.styleable.MyTextView等价于数组[R.attr.customText, R.attr.customColor]。
在<declare-styleable>
中定义的<attr>
在MyTextView中需要通过调用obtainStyledAttributes()
方法来读取解析属性值,其中,Resources.Theme.obtainStyledAttributes有三个重载方法,如下所示:
- public TypedArray obtainStyledAttributes (int[] attrs)
- public TypedArray obtainStyledAttributes (int resid, int[] attrs)
- public TypedArray obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
Resources有一个重载方法
- public TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs)
这几个方法都是返回一个TypedArray对象,这里我们用的是四个参数的方法。如下所示:
private void init(AttributeSet attributeSet, int defStyle) {
//首先判断attributeSet是否为null
if(attributeSet != null){
//获取当前MyView所在的Activity的theme
Resources.Theme theme = getContext().getTheme();
//通过theme的obtainStyledAttributes方法获取TypedArray对象
TypedArray typedArray = theme.obtainStyledAttributes(attributeSet, R.styleable.MyTextView, 0, 0);
//获取typedArray的长度
int count = typedArray.getIndexCount();
//通过for循环遍历typedArray
for(int i = 0; i < count; i++){
//通过typedArray的getIndex方法获取指向R.styleable中对应的属性ID
int styledAttr = typedArray.getIndex(i);
switch (styledAttr){
case R.styleable.MyView_customText:
//如果是R.styleable.MyView_customText,表示属性是customText
//通过typedArray的getString方法获取字符串值
mCustomText = typedArray.getString(i);
break;
case R.styleable.MyView_customColor:
//如果是R.styleable.MyView_customColor,表示属性是customColor
//通过typedArray的getColor方法获取整数类型的颜色值
mCustomColor = typedArray.getColor(i, 0xFF000000);
break;
}
}
//在使用完typedArray之后,要调用recycle方法回收资源
typedArray.recycle();
}
...
}
TypedArray是一个数组,通过该数组可以获取应用了style和theme的XML属性值。我们看上面的obtainStyledAttributes()方法,后面两个参数暂且忽略不计,后面会介绍。第一个参数还是AttributeSet对象,第二个参数是一个int类型的数组,该数组表示想要获取的属性值的属性的R.attr中的ID,此处我们传入的是R.styleable.MyTextView,在上面我们已经提到其值等价于[R.attr.customText, R.attr.customColor],表示我们此处想获取customText和customColor这两个属性的值。
注:TypedArray其实是用来简化我们的工作的,比如上例,如果布局中的属性的值是引用类型(比如:@dimen/dp100),如果使用AttributeSet去获得最终的像素值,那么需要第一步拿到id,第二步再去解析id。而TypedArray会直接去dp100的值,TypedArray正是帮我们简化了这个过程。
<com.cxx.demo.widget.MyTextView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:customText="customText in AttributeSet"
style="@style/RedStyle"
/>
<style name="RedStyle">
<item name="customText">customText in RedStyle</item>
<!-- 红色 -->
<item name="customColor">#FFFF0000</item>
</style>
View的style属性对应的style资源中定义的XML属性值,其实是View直接在layou文件中定义XML属性值的替补值,是用于补漏的,AttributeSet(即在layout中直接定义XML属性)的优先级高于style属性中资源所定义的属性值。
obtainStyledAttributes方法之defStyleAttr
方法obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
中的第三个参数defStyleAttr
,这个参数表示的是一个<style>
中某个属性的ID( R.attr.***
),当Android在AttributeSet和style属性所定义的style资源中都没有找到XML属性值时,就会尝试查找当前theme(theme其实就是一个<style>
资源)中属性为defStyleAttr的值,如果其值是一个style资源,那么Android就会去该资源中再去查找XML属性值。
obtainStyledAttributes方法之defStyleRes
方法obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
中的第四个参数defStyleRes。这个参数表示的是一个<style>
,(R.style.***)
。与defStyleAttr类似,defStyleRes是前面几项的替补值,defStyleRes的优先级最低。与defStyleAttr不同的是,defStyleRes本身直接表示一个style资源,而theme要通过属性defStyleAttr间接找到style资源。
总结
- 可以不通过
<declare-styleable>
节点定义XML属性,不过还是建议将XML属性定义在<declare-styleable>
节点下,因为这样Android会在R.styleable下面帮我们生成很多有用的常量供我们直接使用。 - obtainStyledAttributes方法中,优先级从高到低依次是:直接在layout中设置View的XML属性值(AttributeSet) > 设置View的style属性 > defStyleAttr > defStyleRes