Android自定义View构造函数详解
初始CustomView的构造函数
之前写过一篇实现圆形进度条的博客(自定义圆形进度条),通常我们在实现CustomView的时候,都会先继承View并实现View的三个构造函数,例如:
importandroid.content.Context;
importandroid.graphics.Canvas;
importandroid.util.AttributeSet;
importandroid.view.View;
publicclassMyCustomViewextendsView{
/**
*第一个构造函数
*/
publicMyCustomView(Contextcontext){
this(context,null);
}
/**
*第二个构造函数
*/
publicMyCustomView(Contextcontext,AttributeSetattrs){
this(context,attrs,0);
}
/**
*第三个构造函数
*/
publicMyCustomView(Contextcontext,AttributeSetattrs,intdefStyle){
super(context,attrs,defStyle);
//TODO:获取自定义属性
}
@Override
protectedvoidonDraw(Canvascanvas){
super.onDraw(canvas);
}
}
网上有很多关于三个构造函数使用时机的说法,但是说法正确的却没有几家,这里正式的给大家科普一下:
在代码中直接new一个CustomView实例的时候,会调用第一个构造函数.这个没有任何争议.
在xml布局文件中调用CustomView的时候,会调用第二个构造函数.这个也没有争议.
在xml布局文件中调用CustomView,并且CustomView标签中还有自定义属性时,这里调用的还是第二个构造函数.
也就是说,系统默认只会调用CustomView的前两个构造函数,至于第三个构造函数的调用,通常是我们自己在构造函数中主动调用的(例如,在第二个构造函数中调用第三个构造函数).
至于自定义属性的获取,通常是在构造函数中通过obtainStyledAttributes函数实现的.这里先介绍一下如何生成CustomView的自定义属性.
生成CustomView的自定义属性
CustomView添加自定义属性主要是通过declare-styleable标签为其配置自定义属性,具体做法是:在res/values/目录下增加一个resourcesxml文件,示例如下(res/values/attrs_my_custom_view.xml):
<resources> <declare-styleablename="MyCustomView"> <attrname="custom_attr1"format="string"/> <attrname="custom_attr2"format="string"/> <attrname="custom_attr3"format="string"/> <attrname="custom_attr4"format="string"/> </declare-styleable> <attrname="custom_attr5"format="string"/> </resources>
在上述xml文件中,我们声明了一个自定义属性集MyCustomView,其中包含了custom_attr1,custom_att2,custom_attr3,custom_attr4四个属性.同时,我们还声明了一个独立的属性custom_attr5.
所有resources文件中声明的属性都会在R.attr类中生成对应的成员变量:
publicfinalclassR{
publicstaticfinalclassattr{
publicstaticfinalintcustom_attr1=0x7f010038;
publicstaticfinalintcustom_attr2=0x7f010039;
publicstaticfinalintcustom_attr3=0x7f01003a;
publicstaticfinalintcustom_attr4=0x7f01003b;
publicstaticfinalintcustom_attr5=0x7f010000;
}
}
但是声明在标签中的属性,系统还会在R.styleable类中生成相关的成员变量:
publicstaticfinalclassstyleable{
publicstaticfinalint[]MyCustomView={
0x7f010038,0x7f010039,0x7f01003a,0x7f01003b
};
publicstaticfinalintMyCustomView_custom_attr1=0;
publicstaticfinalintMyCustomView_custom_attr2=1;
publicstaticfinalintMyCustomView_custom_attr3=2;
publicstaticfinalintMyCustomView_custom_attr4=3;
}
可以看出,R.styleable.MyCustomView是一个数组,其中的元素值恰好就是R.attr.custom_attr1~R.attr.custom_attr4的值.而下面的MyCustomView_custom_attr1~MyCustomView_custom_attr4正好就是其对应的索引.
知道了这些之后,我们就可以来学习一下,如何在CustomView的构造函数中获取自定义属性的值了.
在CustomView的构造函数中获取自定义属性
在第三个构造函数中获取自定义属性的代码如下:
publicMyCustomView(Contextcontext,AttributeSetattrs,intdefStyleAttr){
super(context,attrs,defStyleAttr);
TypedArrayta=context.obtainStyledAttributes(attrs,R.styleable.MyCustomView);
Stringattr1=ta.getString(R.styleable.MyCustomView_custom_attr1);
Stringattr2=ta.getString(R.styleable.MyCustomView_custom_attr2);
Stringattr3=ta.getString(R.styleable.MyCustomView_custom_attr3);
Stringattr4=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的时候都用的很熟练了.我们来看一下这个函数的源码实现:
publicfinalTypedArrayobtainStyledAttributes(AttributeSetset,@StyleableResint[]attrs){
returngetTheme().obtainStyledAttributes(set,attrs,0,0);
}
通过对源码的追踪,我们发现context的两个参数的obtainStyledAttributes方法最终是调用了Theme的4个参数的obtainStyledAttributes方法.我们来看一下这个函数的源码实现:
publicTypedArrayobtainStyledAttributes(AttributeSetset,
@StyleableResint[]attrs,@AttrResintdefStyleAttr,@StyleResintdefStyleRes){
finalintlen=attrs.length;
finalTypedArrayarray=TypedArray.obtain(Resources.this,len);
//XXXnotethatfornowweonlyworkwithcompiledXMLfiles.
//TosupportgenericXMLfileswewillneedtomanuallyparse
//outtheattributesfromtheXMLfile(applyingtypeinformation
//containedintheresourcesandsuch).
finalXmlBlock.Parserparser=(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:");
Strings="Attrs:";
inti;
for(i=0;i<set.getAttributeCount();i++){
s=s+""+set.getAttributeName(i);
intid=set.getAttributeNameResource(i);
if(id!=0){
s=s+"(0x"+Integer.toHexString(id)+")";
}
s=s+"="+set.getAttributeValue(i);
}
System.out.println(s);
s="Found:";
TypedValuevalue=newTypedValue();
for(i=0;i<attrs.length;i++){
intd=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);
}
returnarray;
}
这里就不做过多的源码讲解,而是把这四个参数的含义解释给大家:
AttributeSetset:属性值的集合.
int[]attrs:我们自定义属性集合在R类中生成的int型数组.这个数组中包含了自定义属性的资源ID.
intdefStyleAttr:这是当前Theme中的包含的一个指向style的引用.当我们没有给自定义View设置declare-styleable资源集合时,默认从这个集合里面查找布局文件中配置属性值.传入0表示不像该defStyleAttr中查找默认值.
intdefStyleRes:这个也是一个指向Style的资源ID,但是仅在defStyleAttr为0或者defStyleAttr不为0但Theme中没有为defStyleAttr属性赋值时起作用.
由于一个属性可以在很多地方对其进行赋值,包括:XML布局文件中、decalare-styleable、theme中等,它们之间是有优先级次序的,按照优先级从高到低排序如下:
属性赋值优先级次序表:
在布局xml中直接定义>在布局xml中通过style定义>自定义View所在的Activity的Theme中指定style引用>构造函数中defStyleRes指定的默认值
为了让大家有更清楚更直观的了解,再接下来设置自定义属性的章节中,我将对custom_attr1~4这4个属性分别在上述四个地方进行定义,然后在CustomView的构造函数中获取它的值,从而看一下,优先级顺序是否和我们预期的一样.
设置自定义属性值
以下的第几个参数均是针对Resources.Theme类的obtainStyledAttributes四参数构造方法来说明的.
第二个参数——在布局xml文件中为属性赋值
在设置自定义属性之前,我们首先要在主Activity的布局文件中调用我们的CustomView,并且为其设置特定的属性.
主布局文件内容如下:
<FrameLayoutxmlns: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-2817:19:56.54223575-23575/?E/customview:attr1=attr1_xml
05-2817:19:56.54223575-23575/?E/customview:attr2=null
05-2817:19:56.54223575-23575/?E/customview:attr3=null
05-2817:19:56.54223575-23575/?E/customview:attr4=null
注意:
在给自定义属性赋值时,首先需要增加自定义属性的命名空间,例如:xmlns:custom=”http://schemas.Android.com/apk/res-auto”,AndroidStudio推荐使用res-auto,在Eclipse中需要使用CustomView所在的包名:xmlns:cv=”http://schemas.android.com/apk/com.kevintan.eventbussample.view”
这里,在布局文件中我们为custom_attr1赋值为:attr1_xml.自定义View中获取该属性值对应了getTheme().obtainStyledAttributes方法中的第二个参数@StyleableResint[]attrs
第二个参数——在style中为属性赋值
其次,自定义属性还可以在Style中进行赋值.
首先,我们在xml布局文件中还为MyCustomView增加一个自定义的style,style代码如下:
<stylename="TestCustomView"> <itemname="custom_attr1">attr1_style</item> <itemname="custom_attr2">attr2_style</item> </style>
然后,我们修改布局文件,增加style字段:
<FrameLayoutxmlns: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-2817:19:56.54223575-23575/?E/customview:attr1=attr1_xml
05-2817:19:56.54223575-23575/?E/customview:attr2=attr2_style
05-2817:19:56.54223575-23575/?E/customview:attr3=null
05-2817:19:56.54223575-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方法中的第二个参数@StyleableResint[]attrs
第三个参数defStyleAttr
这个参数的意思是:
原文:AnattributeinthecurrentthemethatcontainsareferencetoastyleresourcethatsuppliesdefaultsvaluesfortheTypedArray.Canbe0tonotlookfordefaults.
翻译:这是当前Theme中的包含的一个指向style的引用.当我们没有给自定义View设置declare-styleable资源集合时,默认从这个集合里面查找布局文件中配置属性值.传入0表示不向该defStyleAttr中查找默认值.
为了测试该参数的作用和优先级,我们需要进行如下操作.
首先,我们先声明一个refrence格式的属性,用于表示style的引用.声明在之前的res/values/attrs_my_custom_view.xml文件里即可:
<attrname="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> <actionandroid:name="android.intent.action.MAIN"/> <categoryandroid:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity>
最后,在style.xml中的AppTheme主题下增加MyCustomViewDefStyleAttr的引用实现.
<stylename="AppTheme"parent="android:Theme.Holo.Light.DarkActionBar"> <!--Customizeyourthemehere.--> <itemname="MyCustomViewDefStyleAttr">@style/MyCustomViewDefStyleAttrImpl</item> </style> <stylename="MyCustomViewDefStyleAttrImpl"> <itemname="custom_attr1">attr1_defStyleAttr</item> <itemname="custom_attr2">attr2_defStyleAttr</item> <itemname="custom_attr3">attr3_defStyleAttr</item> </style>
代码验证,记住如果要使用obtainStyledAttributes方法的第三个参数,就需要在第三个构造函数中显示的调用getTheme()的obtainStyledAttributes方法.
publicMyCustomView(Contextcontext,AttributeSetattrs){
//为defStyleAttr进行赋值
this(context,attrs,R.attr.MyCustomViewDefStyleAttr);
}
publicMyCustomView(Contextcontext,AttributeSetattrs,intdefStyleAttr){
super(context,attrs,defStyleAttr);
TypedArrayta=context.getTheme().obtainStyledAttributes(attrs,R.styleable.MyCustomView,defStyleAttr,0);
Stringattr1=ta.getString(R.styleable.MyCustomView_custom_attr1);
Stringattr2=ta.getString(R.styleable.MyCustomView_custom_attr2);
Stringattr3=ta.getString(R.styleable.MyCustomView_custom_attr3);
Stringattr4=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-2817:19:56.54223575-23575/?E/customview:attr1=attr1_xml
05-2817:19:56.54223575-23575/?E/customview:attr2=attr2_style
05-2817:19:56.54223575-23575/?E/customview:attr3=attr3_defStyleAttr
05-2817:19:56.54223575-23575/?E/customview:attr4=null
从结果可以看出,在主题中指定style引用的优先级是低于在xml中直接赋值和使用style字段的.
同时,我们还需要了解一点:
在Android系统中的控件,很多都在构造参数中使用了第三个参数,例如Button.这样做的好处是:当我们切换不同的主题时,Button的样式也能随之进行改变.
第四个参数——通过defStyleRes为属性赋值
这个参数的意思是:
原文:AresourceidentifierofastyleresourcethatsuppliesdefaultvaluesfortheTypedArray,usedonlyifdefStyleAttris0orcannotbefoundinthetheme.Canbe0tonotlookfordefaults.
翻译:这是一个指向Style的资源ID,但是仅在defStyleAttr为0或者defStyleAttr不为0但Theme中没有为defStyleAttr属性赋值时起作用.
通过翻译,我们可以明确两点:
1.defStyleRes:指向一个style引用.
2.defStyleRes的优先级低于defStyleAttr.
为了验证,我们先在theme.xml文件中定义一个style:
<stylename="MyCustomViewDefStyleRes"> <itemname="custom_attr1">attr1_defStyleRes</item> <itemname="custom_attr2">attr2_defStyleRes</item> <itemname="custom_attr3">attr3_defStyleRes</item> <itemname="custom_attr4">attr4_defStyleRes</item> </style>
然后,我们在自定义View的第三个构造函数中的obtainStyledAttributes函数中进行赋值,具体方法如下:
publicMyCustomView(Contextcontext,AttributeSetattrs){
this(context,attrs,R.attr.MyCustomViewDefStyleAttr);
}
publicMyCustomView(Contextcontext,AttributeSetattrs,intdefStyleAttr){
super(context,attrs,defStyleAttr);
//为defStyleRes进行赋值
TypedArrayta=context.getTheme().obtainStyledAttributes(attrs,R.styleable.MyCustomView,defStyleAttr,R.style.MyCustomViewDefStyleRes);
Stringattr1=ta.getString(R.styleable.MyCustomView_custom_attr1);
Stringattr2=ta.getString(R.styleable.MyCustomView_custom_attr2);
Stringattr3=ta.getString(R.styleable.MyCustomView_custom_attr3);
Stringattr4=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-2817:44:09.2823137-3137/?E/customview:attr1=attr1_xml
05-2817:44:09.2823137-3137/?E/customview:attr2=attr2_style
05-2817:44:09.2823137-3137/?E/customview:attr3=attr3_defStyleAttr
05-2817:44:09.2823137-3137/?E/customview:attr4=null
重点:
如果大家认真的看实验结果,肯定会被上面的结果感到奇怪,明明指定了defStyleRes,为什么attr4的值还是null?
是因为之前讲过defStyleRes的使用优先级:只有当defStyleAttr为0或者当前Theme中没有给defStyleAttr属性赋值时才起作用.
所以,这里我们需要修改构造函数,将defStyleAttr设置为0.
publicMyCustomView(Contextcontext,AttributeSetattrs){
//为了验证defStyleRes的作用,将defStyleAttr设置为0
this(context,attrs,0);
}
publicMyCustomView(Contextcontext,AttributeSetattrs,intdefStyleAttr){
super(context,attrs,defStyleAttr);
TypedArrayta=context.getTheme().obtainStyledAttributes(attrs,R.styleable.MyCustomView,defStyleAttr,R.style.MyCustomViewDefStyleRes);
Stringattr1=ta.getString(R.styleable.MyCustomView_custom_attr1);
Stringattr2=ta.getString(R.styleable.MyCustomView_custom_attr2);
Stringattr3=ta.getString(R.styleable.MyCustomView_custom_attr3);
Stringattr4=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-2817:49:03.7075772-5772/?E/customview:attr1=attr1_xml
05-2817:49:03.7075772-5772/?E/customview:attr2=attr2_style
05-2817:49:03.7075772-5772/?E/customview:attr3=attr3_defStyleRes
05-2817:49:03.7075772-5772/?E/customview:attr4=attr4_defStyleRes
后记
在文章结尾,我们再次总结一下自定义属性的属性赋值优先级:
在布局xml中直接定义>在布局xml中通过style定义>自定义View所在的Activity的Theme中指定style引用>构造函数中defStyleRes指定的默认值.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。