http://www.eoeandroid.com/thread-92244-1-1.html
研究Android控件DIY已经有好几周了,平时也懒得记录一些心得体会之类的。今天百无聊赖,突然想起有很多同行对这个话题比较感兴趣,遂整理一下思路,把自制控件的方法写下来。由于小可没有深究Android源代码,以下方法纯属多种尝试之后的结果,所以有某几个问题还不明原因,如有高手刚好路过,敬请赐教!
在嵌入式上开发图形界面已接近两年,个人感觉一个好的平台应该可以允许编程者根据个人的需求对所提供的控件进行再加工或者通过更加底层的方法“画”出自己的控件。这就如同盖房一样,我们拿到手的应该是砖头、水泥,根据需要按照自己或者他人的喜好把砖头摆出各种各样的形状,从而形成独具风格的建筑物。而那些卫生间、厨房、卧室的风格都已经确定,你只能够用这些成品来拼凑的建筑是相当地枯燥乏味的。编写GUI也一样,像Tilcon那样,只能使用仅有的几十种控件的平台,是远远不能满足人的要求的。
到目前为止,可能有很多人还只是停留在使用Android系统提供的控件,拼命地查找API手册,以便能够找到自己需要的功能,或者使用繁杂地布局与众多的控件,经过复杂地逻辑终于实现了某种效果,但是当移植到另一个Activity下的时候却突然发现,移植的难度丝毫不亚于实现!这就是自定义控件产生的根本原因,如同Android SDK自带的控件一样,做好的自制控件可以在xml的图形视图中拖拉添加,只需在xml里设置好参数,也可以在调用类里通过接口或者方法来设置其属性。需要移植的时候,只需将此控件的类文件亦或其所包含的xml等文件添加到你的工程中即可。如果需要修改控件,只需要编辑控件的类文件,与工程中的其他文件基本没有关系,这正体现了java这种面向对象的编程语言的“封装”的精神。在我看来,这是一个很好的习惯,它能让你的工程结构更加清晰,非常利于代码的维护。
在介绍方法之前,先看一个简单的效果图。
图中黑线框的部分就是自定义控件,这表明此控件已经可以拖拉使用了。注意,点了上述标签之后,一定要用鼠标晃一晃黑线位置才能出现,诡异的很!把控件拖到xml中,效果如下图:
package com.Runner;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
public class Runner extends LinearLayout{
TextView mText;
public Runner(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
mText = new TextView(context, attrs);
mText.setText("Hello, World!");
this.addView(mText);
Button mButton = new Button(context, attrs);
mButton.setText("按我改变文字");
mButton.setBackgroundResource(android.R.drawable.btn_default);
mButton.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
mText.setText("兄台似乎被按了?");
}
});
this.addView(mButton);
}
}
接下来回到main.xml的视图模式中,可以看到控件发生了变化!
下面为控件添加背景。添加背景有两种方法,一是在构造函数里根据图片的drawable id直接添加,另一种方法是根据xml描述文件,从main.xml的属性中获得。这两种方法,我推荐用第二种,因为第一种是在代码中添加,只要代码写好了,其他使用者想更改背景,还需要查看源代码并修改源代码,这种做法太霸道,不灵活。因此这里着重介绍第二种。
为了达到上述目的,需要新增一个属性描述文件:attrs.xml。注意,此文件名不能更改,Android只能在此文件中查找属性的格式。在工程目录的res/values下,也就是string.xml的同级目录下新增文件attrs.xml,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="Runner">
<attr name="background" format="reference" />
</declare-styleable>
</resources>
标签declare-styleable在语法合法的情况下,会在工程的R文件中生成一个属性分类,名称就是标签后面的name的值,在本工程中是"Runner"。它包裹起来的属性,就是我们能够在main.xml中设置的属性,用法和android:text="Hello"相同。其中text就对应我们这里的"background","Hello"的格式对应我们这里的format的值,比如Hello是字符串,那我们这里就应该写作:format="string",如果是整形,对应format="integer",字符型:format="char"等等。我们要传递一个drawable的id,则格式为引用"reference"。
现在格式已经具备了,接下来回到main.xml中,为控件设定属性。原始的文件中,有一行定义了xml的命名空间:<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android",这一行在几乎所有的系统自动生成的布局文件中都可以看到,xmlns表明了这一段是用来设定xml的命名空间(xml name space)的,后续的android:字段,就是使用了上面的命名空间,表示后面的属性是系统自带的,要到android所代表的命名空间中寻找相应的格式之类的,如果很难理解,可以将其看作是一个变量,其中存储的是目录路径,和我们的包路径很相似。同样,我们要使用自己定义的属性,也需要一个命名空间,让编译器可以到我们的包路径下寻找对应的文件和格式等,在上一行命名空间下面紧接着定义我们自己的命名空间如下:
xmlns:Runner="http://schemas.android.com/apk/res/com.Runner"
注意,这里不能乱写:xmlns:Runner="http://schemas.android.com/apk/res/ 这一段是固定的,后面的是我们工程的包路径,既不是工程名,也不是我们控件的解释类所位于的包路径,必须是工程的包路径,也就是主类的package字段的值!原因是main.xml要首先检查属性的格式,就是上面提到的format的值,而这个值是产生在工程的R类里,而R类是位于工程的最顶端的!使用逻辑倒推就一目了然了。
接下来定义属性,方式和普通控件类似,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:Runner="http://schemas.android.com/apk/res/com.Runner"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView android:layout_width="fill_parent"
android:layout_height="wrap_content" android:text="@string/hello" />
<com.Runner.Runner android:id="@+id/runner1"
android:layout_width="wrap_content" android:layout_height="wrap_content"
Runner:background="@drawable/bg">
</com.Runner.Runner>
</LinearLayout>
我把图片放在了drawable里,名字叫做bg.png。
再接下来就是最后一步,到解释类里读取上面定义的背景资源,并设置给自己,作为背景图。
细心的人会发现,当我们添加了attrs.xml并在其中添加了合法的styleable之后,编译器会在工程的R类里生成一个名为styleable的子类,里面存放的就是我们定义的属性。因此,我们需要从这个styleable里读取需要的数据。代码如下:
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Runner);
通过调用此方法,我们获得了Runner下的所有属性,因此还需要从中挑选出对我们有用的某一条属性,再删选一遍:
Drawable bg = a.getDrawable(R.styleable.Runner_background);
这样我们就从Runner的属性序列里找到了所需要的背景资源,由于上面定义的格式是引用,所以这里直接调用getDrawable的方法,获得资源。同理,如果我们定义的是integer整形,那么这里需要调用getInt或者getInteger方法。
已经获得了Drawable,就如同馒头已经拿在手上,神马都不用说了,直接往嘴里塞吧:
this.setBackgroundDrawable(bg);
OK!背景已经添加了,为了使编程不至于蛋疼,我选了一个美女给大家丰富一下精神生活,如下图: