Android自定义View是程序猿从初级阶段进阶的必由之路,而自定义View必然会伴随自定义属性,本篇先来讲讲安卓自定义属性
1、自定义View的属性,首先在res/values/ 下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SwipeListView">
<attr name="swipeOpenOnLongPress" format="boolean"/>
<attr name="swipeAnimationTime" format="integer"/>
<attr name="swipeOffsetLeft" format="dimension"/>
<attr name="swipeBackView" format="reference"/>
<attr name="swipeMode" format="enum">
<enum name="none" value="0"/>
<enum name="both" value="1"/>
<enum name="right" value="2"/>
<enum name="left" value="3"/>
</attr>
</declare-styleable>
</resources>
attr子元素:
定义具体的属性,format表示这个属性的值的类型,类型有以下几种:
1.reference:参考指定Theme中资源ID,这个类型意思就是你传的值可以是引用资源
2.string:字符串,如果你想别人既能直接写值也可以用类似"@string/test"引用资源的方式,可以写成format="string|reference"
3.Color:颜色
4.boolean:布尔值
5.dimension:尺寸值
6.float:浮点型
7.integer:整型
8.fraction:百分数
9.enum:枚举 ,如果你提供的属性只能让别人选择,不能随便传入,就可以写成这样
<attr name="language">
<enum name="china" value="1"/>
<enum name="English" value="2"/>
</attr>
10.flag:位或运算
declare-styleable子元素:
定义一个styleable对象,每个styleable对象就是一组attr属性的集合,注意:这里的name属性并不是一定要和自定义类名相同,只是为了好区分对应类的属性而已
注意:上面的属性资源文件定义了该属性之后,至于到底是哪个自定义View组件中来使用该属性,该属性到底能发挥什么作用, 就不归该属性资源文件管了,也就是说这个属性资源文件是个公共的,大家都可以用,但是为了方便管理,一般都是一个自定义View里的属性写成一个declare-styleable集合.属性资源所定义的属性到底可以返回什么作用,取决于自定义组件的代码实现
2.然后在布局中声明我们的自定义View
<com.huaxun.view.SwipeListView.SwipeListView
xmlns:swipe="http://schemas.android.com/apk/com.huaxun.test"
android:id="@+id/music_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
swipe:swipeAnimationTime="500"
swipe:swipeBackView="@+id/back"
swipe:swipeFrontView="@+id/front"
swipe:swipeMode="both"
swipe:swipeOffsetLeft="160dp"
swipe:swipeOpenOnLongPress="true" />
一定要引入 xmlns:swipe="http://schemas.android.com/apk/res/com.huaxun"我们的命名空间,后面的包路径指的是项目的package,不然组件的属性设置不了
更新: 对于自定义属性资源,现在可以不使用http://schemas.android.com/apk/res/<Packge name> 的形式了, 统一用http://schemas.android.com/apk/res-auto
延伸:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
......
</RelativeLayout>
Android应用程序将所有的静态资源都封装在了APK文件中,并根据这些资源文件名(不包括扩展名)或key属性的值生成资源ID。这些ID将作为变量的形式被定义在R类的相应子类中。例如,所有的图像资源(res/drawable目录中的资源文件)都会在R.drawable类中生成相应的变量,变量名就是图像资源的文件名。当使用这些资源时,只要引用R类中相应的变量,系统就会知道上哪去寻找相应的资源。例如,"@string/hello"引用了字符串资源hello。"@drawable/icon"引用了图像资源文件(可能是icon.png、icon.jpg等图像)
系统内部有一个系统级的R.java文件,所有的系统资源生成的ID都在该文件中的R类相关子类中定义。而在这个R类中有一个attr子类,用于定义系统中所有的属性,也就是XML标签设置的属性名,而这个R类的Package就是android。也正是由于上面申明了系统的命名空间,我们才可以使用诸如android:layout_width,android:background等安卓系统属性。
3.在View的构造方法中,获得我们的自定义的属性信息
TypedArray styled = getContext().obtainStyledAttributes(attrs, R.styleable.SwipeListView,defStyle,0);
swipeMode = styled.getInt(R.styleable.SwipeListView_swipeMode, SWIPE_MODE_BOTH);
swipeOffsetLeft = styled.getDimensionPixelSize(R.styleable.SwipeListView_swipeOffsetLeft, 0);
swipeOpenOnLongPress = styled.getBoolean(R.styleable.SwipeListView_swipeOpenOnLongPress, true);
swipeAnimationTime = styled.getInteger(R.styleable.SwipeListView_swipeAnimationTime, 0);
swipeFrontView = styled.getResourceId(R.styleable.SwipeListView_swipeFrontView, 0);
swipeBackView = styled.getResourceId(R.styleable.SwipeListView_swipeBackView, 0);
styled.recycle();
1)先来看看obtainStyledAttributes的四个参数的作用
obtainStyledAttributes这个方法最终调用的的是obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes); 前两个参数一目了然,来看看第三四个参数
先看第四个参数defStyleRes,其实是用于指定一个style,我们在style.xml里面编写
<style name="style_swipeListView">
<item name="swipeOpenOnLongPress">false</item>
<item name="swipeAnimationTime">3000</item>
<item name="swipeOffsetLeft">139dp</item>
</style>
可
以看到我们申明了一个style,然后我们修改刚才获取属性的代码
TypedArray styled = getContext().obtainStyledAttributes(attrs, R.styleable.SwipeListView,0,R.style.style_swipeListView);
在布局文件中不设置任何属性
<com.huaxun.view.SwipeListView.SwipeListView
xmlns:swipe="http://schemas.android.com/apk/com.huaxun"
android:id="@+id/music_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
运行后可以看到,swipeOpenOnLongPress=true, swipeAnimationTime=3000, swipeOffsetLeft=278 //139dp
从结果可以明显看出,如果我们不在布局中设置任何属性,会从style中读取相关属性。
接着看第三个参数defStyleAttr,它是一个引用类型属性,指向一个style,并且在当前Theme中进行设置,去style.xml里面,找到我们使用的Theme,添加一条Item:
<style name="AppTheme" parent="AppBaseTheme">
<item name="attrViewStyleRef">@style/AttrViewStyle</item>
</style>
<style name="attrViewStyleRef">
<item name="swipeOpenOnLongPress">true</item>
<item name="swipeAnimationTime">5000</item>
<item name="swipeOffsetLeft">888dp</item>
</style>
然后我们再次修改刚才获取属性的代码
TypedArray styled = getContext().obtainStyledAttributes(attrs,R.styleable.SwipeListView,R.attr.attrViewStyleRef,0);
运行后可以看到,swipeOpenOnLongPress=false, swipeAnimationTime=5000, swipeOffsetLeft=1776 //888dp
对于第三个参数,实际上用的还是比较多的,比如系统的Button,EditText,它们都会在构造函数指定第三个参数
Public Button(Context context, AttributeSet attrs) { this(context, attrs, com.android.intenel.R.attr.buttonStyle);}
提供一些参数样式,比如background,textColor等,所以我们切换不同的主题,会发现控件的样式会发生一些变化,就是因为不同的主题设置了不同的style。推演到我们自定义View,如果你的属性非常多,你也可以提供默认style,然后让用户去设置到theme里面即可。
只有defStyleAttr设置为0或者Theme中没有找到相关属性时,才会去defStyleRes中读取,defStyleAttr的优先级更高。
2)构造函数中调用初始化代码有两种方式
第一种:
public GifImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public GifImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public GifImageView(Context context) {
this(context, null);
}
第二种:
public GifImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public GifImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public GifImageView(Context context) {
super(context);
init();
}
这两种写法有什么区别呢?
一,如果需要设置obtainStyledAttributes的第三个参数,即defStyledAttr,一般使用第一种方式,会在两个参数构造中调用三个参数的构造函数,(默认调用两个参数的构造函数)同时传入defStyledAttr。如果没有此参数,两种写法没有区别。
二,继承系统已有控件去实现自定义View,比如继承Button,第一种方式会覆盖Button默认在theme里面的style(默认调用两个参数的构造函数),相对来说第二种方式更合适。
3)获取自定义属性有两种方式
第一种:
我们上面的写法
第二种:
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0);
int n = a.getIndexCount();
for (int i = 0; i < n; i++)
{
int attr = a.getIndex(i);
switch (attr)
{
case R.styleable.CustomTitleView_titleText:
mTitleText = a.getString(attr);
break;
case R.styleable.CustomTitleView_titleTextColor:
mTitleTextColor = a.getColor(attr, Color.BLACK);
break;
case R.styleable.CustomTitleView_titleTextSize:
mTitleTextSize = a.getDimensionPixelSize(attr, 10);
break;
}
两种写法的区别:
第一种写法,不管你有没有在布局中使用该属性,都会执行getXXX方法,第二种只有你布局中使用了该属性才会执行getXXX
假设有以下场景:
private int attr_mode = 1; //默认为1
attr_mode = a.getInt(attr, 0);
可能你自定义属性的默认值为1,然而你根本没有在布局文件中设置这个属性,这样运行时它变成了0(而不是默认值),而第二种方法就不存在这个问题了。
还有个场景,假如你是继承某个View,父类View已经对该成员变量进行了赋值,然后你这边需要根据用户的设置去更新这个值,第一种写法如果用户没有设置,你可能就将父类的赋值给覆盖了。