http://www.eoeandroid.com/thread-92244-1-1.html
研究Android控件DIY已经有好几周了,平时也懒得记录一些心得体会之类的。今天百无聊赖,突然想起有很多同行对这个话题比较感兴趣,遂整理一下思路,把自制控件的方法写下来。由于小可没有深究Android源代码,以下方法纯属多种尝试之后的结果,所以有某几个问题还不明原因,如有高手刚好路过,敬请赐教!
在嵌入式上开发图形界面已接近两年,个人感觉一个好的平台应该可以允许编程者根据个人的需求对所提供的控件进行再加工或者通过更加底层的方法“画”出自己的控件。这就如同盖房一样,我们拿到手的应该是砖头、水泥,根据需要按照自己或者他人的喜好把砖头摆出各种各样的形状,从而形成独具风格的建筑物。而那些卫生间、厨房、卧室的风格都已经确定,你只能够用这些成品来拼凑的建筑是相当地枯燥乏味的。编写GUI也一样,像Tilcon那样,只能使用仅有的几十种控件的平台,是远远不能满足人的要求的。
到目前为止,可能有很多人还只是停留在使用Android系统提供的控件,拼命地查找API手册,以便能够找到自己需要的功能,或者使用繁杂地布局与众多的控件,经过复杂地逻辑终于实现了某种效果,但是当移植到另一个Activity下的时候却突然发现,移植的难度丝毫不亚于实现!这就是自定义控件产生的根本原因,如同Android SDK自带的控件一样,做好的自制控件可以在xml的图形视图中拖拉添加,只需在xml里设置好参数,也可以在调用类里通过接口或者方法来设置其属性。需要移植的时候,只需将此控件的类文件亦或其所包含的xml等文件添加到你的工程中即可。如果需要修改控件,只需要编辑控件的类文件,与工程中的其他文件基本没有关系,这正体现了java这种面向对象的编程语言的“封装”的精神。在我看来,这是一个很好的习惯,它能让你的工程结构更加清晰,非常利于代码的维护。
在介绍方法之前,先看一个简单的效果图。
图中有2个自定义控件,一个是竖条与玻璃半透明版的组合,这是一个可以滑动的板,点击竖条实现板内容的动画效果开合,并且里面的内容可以随意添加Activity,当然也就支持Activity的xml了。这样做的好处是板中的内容可以随意更改,并且代码维护时,对于里面的更改,丝毫不影响外部。第二个自定义控件是有数字的滚轮(素材使用了Iphone的,又用ps二次加工,并非用于商业目的,请apple不要追究我的法律责任),这个控件可以带着一行行的文本上下滚动,并且始终令可视区域的3行居中。在滚轮的滚动中,你可以选择滚动是否具有惯性的反弹效果,也可以更改文字。甚至可以修改源代码,使滚动的内容不是文字,而是图片。就是这么随心所欲!
一、自定义控件的方法
我摸索出来的自定义控件的方法有两类——利用既有控件封装和使用画布自制。所谓用控件封装,就是“累积木”式的,这种方法简单易行,很易上手,效果基本上可以满足大部分需要。控件基本上是继承自某种Layout,在其中添加自己需要的控件,最终形成独具特色的控件。使用画布自己绘制是一种比较彻底的方式,如同以前GUI编程中经常用到的点画线、线画面、贴图、调整位置等,只是Android提供了动画Animation的支持,使得这些点、线、面、图变得生动了起来。但是这种方法的编程量及需要的知识也较前一种有了明显的增加。
二、自定义控件需要的文件
自定义控件最核心的文件就是一个解释类,它是整个控件的灵魂。它负责初始化、控制你的控件的状态。同时在eclipse中,布局的xml文件能够以视图的形式展现在编程人员面前,这个解释类是功不可没的,如果解释类没有做好,在布局的视图界面下将只能看到错误提示。
另一个文件是属性格式文件(attrs.xml),位于工程的res/values/下,这个文件是用户自己添加的,具有自己的格式,形如以下代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SlidingBoard">
<attr name="handle_SlidingBoard" format="reference" />
<attr name="handleBg_opened_SlidingBoard" format="reference" />
<attr name="handleBg_closed_SlidingBoard" format="reference" />
<attr name="handle_BoardWkState_Indicator" format="reference" />
<attr name="handle_BoardWorkState_Disabled" format="reference" />
<attr name="handle_BoardWorkState_Enabled" format="reference" />
<attr name="content_SlidingBoard" format="reference" />
</declare-styleable>
</resources>
declare-styleable标签后的名称是控件名称,attr标签后的名称是属性的名称,format是指此属性在传递值的时候是按照那种格式解析的,例如char(char)型、int(integer)型或者是引用(reference)。随后编译器会将某个属性的前面加一个控件名称作为前缀,形成的完整控件名如:
SlidingBoard_ handle_SlidingBoard。此文件的用法会在后面详细介绍。
如果有需要,可以为此控件做一个布局文件,如同main.xml一样,在里面放好控件,用解释类把它读进来,再作为一个view添加到自身。
上文提到的3个文件是最常见的,有的可能还需要Animatin、图片描述文件等,不一而足。但是我的观点是,尽量减少控件所使用的文件!如果使用的文件多,则降低了其独立性和灵活性,使用的时候,必须把将图片、资源描述文件、属性、布局等一大堆文件都放到工程中,有时还可能增加控件耦合性或者命名冲突之类的,这样的做法不可取。最好的效果就是像Android SDK提供的控件那样,神马都不用,只需要那一个文件!要达到这个目的不是不可以,只需要多写几行代码即可。
三、自定义控件的步骤
这里谈的步骤有点笼统,因为每个人实现的控件和实现的方法都不一样,所以没有统一的步骤可言。这里只谈最基本的步骤,有了最基本的,其他的都是凤毛麟角了。
1.解释类
首先创建解释类,为了简单起见,创建的位置就选择src的主类文件同级包下即可。这里假定包名是com.Runner_Widget,那么这个包下面应该有Runner_Widget.java和Runner.java,第二个就是新创建的java类文件,这就是我们的解释类。
解释类的类名就是你控件的名称,也是文件的名称,一旦确定,就尽量不要改,以免带来不必要的麻烦。解释类继承的父类根据上文提到的两种方法有所差异。如果使用“累积木”式的,那么大部分情况下是继承自某种Layout,如LinearLayout(线性图层)、FrameLayout(帧图层)、RelativeLayout(相对图层)等,因为你的控件在绝大多数情况下,是要容纳其他控件的,也就是说你的控件从某种意义上讲应该是一个容器。而自己“画”的控件,应该是继承自View或者ViewGroup,在其中使用画布(Canvas)和画笔(Paint)画。为了简单起见,本文只讲“累积木”式的方法,对另一种方法有兴趣的人可以查阅画布相关资料。
令Runner类继承自LinearLayout,并定义其构造函数Runner,构造函数有带1个、2个、3个实参的3个重载,第一个不能选,因为后续需要至少2个参数,而且eclipse也会提示错误。选择具有两个实参的就可以了,代码如下:
public Class Runner extends LinearLayout{
public Runner(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
这是最初的形式,到这里已经成了一个控件,但只是一个摆设,什么作用都没有,因此接下来要做的就是在其中添加内容,使其成为一个真正的控件,一个有表现、有动作,一个有灵魂,一个活生生的控件。
接着,我们在这个控件中添加一个文本,这样这个控件就可以被看到了。在构造函数里添加如下代码:
TextView mText = new TextView(context, attrs);
mText.setText("Hello, World!");
this.addView(mText);
这时到main.xml的视图模式下找到左边的“自定义及库视图(Custom & Library Views)”,点击等待几秒或者用鼠标晃一晃,就可以发现自己定义的Runner控件了,如下图所示。
![01.bmp 01.bmp](http://www.eoeandroid.com/data/attachment/forum/201107/29/231400za41lzp3q0kq3sq0.bmp)
图中黑线框的部分就是自定义控件,这表明此控件已经可以拖拉使用了。注意,点了上述标签之后,一定要用鼠标晃一晃黑线位置才能出现,诡异的很!把控件拖到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的视图模式中,可以看到控件发生了变化!
由于我们的Runner默认是水平视图,可以通过LayoutParams来改为垂直视图,代码如下:
//更改视图为垂直
this.setOrientation(LinearLayout.VERTICAL);
将其添加到上述构造函数的一开始,即super(context, attrs);后,否则可能会影响到其后控件的大小,具体原因请看17楼的提问和19楼的探讨。
现在可以运行一下,看一下效果了!
![04.bmp 04.bmp](http://www.eoeandroid.com/data/attachment/forum/201107/29/231805a8d7kyz18zctoc8c.bmp)
下面为控件添加背景。添加背景有两种方法,一是在构造函数里根据图片的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!背景已经添加了,为了使编程不至于蛋疼,我选了一个美女给大家丰富一下精神生活,如下图:
四、结语
这是一个简单且蛋疼的例子,但这个例子最大的作用绝不是让读者蛋疼,而是起到抛砖引玉的作用,掌握了这些简单的步骤,复杂的实现也只是灵感和汗水了,就如同家庭主妇和酒店大厨的区别,只要会做饭了,要成为大厨,就属于另一个范畴了。
第一张图的滚轮控件,等下次发帖的时候再发吧,今天太晚了,还要去练琴。有兴趣的就先琢磨一下它的实现吧,嘿嘿。