为Android自定义部件实现自定义的XML配置属性
Custom XML Attributes For Your Custom Android Widgets
这个问题提的太好了!为了回答这个问题,下面我将先来演示如何为你的自定义部件实现自定义属性,使之可以声明在XML文件当中。我们的目标是这样的,就像下面这段,在你的layout文件中这样写: 我们已经创建了一个自定义部件MyCustomWidget
,除了标准的配置属性(android:
XXX)之外,我们还要添加几个自定义的属性:myText="Something",
fancyColors="true"
,onAction="doSomething"
。假设这些关键字我们已经可以通过编码在程序中实现定义我们的部件的属性,但是我们希望像SDK内置部件那样可以方便的放在layout文件中通过声明来设置:
<com.kdion.tutorial.MyCustomWidget android:id="@+id/theId" <!-- ...more stuff here... --> custom:myText="Something" custom:fancyColors="true" custom:onAction="myDoSomething" />
第一步:attrs.xml
为了实现我们的目标,添加自定义属性,第一步就是要让android系统知道我们的定义。如何声明我们的定义呢,你需要在项目的文件夹下的res/values/文件夹下建立一个XML文件,名为
attrs.xml
:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="MyCustomWidget"> <attr name="myText" format="string"/> <attr name="fancyColors" format="boolean"/> <attr name="onAction" format="string"/> </declare-styleable> </resources>
首先要注意,在必须的resources标签的内部的第一个标签是
declare-styleable标签,声明了属性
name="MyCustomWidget",这让
android
知道我们想要给那个部件类添加自定义属性。当然,据我所知,你只能对你的自定义部件这么干——我可不认为你可以这样写:declare-styleable name="EditText"
(我也想不出来你为什么要这么做^_^)。 其次,我们要使用attr标签来声明属性,
name定义我们要在XML文件中使用的属性名称,format用来定义属性的类型。主要有如下你中类型:string,integer,float,
dimension,boolean, float, resource, enum 和color...... 可能还有一到两种,但是我没有大量的时间来找出他们。
第二部:在你的自定义部件中添加读取配置在XML中的属性的程序逻辑
现在,我们已有方法在XML文件中向自定义部件来传递属性了,但是我们还要自己来读取它们。这样代码需要实现在传递AttributeSet参数的构造函数中,因为那(
AttributeSet
)就是定义在XML文件中的属性集合。为了读取这些属性,你首先需要从AttributeSet
创建一个TypedArray,然后从中读取配置值:
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyCustomWidget); final int N = a.getIndexCount(); for (int i = 0; i < N; ++i) { int attr = a.getIndex(i); switch (attr) { case R.styleable.MyCustomWidget_myText: String myText = a.getString(attr); //...do something with myText... break; case R.styleable.MyCustomWidget_fancyColors: boolean fancyColors = a.getBoolean(attr, false); //...do something with fancyColors... break; case R.styleable.MyCustomWidget_onAction: String onAction = a.getString(attr); //...we'll setup the callback in a bit... break; } } a.recycle();
首先调用obtainStyledAttributes,传递从构造函数的参数
中获取的AttributeSet,并且同时传递
R.styleable.MyCustomWidget
,这告诉系统只返回我们自己在attrs.xml定义的属性。接下来,我们只需要简单的循环读取每个属性,并在
switch语句中判读是那种属性,每个属性系统已经用我们配置在attrs.xml中的name属性为我们声明了索引变量,我们可以方便的使用,其形式如下:
R.styleable.ClassName_attributeName例如对于本例来说是:
R.styleable.MyCustomWidget_myText
,R.styleable.MyCustomWidget_fancyColors
, 等等。
当然,还有另外一种方法,不论是否用户进行了配置,代码中依次读取自定义的属性:
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyCustomWidget); String myText = a.getString(R.styleable.MyCustomWidget_myText); //...do something with myText... boolean fancyColors = a.getBoolean(R.styleable.MyCustomWidget_fancyColors, false); //...do something with fancyColors...
但是我喜欢使用switch语句,因为这样我们只是读取了真正声明在XML文件中的属性。当你只有一两个属性时,这不成问题,但是如果你有大量的属性,只读取用户声明的属性势必要节省处理时间。另外这也是
Google SDK中的官方做法,因此,除非我有比android实现者更好的更可靠方法,我都将采用他们的方法^_^。
当然,到目前我还没有演示如何使用定义在XML中的回调函数名称,在代码中实现回调函数的调用(callback),先别急,我们稍后就会讲到。
第三步:在XML添加自定义属性声明
最后一步是在layout文件中声明我们的自定义属性:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res/com.kdion.tutorial" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <com.kdion.tutorial.MyCustomWidget android:id="@+id/theId" android:layout_width="wrap_content" android:layout_height="wrap_content" custom:myText="Something" custom:fancyColors="true" custom:onAction="myDoSomething" /> <!-- Other stuff --> </LinearLayout>最重要的是在根元素中添加一新的命名空间属性(
xmlns,就在
xmlns:android
这个属性后面),在这儿我添加的命名空间是"http://schemas.android.com/apk/res/your.package.namespace"
。另外注意,本例中我使用的是“custom”
,其实在xmlns
:冒号后面你是可以起任意名称的,例如你可以使用“
app”,“mine”,“foobar”
等等任何名称(好吧,不是任何的,你懂的^_^),只要在定义你的自定义部件时保持一致即可。
好了,已全部搞定!你现在已经可以在layout文件中使用你的自定义属性来配置你的自定义部件了(有点儿绕@_@)。
自定义属性实现回调函数(callback)
还记得吗,一开始的问题是:如何在XML layout文件中声明自定义部件的回调函数,就像定义内置部件的点击事件android:onClick="myMethod"
这样来定义自定义部件的回调函数?在上面我们看到,在第一步中我们只是简单的把我们的回调函数的属性类型定义为字符串类型(接受我们将要调用的函数的名称),可是,我们读取该属性之后,接下来应该怎么做呢?开始的时候,我完全没有头绪——但是幸运的是Android已经解决了,并且她是开源的,所以这就变成一件小事,我们只需追踪android的源代码,看看她是如何处理onClick的设置的,然后,我们就可以照猫画虎来解决我们的问题。
我最终在android.view.View
找到相关的代码,位于构造函数中读取传入的配置属性,处理点击事件R.styleable.View_onClick
这一段中。对于API 9版(2.3),你可以在这里查看代码。我不会在这里重复整个代码,应为相关case段代码太长太复杂了。但是,我会尽力截取核心的代码放在这里以便我可以解读,你也可以跟着理解它(with
the actual source is best,这句怎么翻?)。
case R.styleable.View_onClick: if (context.isRestricted()) { throw IllegalStateException(); } final String handlerName = a.getString(attr); if (handlerName != null) { setOnClickListener(new OnClickListener() { private Method mHandler; public void onClick(View v) { if (mHandler == null) { try { mHandler = getContext().getClass().getMethod(handlerName, View.class); } catch (NoSuchMethodException e) { throw IllegalStateException(); } } try { mHandler.invoke(getContext(), View.this); } catch (IllegalAccessException e) { throw IllegalStateException(); } catch (InvocationTargetException e) { throw IllegalStateException(); } } }); } break;我们看到,首先代码检查了参数
context(是构造方法的参数)是否是
restricted,如果是则抛出例外。否则,以一个实现了接口
OnClickListener的匿名方法
为参数来
调用setOnClickListener进行事件处理方法的注册。用户在XML中设置的回调方法是在匿名方法中进行调用的。
显然,你可以轻易的替换成你自己的方法和类。译者:佣工7001
Context
所属的类(通常就是我们的
Activity
)中获取方法时(
getMethod
),方法的名称就是在XML配置文件中所设置的回调函数的名称。另外,之所以还要传递参数View.class给getMethod
,是因为onClick方法有一个
View类型的参数。
因此如果你的回调函数没有参数,那么你在调用getMethod
时就不必给出第二个参数。
最后,以View的当前实例为参数来调用该方法。 上面的代码实际上有点儿小问题,因为它在例外处理时忽略了很多信息 ,在你自己的代码中是不会想这样做的。但是不论如何,这就是你如何实现在自定义的XML属性中为自定义部件实现设置回调函数的方法!