自定义view的时候,有时需要用到自定义属性,方便我们定制View。一般来说,自定义属性过程如下:
- 定义属性:在values下的attrs.xml内编写declare-styleable标签来定义属性;
- 使用属性:在布局文件中通过获取
- 获取属性:在自定义view中使用TypedArray获取自定义的属性。
下面按照自定义View的流程讲讲自定义属性:
首先说明一下自定义View的四个构造函数的区别:
public class CustomView extends View {
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
在使用过程中,一般都是联级调用。第一个构造函数用处并不大,主要在Java代码中声明View时才使用;在布局文件中使用自定义的View,则调用的是第二个构造函数;第三,第四个构造函数是与系统主题有关的,从参数defStyleAttr和defStyleRes也可以看得出来;也就是说,在自定义View时,如果不需要view跟随主题改变,则前两个构造函数就足够了。
关于AttributeSet
在第二个构造函数中,有个AttributeSet参数,那这个参数有啥用的呢?
查看源码,我们可以发现AttributeSet是个接口,该接口用于解析xml布局中的属性。举个例子,假设定义了一个CustomerView,在布局中使用:
<com.example.developmc.customview.CustomView
android:layout_width="100dp"
android:layout_height="100dp" />
然后我们在构造函数中用如下代码打印AttributeSet中的属性:
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
int attributeCount = attrs.getAttributeCount();
for (int i = 0; i < attributeCount; i++) {
String attrName = attrs.getAttributeName(i);
String attrVal = attrs.getAttributeValue(i);
Log.e("AttributeSet:", "attrName = " + attrName + " , attrVal = " + attrVal);
}
}
打印结果如下:
attrName = layout_width , attrVal = 100.0dip
attrName = layout_height , attrVal = 100.0dip
可以看到AttributeSet包含了所有在布局中定义的属性,并且能够按顺序地取得各个属性的name和value。换言之,AttributeSet用于解析View在xml布局中的所有属性的name和value,这也是自定义View要使用第二个构造函数的原因。
细心观察一下,我们在布局中layout_width的值是100dp,但打印出来的值却是100dip,这单位不对呀!抱着疑问,我们修改一下布局:
<com.example.developmc.customview.CustomView
android:layout_width="@dimen/dimen_100"
android:layout_height="100dp" />
其中dimen_100的定义是:
<dimen name="dimen_100">100dp</dimen>
再次打印,结果如下:
attrName = layout_width , attrVal = @2131165262
attrName = layout_height , attrVal = 100.0dip
可以看到layout_width变成了一个奇怪的值@2131165262,这个值是怎么来的呢?
我们知道,Android会在R.java中为定义的属性生成资源标识符(一个十六进制的数值)。在app/build/generated/r/debug下找到并打开R.java,找到dimen_100,对应的值是0x7f07004e,将这个数值转为十进制,正好就是“2131165262”!
到这里,我们可以得出结论,当在布局中直接赋值(如:android:layout_width=”100dp”),Attribute拿到的值,数值是正确的,但单位可能会不对; 当在布局中为属性赋引用值(如:android:layout_width=”@dimen/dimen_100″),Attribute拿到的是该值对应的资源标识符!总的来说,Attribute解析出来的属性值并不能直接使用!不要怕,TypedArray就是用于简化这方面的工作的。
关于TypedArray
先来回忆一下,我们是如何同时使用Arrtibute和TypedArray的:
首先,新建文件attrs.xml,为自定义View添加一个自定义属性:
<declare-styleable name="CustomView">
<attr name="customWidth" format="dimension"/>
</declare-styleable>
然后在布局文件中使用自定义的属性customWidth:
<com.example.developmc.customview.CustomView
android:layout_width="@dimen/dimen_100"
android:layout_height="100dp"
app:customWidth="@dimen/dimen_100"/>
最后在构造函数中用TypedArray解析customWidth的value,代码如下:
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
int attributeCount = attrs.getAttributeCount();
for (int i = 0; i < attributeCount; i++) {
String attrName = attrs.getAttributeName(i);
String attrVal = attrs.getAttributeValue(i);
Log.e("AttributeSet:", "attrName = " + attrName + " , attrVal = " + attrVal);
}
TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.CustomView);
float width = array.getDimension(R.styleable.CustomView_customWidth,200f);
Log.e("width:", "widthVal = "+String.valueOf(width));
array.recycle();
}
打印结果是:
attrName = layout_width , attrVal = @2131165262
attrName = layout_height , attrVal = 100.0dip
attrName = customWidth , attrVal = @2131165262
widthVal = 350.0
测试虚拟机的像素是:560dpi,那么通过计算,100dp对应的像素就是350dip
通过打印结果,可以看到使用Attribute和TypedArray的区别:如果使用Attribute,拿到的结果并不能直接使用,需要进一步处理;而TypedArray则直接取得了正确的数值,简化了这个步骤。所以在使用自定义属性时,我们总是应该使用TypedArray的方式获取属性值。
这里需要注意的一点是:每次使用完TypedArray之后,要记得调用recycle()回收。这是为什么呢?
从上述代码我们是通过context.obtainStyledAttributes获取TypeadArray实例的,并不是通过new实例的方式获取的。事实上,TypedArray类,没有公有的构造函数,是一个典型的单例模式,程序在运行时维护一个TypedArray池,使用时,向该池中请求一个实例,用完之后,调用 recycle() 方法来释放该实例,从而使其可被其他模块复用。
关于declare-styleable
我们一般在attrs.xml中通过<declare-styleable>标签声明自定义的属性;先看看下面的代码:
<resources>
<declare-styleable name="CustomView">
<attr name="customWidth" format="dimension"/>
</declare-styleable>
<attr name="customHeight" format="dimension"/>
</resources>
在上述attrs.xml中,我们自定义了两个属性:customWidth和customHeight,其中customHeight没有声明在declare-styleable。运行代码后,在生成的R.java文件中,可以同时看到这两个属性:
public static final class attr {
public static final int customWidth=0x7f0100ce;
public static final int customHeight=0x7f010001;
}
可以看到定义在declare-styleable中与直接用attr定义没有实质的不同,系统都会为我们在R.attr中生成响应的属性。但不同的是,如果声明在declare-styleable中,系统除了在R.java的attr类下生成资源标识符,还会为我们在R.java内的styleable类中生成相关的属性:
public static final class styleable {
public static final int[] CustomView = {
0x7f0100ce
};
}
可以看到customWidth属性对应的标识符0x7f0100ce被保存在styleable内的数组中。如上所示,R.styleable.CustomView是一个int[],而里面的元素正是declare-styleable内声明的元素,这个数组在自定义View的构造函数中获得属性值时会用到。
将自定义属性分组声明在declare-styleabe中的作用就是系统会自动为我们生成这些东西,如果不声明在declare-styleable中,我们也可以在需要的时候自己构建这个数组,只是比较麻烦。当我们有多个自定义View需要用到同一个自定义属性时,就不能同时在两个declare-styleabe下声明同一个属性了(编译不通过),这时就可以把这个属性直接定义在attr下了,然后在需要使用时,自己构建数组引用即可。
本文就到这里,谢谢各位。