Android自定义View构造函数详解
初始custom view的构造函数
之前写过一篇实现圆形进度条的博客(),通常我们在实现custom view的时候,都会先继承view并实现view的三个构造函数,例如:
import android.content.context; import android.graphics.canvas; import android.util.attributeset; import android.view.view; public class mycustomview extends view { /** * 第一个构造函数 */ public mycustomview(context context) { this(context, null); } /** * 第二个构造函数 */ public mycustomview(context context, attributeset attrs) { this(context, attrs, 0); } /** * 第三个构造函数 */ public mycustomview(context context, attributeset attrs, int defstyle) { super(context, attrs, defstyle); // todo:获取自定义属性 } @override protected void ondraw(canvas canvas) { super.ondraw(canvas); } }
网上有很多关于三个构造函数使用时机的说法,但是说法正确的却没有几家,这里正式的给大家科普一下:
在代码中直接new一个custom view实例的时候,会调用第一个构造函数.这个没有任何争议.
在xml布局文件中调用custom view的时候,会调用第二个构造函数.这个也没有争议.
在xml布局文件中调用custom view,并且custom view标签中还有自定义属性时,这里调用的还是第二个构造函数.
也就是说,系统默认只会调用custom view的前两个构造函数,至于第三个构造函数的调用,通常是我们自己在构造函数中主动调用的(例如,在第二个构造函数中调用第三个构造函数).
至于自定义属性的获取,通常是在构造函数中通过obtainstyledattributes函数实现的.这里先介绍一下如何生成custom view的自定义属性.
生成custom view的自定义属性
custom view添加自定义属性主要是通过declare-styleable标签为其配置自定义属性,具体做法是: 在res/values/目录下增加一个resources xml文件,示例如下(res/values/attrs_my_custom_view.xml):
<resources> <declare-styleable name="mycustomview"> <attr name="custom_attr1" format="string" /> <attr name="custom_attr2" format="string" /> <attr name="custom_attr3" format="string" /> <attr name="custom_attr4" format="string" /> </declare-styleable> <attr name="custom_attr5" format="string" /> </resources>
在上述xml文件中,我们声明了一个自定义属性集mycustomview,其中包含了custom_attr1,custom_att2,custom_attr3,custom_attr4四个属性.同时,我们还声明了一个独立的属性custom_attr5.
所有resources文件中声明的属性都会在r.attr类中生成对应的成员变量:
public final class r { public static final class attr { public static final int custom_attr1=0x7f010038; public static final int custom_attr2=0x7f010039; public static final int custom_attr3=0x7f01003a; public static final int custom_attr4=0x7f01003b; public static final int custom_attr5=0x7f010000; } }
但是声明在标签中的属性,系统还会在r.styleable类中生成相关的成员变量:
public static final class styleable { public static final int[] mycustomview = { 0x7f010038, 0x7f010039, 0x7f01003a, 0x7f01003b }; public static final int mycustomview_custom_attr1 = 0; public static final int mycustomview_custom_attr2 = 1; public static final int mycustomview_custom_attr3 = 2; public static final int mycustomview_custom_attr4 = 3; }
可以看出,r.styleable.mycustomview是一个数组,其中的元素值恰好就是r.attr.custom_attr1~r.attr.custom_attr4的值.而下面的mycustomview_custom_attr1~mycustomview_custom_attr4正好就是其对应的索引.
知道了这些之后,我们就可以来学习一下,如何在custom view的构造函数中获取自定义属性的值了.
在custom view的构造函数中获取自定义属性
在第三个构造函数中获取自定义属性的代码如下:
public mycustomview(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); typedarray ta = context.obtainstyledattributes(attrs, r.styleable.mycustomview); string attr1 = ta.getstring(r.styleable.mycustomview_custom_attr1); string attr2 = ta.getstring(r.styleable.mycustomview_custom_attr2); string attr3 = ta.getstring(r.styleable.mycustomview_custom_attr3); string attr4 = ta.getstring(r.styleable.mycustomview_custom_attr4); log.e("customview", "attr1=" + attr1); log.e("customview", "attr2=" + attr2); log.e("customview", "attr3=" + attr3); log.e("customview", "attr4=" + attr4); ta.recycle(); }
关于自定义属性的获取,我们主要是调用了context.obtainstyledattributes这个函数,相信这个函数大家自定义view的时候都用的很熟练了.我们来看一下这个函数的源码实现:
public final typedarray obtainstyledattributes(attributeset set, @styleableres int[] attrs) { return gettheme().obtainstyledattributes(set, attrs, 0, 0); }
通过对源码的追踪,我们发现context的两个参数的obtainstyledattributes方法最终是调用了theme的4个参数的obtainstyledattributes方法.我们来看一下这个函数的源码实现:
public typedarray obtainstyledattributes(attributeset set, @styleableres int[] attrs, @attrres int defstyleattr, @styleres int defstyleres) { final int len = attrs.length; final typedarray array = typedarray.obtain(resources.this, len); // xxx note that for now we only work with compiled xml files. // to support generic xml files we will need to manually parse // out the attributes from the xml file (applying type information // contained in the resources and such). final xmlblock.parser parser = (xmlblock.parser)set; assetmanager.applystyle(mtheme, defstyleattr, defstyleres, parser != null ? parser.mparsestate : 0, attrs, array.mdata, array.mindices); array.mtheme = this; array.mxml = parser; if (false) { int[] data = array.mdata; system.out.println("attributes:"); string s = " attrs:"; int i; for (i=0; i<set.getattributecount(); i++) { s = s + " " + set.getattributename(i); int id = set.getattributenameresource(i); if (id != 0) { s = s + "(0x" + integer.tohexstring(id) + ")"; } s = s + "=" + set.getattributevalue(i); } system.out.println(s); s = " found:"; typedvalue value = new typedvalue(); for (i=0; i<attrs.length; i++) { int d = i*assetmanager.style_num_entries; value.type = data[d+assetmanager.style_type]; value.data = data[d+assetmanager.style_data]; value.assetcookie = data[d+assetmanager.style_asset_cookie]; value.resourceid = data[d+assetmanager.style_resource_id]; s = s + " 0x" + integer.tohexstring(attrs[i]) + "=" + value; } system.out.println(s); } return array; }
这里就不做过多的源码讲解,而是把这四个参数的含义解释给大家:
attributeset set: 属性值的集合.
int[] attrs: 我们自定义属性集合在r类中生成的int型数组.这个数组中包含了自定义属性的资源id.
int defstyleattr: 这是当前theme中的包含的一个指向style的引用.当我们没有给自定义view设置declare-styleable资源集合时,默认从这个集合里面查找布局文件中配置属性值.传入0表示不像该defstyleattr中查找默认值.
int defstyleres: 这个也是一个指向style的资源id,但是仅在defstyleattr为0或者defstyleattr不为0但theme中没有为defstyleattr属性赋值时起作用.
由于一个属性可以在很多地方对其进行赋值,包括: xml布局文件中、decalare-styleable、theme中等,它们之间是有优先级次序的,按照优先级从高到低排序如下:
属性赋值优先级次序表:
在布局xml中直接定义 > 在布局xml中通过style定义 > 自定义view所在的activity的theme中指定style引用 > 构造函数中defstyleres指定的默认值
为了让大家有更清楚更直观的了解,再接下来设置自定义属性的章节中,我将对custom_attr1~4这4个属性分别在上述四个地方进行定义,然后在custom view的构造函数中获取它的值,从而看一下,优先级顺序是否和我们预期的一样.
设置自定义属性值
以下的第几个参数均是针对resources.theme类的obtainstyledattributes四参数构造方法来说明的.
第二个参数——在布局xml文件中为属性赋值
在设置自定义属性之前,我们首先要在主activity的布局文件中调用我们的custom view,并且为其设置特定的属性.
主布局文件内容如下:
<framelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res-auto" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent"> <com.kevintan.eventbussample.view.mycustomview android:id="@+id/id_custom_view" android:layout_width="400dp" android:layout_height="400dp" custom:custom_attr1="attr1_xml" style="@style/testcustomview"/> </framelayout>
示例结果:
05-28 17:19:56.542 23575-23575/? e/customview: attr1=attr1_xml
05-28 17:19:56.542 23575-23575/? e/customview: attr2=null
05-28 17:19:56.542 23575-23575/? e/customview: attr3=null
05-28 17:19:56.542 23575-23575/? e/customview: attr4=null
注意:
在给自定义属性赋值时,首先需要增加自定义属性的命名空间,例如: xmlns:custom=”http://schemas.android.com/apk/res-auto”,android studio推荐使用res-auto,在eclipse中需要使用custom view所在的包名: xmlns:cv=”http://schemas.android.com/apk/com.kevintan.eventbussample.view”
这里,在布局文件中我们为custom_attr1赋值为: attr1_xml.自定义view中获取该属性值对应了gettheme().obtainstyledattributes方法中的第二个参数@styleableres int[] attrs
第二个参数——在style中为属性赋值
其次,自定义属性还可以在style中进行赋值.
首先,我们在xml布局文件中还为mycustomview增加一个自定义的style,style代码如下:
<style name="testcustomview"> <item name="custom_attr1">attr1_style</item> <item name="custom_attr2">attr2_style</item> </style>
然后,我们修改布局文件,增加style字段:
<framelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res-auto" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent"> <com.kevintan.eventbussample.view.mycustomview android:id="@+id/id_custom_view" android:layout_width="400dp" android:layout_height="400dp" custom:custom_attr1="attr1_xml" style="@style/testcustomview"/> </framelayout>
示例结果:
05-28 17:19:56.542 23575-23575/? e/customview: attr1=attr1_xml
05-28 17:19:56.542 23575-23575/? e/customview: attr2=attr2_style
05-28 17:19:56.542 23575-23575/? e/customview: attr3=null
05-28 17:19:56.542 23575-23575/? e/customview: attr4=null
这里我们再次对custom_attr1属性进行了赋值,同时我们对custom_attr2也进行了赋值.
小提示:
聪明的同学肯定都猜到我这样赋值的作用了,但是还是要简述一下:
对于custom_attr1,我们在xml布局文件、style、defstyle和theme中均进行赋值,那最终得到的结果必然能证实谁的优先级最高.
对于custom_attr2,我们在style、defstyle和theme中进行赋值,通过得到的结果我们能知道谁的优先级第二高.
对于custom_attr3和custom_attr4的赋值情况我就不多解释了,我相信大家都懂得!!
同时,还需要大家注意的是,只要是layout布局文件中,无论是通过namespace直接为属性赋值,还是通过style为属性赋值,在构造函数获取时都对应了gettheme().obtainstyledattributes方法中的第二个参数@styleableres int[] attrs
第三个参数defstyleattr
这个参数的意思是:
原文: an attribute in the current theme that contains areference to a style resource that supplies defaults values for the typedarray. can be 0 to not look for defaults.
翻译: 这是当前theme中的包含的一个指向style的引用.当我们没有给自定义view设置declare-styleable资源集合时,默认从这个集合里面查找布局文件中配置属性值.传入0表示不向该defstyleattr中查找默认值.
为了测试该参数的作用和优先级,我们需要进行如下操作.
首先, 我们先声明一个refrence格式的属性, 用于表示style的引用.声明在之前的res/values/attrs_my_custom_view.xml文件里即可:
<attr name="mycustomviewdefstyleattr" format="reference"/>
然后,需要到androidmanifest.xml中查看包含该自定义view的activity所使用的主题是:
<activity> android:name="com.kevintan.eventbussample.mainactivity" android:theme="@style/apptheme" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.main" /> <category android:name="android.intent.category.launcher" /> </intent-filter> </activity>
最后,在style.xml中的apptheme主题下增加mycustomviewdefstyleattr的引用实现.
<style name="apptheme" parent="android:theme.holo.light.darkactionbar"> <!-- customize your theme here. --> <item name="mycustomviewdefstyleattr">@style/mycustomviewdefstyleattrimpl</item> </style> <style name="mycustomviewdefstyleattrimpl"> <item name="custom_attr1">attr1_defstyleattr</item> <item name="custom_attr2">attr2_defstyleattr</item> <item name="custom_attr3">attr3_defstyleattr</item> </style>
代码验证,记住如果要使用obtainstyledattributes方法的第三个参数, 就需要在第三个构造函数中显示的调用gettheme()的obtainstyledattributes方法.
public mycustomview(context context, attributeset attrs) { // 为defstyleattr进行赋值 this(context, attrs, r.attr.mycustomviewdefstyleattr); } public mycustomview(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); typedarray ta = context.gettheme().obtainstyledattributes(attrs, r.styleable.mycustomview, defstyleattr, 0); string attr1 = ta.getstring(r.styleable.mycustomview_custom_attr1); string attr2 = ta.getstring(r.styleable.mycustomview_custom_attr2); string attr3 = ta.getstring(r.styleable.mycustomview_custom_attr3); string attr4 = ta.getstring(r.styleable.mycustomview_custom_attr4); log.e("customview", "attr1=" + attr1); log.e("customview", "attr2=" + attr2); log.e("customview", "attr3=" + attr3); log.e("customview", "attr4=" + attr4); ta.recycle(); }
代码中,有两点需要大家注意:
我在第二个构造参数中已经对defstyleattr进行了赋值,第三个构造参数直接使用传入参数即可.
第三个构造参数中,我使用了gettheme()的obtainstyledattributes方法来代替context的2个参数的obtainstyledattributes构造方法.
示例结果:
05-28 17:19:56.542 23575-23575/? e/customview: attr1=attr1_xml
05-28 17:19:56.542 23575-23575/? e/customview: attr2=attr2_style
05-28 17:19:56.542 23575-23575/? e/customview: attr3=attr3_defstyleattr
05-28 17:19:56.542 23575-23575/? e/customview: attr4=null
从结果可以看出,在主题中指定style引用的优先级是低于在xml中直接赋值和使用style字段的.
同时,我们还需要了解一点:
在android系统中的控件,很多都在构造参数中使用了第三个参数,例如button.这样做的好处是: 当我们切换不同的主题时,button的样式也能随之进行改变.
第四个参数——通过defstyleres为属性赋值
这个参数的意思是:
原文: a resource identifier of a style resource that supplies default values for the typedarray, used only if defstyleattr is 0 or can not be found in the theme. can be 0 to not look for defaults.
翻译: 这是一个指向style的资源id,但是仅在defstyleattr为0或者defstyleattr不为0但theme中没有为defstyleattr属性赋值时起作用.
通过翻译,我们可以明确两点:
1.defstyleres: 指向一个style引用.
2.defstyleres的优先级低于defstyleattr.
为了验证,我们先在theme.xml文件中定义一个style:
<style name="mycustomviewdefstyleres"> <item name="custom_attr1">attr1_defstyleres</item> <item name="custom_attr2">attr2_defstyleres</item> <item name="custom_attr3">attr3_defstyleres</item> <item name="custom_attr4">attr4_defstyleres</item> </style>
然后,我们在自定义view的第三个构造函数中的obtainstyledattributes函数中进行赋值,具体方法如下:
public mycustomview(context context, attributeset attrs) { this(context, attrs, r.attr.mycustomviewdefstyleattr); } public mycustomview(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); // 为defstyleres进行赋值 typedarray ta = context.gettheme().obtainstyledattributes(attrs, r.styleable.mycustomview, defstyleattr, r.style.mycustomviewdefstyleres); string attr1 = ta.getstring(r.styleable.mycustomview_custom_attr1); string attr2 = ta.getstring(r.styleable.mycustomview_custom_attr2); string attr3 = ta.getstring(r.styleable.mycustomview_custom_attr3); string attr4 = ta.getstring(r.styleable.mycustomview_custom_attr4); log.e("customview", "attr1=" + attr1); log.e("customview", "attr2=" + attr2); log.e("customview", "attr3=" + attr3); log.e("customview", "attr4=" + attr4); ta.recycle(); }
测试结果:
05-28 17:44:09.282 3137-3137/? e/customview: attr1=attr1_xml
05-28 17:44:09.282 3137-3137/? e/customview: attr2=attr2_style
05-28 17:44:09.282 3137-3137/? e/customview: attr3=attr3_defstyleattr
05-28 17:44:09.282 3137-3137/? e/customview: attr4=null
重点:
如果大家认真的看实验结果,肯定会被上面的结果感到奇怪,明明指定了defstyleres,为什么attr4的值还是null?
是因为之前讲过defstyleres的使用优先级:只有当defstyleattr为0或者当前theme中没有给defstyleattr属性赋值时才起作用.
所以,这里我们需要修改构造函数,将defstyleattr设置为0.
public mycustomview(context context, attributeset attrs) { // 为了验证defstyleres的作用,将defstyleattr设置为0 this(context, attrs, 0); } public mycustomview(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); typedarray ta = context.gettheme().obtainstyledattributes(attrs, r.styleable.mycustomview, defstyleattr, r.style.mycustomviewdefstyleres); string attr1 = ta.getstring(r.styleable.mycustomview_custom_attr1); string attr2 = ta.getstring(r.styleable.mycustomview_custom_attr2); string attr3 = ta.getstring(r.styleable.mycustomview_custom_attr3); string attr4 = ta.getstring(r.styleable.mycustomview_custom_attr4); log.e("customview", "attr1=" + attr1); log.e("customview", "attr2=" + attr2); log.e("customview", "attr3=" + attr3); log.e("customview", "attr4=" + attr4); ta.recycle(); }
最终结果:
05-28 17:49:03.707 5772-5772/? e/customview: attr1=attr1_xml
05-28 17:49:03.707 5772-5772/? e/customview: attr2=attr2_style
05-28 17:49:03.707 5772-5772/? e/customview: attr3=attr3_defstyleres
05-28 17:49:03.707 5772-5772/? e/customview: attr4=attr4_defstyleres
后记
在文章结尾, 我们再次总结一下自定义属性的属性赋值优先级:
在布局xml中直接定义 > 在布局xml中通过style定义 > 自定义view所在的activity的theme中指定style引用 > 构造函数中defstyleres指定的默认值.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。