Android(安卓)-- 自定义 View XML属性详解
1. View的构造函数
自定义View必须重写至少一个构造函数
// 如果View是在Java代码里面new的,则调用第一个构造函数 public CustomView7(Context context) { super(context); } // 如果View是在.xml里声明的,则调用第二个构造函数 public CustomView7(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } // 不会自动调用 // 一般是在第二个构造函数里主动调用 // 如View有style属性时 public CustomView7(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } //API21之后才使用 // 不会自动调用 // 一般是在第二个构造函数里主动调用 // 如View有style属性时 public CustomView7(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); }
下面对构造函数的参数使用进行详解
2. 为View添加自定义XML属性
Android中的各种Widget都提供了很多XML属性,我们可以利用这些XML属性在layout文件中为Widget的属性赋值。
如下所示:
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" />
我们可以通过TextView所提供的的XML属性android:text
为TextView的文本赋值。
在自定义View的时候也会经常需要自定义View的XML属性。
想要自定义XML属性,总的来说包括三步:
- 在xml资源文件中定义各种
attr
,并指定attr
的数据类型。 - 在自定义View的构造函数中解析这些从XML中定义的属性值,将其存放到View对应的成员变量中。
- 在layout文件中为自定义View的XML属性赋值。
下面围绕这三步对 自定义View XML属性 进行详解。
假设我们有一个自定义View,其类名是 com.example.jjzg.customview.view.CustomView6,其中 com.example.jjzg是应用程序的包名。
CustomView6直接继承自View,想让CustomView6可以显示文本,即传递文本给CustomView6,相当于非常简单的TextView。
2.1 在xml资源文件中定义attr
首先,在res/values目录新建一个名为attrs.xml文件(文件名可随意命名,只要是xml文件就行),在该文件中定义CustomView6所支持的XML属性。
该文件的根节点是
,在
节点下可以添加多个节点,在
节点中通过
name
指定XML属性名称,通过format
指定XML属性值的类型。
由上图可知,·format
支持多种类型。
format
支持的类型有:
-
enum
表示attr是枚举类型。
在定义attr时,可以将attr的format设置为enum,也可以不用设置attr的format属性,但必须在attr节点下添加一个或多个enum节点,如下所示:<attr name="customAttr"> <enum name="man" value="0" /> <enum name="woman" value="1" /></attr>
这样attr的属性值只能取man或woman了。
-
boolean
表示attr是布尔类型的值,取值只能是true或false。 -
color
表示attr是类型颜色,例如#ff0000,也可以使用一个指向Color的资源,如@android:color/background_dark,但是不能用0xffff0000这样的值。 -
dimension
表示attr是尺寸类型,如取值16px、16dp,也可以使用一个指向dimen
类型的资源,如@android:dimen/app_icon_size。 -
float
表示attr是浮点数类型,取值只能是浮点数或整数。 -
fraction
表示attr是百分数类型,取值只能以%结尾,例如30%、120.5%等。 -
integer
表示attr是整数类型,取值只能是整数,不能是浮点数。 -
string
表示attr是字符串类型。 -
reference
表示attr的值只能指向某一资源的ID,如取值@id/textView。(下面的例子中会使用到) -
flag
表示attr是bit位标记。
flag和enum有相似之处,定义了flag的attr,在设置值时,可以通过|
设置多个值,而且每个值都对应一个bit位,这样通过按位或操作符|
可以将多个值合成一个值。
一般在用flag表示某个字段支持多个特性,需要注意的是:要想使用flag类型,不要设置attr的format的属性,直接在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>
在节点下通过定义多个表示其支持的值,value的值一般是0或者是2的N次方(N为大于等于0的整数)。
对于上面的例子我们在实际设置值是可以设置单独的值,如none、bold、italic、underline,也可以通过|
设置多个值,例如app:customAttr="italic|underline"
。
根据上面的需求,要传递文本给CustomView6,所以在attrs.xml文件里定义两个属性,文本和文本颜色:
<?xml version="1.0" encoding="utf-8"?><resources> <attr name="customText_6" format="string" /> <attr name="customColor_6" format="color" /></resources>
2.2 在layout文件中为自定义View的XML属性赋值
当指定了XML属性的名称和属性值的类型后,就可以在layout文件中通过XML属性为其赋值了。
通过com.example.jjzg.customview.view.CustomView6
在layout中引入了CustomView6,为了能够使用自定义属性,通常要指定一个 自定义的命名空间 以区别于Android的命名空间xmlns:android
。
2.2.1 自定义的命名空间
自定义命名空间的名字可以是任意的,通常一般用xmlns:app
。
不同的开发软件,自定义命名空间的写法也是不一样的:
- Eclipse开发工具
可以这样定义命名空间xmlns:app=http://schemas.android.com/apk/res/com.example.jjzg
,其中com.example.jjzg是应用程序的包名。 - Android Studio开发工具
如果在Android Studio中采用Eclipse那种命名空间的格式,是有问题的。Android Studio使用Gradle进行build,而Gradle不允许自定义的命名空间以包名结尾。正确格式:xmlns:app="http://schemas.android.com/apk/res-auto"
,这样定义的命名空间自动指向当前App的命名空间。
2.2.2 为XML属性赋值
在正确定义app命名空间之后,就可以用app:customText_6
为CustomView6的customText_6属性赋值了。
2.3 在自定义View的构造函数中解析XML中定义的属性值
在自定义View的构造函数中解析这些从XML中定义的属性值,将其存放到View对应的成员变量中。
2.3.1 基本用法
前面已经在attrs.xml文件中对自定义控件CustomView6定义了属性,并在layout文件中对其属性进行了赋值,接下来看下自定义CustomView6的具体实现:
package com.example.jjzg.customview.view;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.support.annotation.Nullable;import android.text.TextUtils;import android.util.AttributeSet;import android.view.View;import com.example.jjzg.R;public class CustomView6 extends View { //存储要显示的文本 private String mCustomText; //存储文本的显示颜色 private int mCustomColor = Color.RED; //画笔 private Paint mTextPaint; //字体大小 private float fontSize = getResources().getDimension(R.dimen.font_size_16); public CustomView6(Context context) { this(context, null); } public CustomView6(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public CustomView6(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(attrs); } private void init(AttributeSet attrs){ if (attrs != null){ //获取AttributeSet中所有的XML属性的数量 int count = attrs.getAttributeCount(); //遍历AttributeSet中的XML属性 for (int i = 0; i < count; i++){ //获取attr的资源ID int attrResId = attrs.getAttributeNameResource(i); switch (attrResId){ case R.attr.customText_6: //customText属性 mCustomText = attrs.getAttributeValue(i); break; case R.attr.customColor_6: //customColor属性 //如果读取不到对应的颜色值,那么就用红色作为默认颜色 mCustomColor = attrs.getAttributeIntValue(i, Color.RED); break; } } mTextPaint = new Paint(); mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG); mTextPaint.setTextSize(fontSize); mTextPaint.setColor(mCustomColor); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (!TextUtils.isEmpty(mCustomText)){ canvas.drawText(mCustomText, 0, fontSize, mTextPaint); } }}
在CustomView6中定义了两个成员变量mCustomText和mCustomColor,CustomView6的几个构造函数都会调用init方法,这里重点看下init方法:
-
传递给init方法的是一个 AttributeSet 对象,可以把它看成是一个索引数组,这个数组里存储着属性的索引,通过索引可以得到XML属性名和属性值。
-
通过调用 AttributeSe t的 getAttributeCount() 方法可以获得XML属性的数量,之后就可以在for循环中通过索引遍历 AttributeSet 的属性名和属性值。
AttributeSet 中有很多getXXX方法,一般必须的参数都是索引号,说几个常用的方法:- 通过 AttributeSet 的
public abstract String getAttributeName (int index)
方法可以得到对应索引的XML属性名。 - 通过 AttributeSet 的
public abstract int getAttributeNameResource (int index)
方法可以得到对应索引的XML属性在R.attr中的资源ID,例如 R.attr.customText、R.attr.customColor。 - 如果index对应的XML属性的format是string,那么通过 AttributeSet 的
public abstract String getAttributeName (int index)
方法可以得到对应索引的XML属性的值,该方法返回的是String。
除此之外,AttributeSet 还有 getAttributeIntValue、getAttributeFloatValue、getAttributeListValue 等方法,返回不同类型的属性值。
- 通过 AttributeSet 的
-
通过 attrs.getAttributeNameResource(i) 得到获取 attr 的资源 ID,然后对attrResId进行switch判断:
- 如果是 R.attr.customText,表示当前属性是customText,通过 attrs.getAttributeValue(i) 读取customText属性值,并将其赋值给成员变量 mCustomText。
- 如果是 R.attr.customColor,表示当前属性是customColor,由于Android中用一个4字节的int型整数表示颜色,所以我们通过 attrs.getAttributeIntValue(i, 0xFF000000) 读取了customColor的颜色值,并将其赋值给成员变量mCustomColor。
-
重写onDraw方法,把画笔mTextPaint的颜色设置为mCustomColor颜色,通过执行canvas.drawText()将mCustomText绘制到界面上。这样,CustomView6就使用了customText和customColor这两个XML属性。
运行效果如下:
2.3.2 使用
和obtainStyledAttributes方法
在上面的attrs.xml文件中,customText和customColor这两个属性都是直接在
节点下定义的。
这样定义属性存在一个问题:不能通过style或theme设置这两个属性的值。
要想能通过style或theme设置XML属性的值,需要在
节点下添加
节点,并在
节点下定义。
如下所示:
<resources> <declare-styleable name="MyView"> <attr name="customText" format="string" /> <attr name="customColor" format="color" /> </declare-styleable></resources>
需要给declare-styleable
设置name属性,一般name设置为自定义View的名字,我们此处设置为MyView。
2.3.2.1 不同节点下定义attr的区别
在
下面定义属性 与 直接在
下面定义属性 其 本质上没有太大区别,无论哪种方式定义
,都会在 R.attr 类中定义 R.attr.customText 和 R.attr.customColor。
不同的是,
节点会在 R.styleable 这个内部类中有如下定义:
R.styleable.MyView是一个int数组,其值为0x7f010038和 0x7f010039。0x7f010038就是属性R.attr.customText,0x7f010039就是属性R.attr.customColor。也就是R.styleable.MyView等价于数组[R.attr.customText, R.attr.customColor]。
同样可以在 R.styleable 中发现 R.styleable.MyView_customColor 和 R.styleable.MyView_customText 这两个ID。
中的name加上里面的属性的name就组成了R.styleable中的MyView_customColor 和 MyView_customText,中间以下划线连接。如下图所示:
其中 R.styleable.MyView_customColor 对应 R.attr.customColor,R.styleable.MyView_customText 对应 R.attr.customText。
再来看下直接在
下面定义,在 R.attr 中会发现有 customText 和 customColor,如下图所示:
R.attr.customText 和 R.attr.customColor 分别是属性 customText 和 customColor 的资源ID。
2.3.2.2 obtainStyledAttributes的使用
在
中定义的,在MyView中需要通过调用obtainStyledAttributes方法来读取解析属性值。
obtainStyledAttributes有三个重载方法,分别如下所示:
- public TypedArray obtainStyledAttributes (int[] attrs)
- public TypedArray obtainStyledAttributes (int resid, int[] attrs)
- public TypedArray obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
直接来看第三个最复杂的方法。
先在res/valeus/styles.xml文件中定义style:
<style name="YellowStyle"> <item name="customText">customText in YellowStyle</item> <item name="customColor">#FFEB3B</item> </style>
然后在layout文件中将CustomView7的style属性设置为上面的style:
<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" android:orientation="vertical"> <com.example.jjzg.customview.view.CustomView7 android:layout_width="wrap_content" android:layout_height="wrap_content" app:customText="customText in AttributeSet" style="@style/YellowStyle"/></LinearLayout>
接着看下CustomView7:
(CustomView7 和 CustomView6 的代码基本相同,唯一的不同点在init方法中,这里主要对init方法进行详解)
...private void init(Context context, AttributeSet attrs, int defStyleAttr){ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView, 0, 0); mCustomText = typedArray.getString(R.styleable.MyView_customText); mCustomColor = typedArray.getColor(R.styleable.MyView_customColor, Color.RED); typedArray.recycle(); mTextPaint = new Paint(); mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG); mTextPaint.setTextSize(fontSize); mTextPaint.setColor(mCustomColor); } ...
运行效果如下:
解析init方法:
- 通过
context.obtainStyledAttributes(attrs, R.styleable.MyView, 0, 0)
方法返回TypedArray对象。
TypedArray是一个数组,通过该数组可以获取应用style和theme的XML属性值。
上面这个方法有四个参数,后面两个参数都是0,大家暂且忽略不计,后面会介绍。
参数1:AttributeSet对象,存放自定义XML属性值。
参数2:int类型的数组,该数组表示想要获取属性值的属性的R.attr中的ID,此处我们传入的是R.styleable.MyView,在上面我们已经提到其值等价于[R.attr.customText, R.attr.customColor],表示我们此处想获取customText和customColor这两个属性的值。 - 如果在layout文件中直接为CustomView7设置了某些XML属性,那么这些XML属性及其属性值就会出现在AttributeSet中,那Android就会直接使用AttributeSet中该XML属性值作为context.obtainStyledAttributes的返回值。
如上面的例子中,通过app:customText="customText in AttributeSet"
设置了CustomView7的XML属性,最终运行的效果显示的也是文本”customText in AttributeSet”。 - 如果在layout文件中没有为CustomView7设置某个XML属性,但是给其设置了style属性,例如
style="@style/RedStyle"
,并且在style中指定了相应的XML属性,那么Android就会用style属性所对应的style资源中的XML属性值作为context.obtainStyledAttributes的返回值。
如上面的例子中,我们在layout文件中没有设置app:customColor的值,但是在其style属性所对应的YellowStyle资源中将customColor设置成了黄色,最终文本也是以黄色显示在界面上的。
小结:
View的style属性对应的style资源中定义的XML属性值其实是View直接在layou文件中定义XML属性值的替补值,是用于补漏的,AttributeSet(即在layout中直接定义XML属性)的优先级高于style属性中资源所定义的属性值。
2.3.2.3 obtainStyledAttributes方法之defStyleAttr
上面讲了obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
方法中的两个参数,再来看下第三个参数defStyleAttr。
defStyleAttr:
- 表示
中某个属性的ID
- 当在AttributeSet和style属性所定义的style资源中都没有找到XML属性值时,就会查找当前theme(theme其实就是一个
资源)中属性为defStyleAttr的值,如果其值是一个style资源,那Android就会去该资源中再去查找XML属性值。
听起来比较费劲,下面举例说明。
更改values/styles.xml文件:
<resources> <!-- Base application theme. --> <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="myViewStyle">@style/GreenStyle</item> </style> <style name="GreenStyle"> <item name="customText">customText in GreenStyle</item> <item name="customColor">#FF00FF00</item> </style> <style name="HelloWorldStyle"> <item name="customText">customText in HelloWorldStyle</item> </style> <style name="YellowStyle"> <item name="customText">customText in YellowStyle</item> <item name="customColor">#FFEB3B</item> </style></resources>
这里添加了HelloWorldStyle和GreenStyle,其中HelloWorldStyle只定义了customText的属性值,而GreenStyle同时定义了customText和customColor的值。
在AppTheme中,设置了myViewStyle这个属性的值:
<item name="myViewStyle">@style/GreenStyle</item>
myViewStyle这个属性是在values/attrs.xml中定义的:
<resources> <attr name="myViewStyle" format="reference" /> <declare-styleable name="MyView"> <attr name="customText" format="string" /> <attr name="customColor" format="color" /> </declare-styleable></resources>
myViewStyle被定义为一个reference格式,即指向一个资源类型。
我们在AppTheme中将其赋值为@style/GreenStyle
,即在AppTheme中,myViewStyle的就是GreenStyle,其指向了一个style资源。
更改CustomView7中的init方法:
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView, R.attr.myViewStyle, 0);
注意,上面obtainStyledAttributes方法最后一个参数还是为0,可以忽略,但是第三个参数的值不再是0,而是R.attr.myViewStyle。
然后更新layout文件:
<com.example.jjzg.customview.view.CustomView7 android:layout_width="wrap_content" android:layout_height="wrap_content" style="@style/HelloWorldStyle"/>
先看下运行效果:
显示的是绿色的customText in HelloWorldStyle,分析下:
- 由于这次没有通过layout文件直接设置CustomView7的XML属性的值,所以AttributeSet本身是没有XML属性值的,我们直接忽略掉AttributeSet。
- 通过
style="@style/HelloWorldStyle"
为CustomView7设置了style为HelloWorldStyle,HelloWorldStyle中定义customText的属性值为“customText in HelloWorldStyle”,所以最终customText的值就是”customText in HelloWorldStyle”,在界面上显示的也是该值。 - HelloWorldStyle中并没有定义customColor的属性值。
将 context.obtainStyledAttributes 方法的第三个参数设置为R.attr.myViewStyle,此处的theme就是上面提到的AppTheme,Android会去AppTheme中查找属性为myViewStyle的值,之前提到了,它的值就是@style/GreenStyle,即GreenStyle,由于该值是个style资源,Android就会去该资源中查找customColor的值,GreenStyle定义了customColor的颜色为绿色,所以CustomView7最终所使用的customColor的值就是绿色。
小结:
此处的第三个参数的作用是:当在AttributeSet和style属性中都没有找到属性值时,就去Theme的某个属性(即第三个参数)中查看其值是否是style资源,如果有style资源再去这个style资源中查找XML属性值作为替补值。
2.3.2.4 obtainStyledAttributes方法之defStyleRes
最后看一下方法obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
中的第四个参数defStyleRes。
与defStyleAttr类似,defStyleRes是前面几项的替补值,defStyleRes的优先级最低。
与defStyleAttr不同的是,defStyleRes本身直接表示一个style资源,而theme要通过属性defStyleAttr间接找到style资源。
在values/styles.xml文件中添加 BlueStyle这个style:
<resources> <!-- Base application theme. --> <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="myViewStyle">@style/GreenStyle</item> </style> <style name="BlueStyle"> <item name="customText">customText in BlueStyle</item> <item name="customColor">#FF0000FF</item> </style> <style name="GreenStyle"> <item name="customText">customText in GreenStyle</item> <item name="customColor">#FF00FF00</item> </style> <style name="HelloWorldStyle"> <item name="customText">customText in HelloWorldStyle</item> </style> <style name="YellowStyle"> <item name="customText">customText in YellowStyle</item> <item name="customColor">#FFEB3B</item> </style></resources>
将layout文件改为如下所示:
<com.example.jjzg.customview.view.CustomView7 android:layout_width="wrap_content" android:layout_height="wrap_content"/>
更改CustomView7中的init方法:
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView, 0, R.style.BlueStyle);
第三个参数设置为0,第四个参数不再是0,而是R.style.BlueStyle。
运行界面如下所示:
小结:
当defStyleAttr(即View的构造函数的第三个参数)不为0且在Theme中有为这个attr赋值时,defStyleRes(通过obtainStyledAttributes的第四个参数指定)不起作用。
3. 总结
- 可以不通过
节点定义XML属性,不过还是建议将XML属性定义在
节点下,这样Android会在R.styleable下面帮我们生成很多有用的常量供我们直接使用。 - 在obtainStyledAttributes方法中,优先级从高到低依次是:
直接在layout中设置View的XML属性值(AttributeSet)> 设置View的style属性 > defStyleAttr > defStyleRes。
更多相关文章
- 轻松玩转Camera,使用CameraView来拍照,修改CameraView 实现自定义
- Android(安卓)隐藏底部虚拟键
- android dex分包支持
- 几个Android常见wraning警告处理方法
- eclipse 和 android studio 快捷键对比
- Android中如何实现WebView与JavaScript的相互调用
- android 点击退出程序
- Android(安卓)WebView使用基础
- android系统中的多线程(二): 关于在work thread中对UI进行更新和