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>
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(); }这两种写法有什么区别呢?
二,继承系统已有控件去实现自定义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已经对该成员变量进行了赋值,然后你这边需要根据用户的设置去更新这个值,第一种写法如果用户没有设置,你可能就将父类的赋值给覆盖了。