构建自定义组件
Android中,你的应用程序程序与View类组件有着一种固定的联系,例如按钮(Button)、文本框(TextView),可编辑文本框(EditText),列表框(ListView),复选框(CheckBox),单选框(RadioButton),滚动条(Gallery),微调器(Spinner), 等等,还有一些比较先进的有着特殊用途的View组件,例如AutoCompleteTextView, ImageSwitcher和 TextSwitcher。除此之外,种类繁多的像 线性布局(LinearLayout), 框架布局(FrameLayout), 这样的布局组件(Layout)也被认为是View组件,他们是从View类派生过来的。
你的应用程序就是这些控制组件和布局组件以某种方式结合显示在屏幕上,一般来说这些组件对你来说基本够用,但是你也应该知道你是可以通过类继承创建属于自己的组件,一般可以继承像View、Layouts(布局组件)这样的组件,甚至可以是一些比较高级的控制类组件。下面我们说一下为什么要继承:
· 你可以为实现某种功能创建一个完全自定义风格的组件,例如用二维的图形创建控制组件实现声音的控制,就像电子控制一样。
· 你可以把几种组件结合形成一个新的组件,你的组件可能同时包含ComboBox(一个能输入的文本列表)和dual-pane selector control(左右两个List窗口,你可以分配窗口每一项的从属关系)等等。
· 你可以创建自己的布局组件(Layout)。SDK中的布局组件已经提供了一系列的选项让你打造属于自己的应用程序,但是高级的开发人员会发现根据现有的Layout组件开发新的Layout组件是很有必要的,甚至是完全从底层开发新的组件。
· 你可以覆盖一个现有组件的显示或功能。例如,改变EditText(可编辑文本)组件在屏幕上的显示方式(可以参考Notepad的例子,里面教你如何创建一个下划线的显示页面)。
· 你可以捕获像按键按下这样的事件,以一些通用的方法来处理这些事件(一个游戏的例子)。
为了实现某种目标你可能很有必要扩展一个已经存在的View组件,下面我们结合一些例子教你如何去做。
内容:
完全自定义组件(Fully Customized Components )
定制组件的例子(Customized Component Example )
组件的混合(或者控制类的混合) (Compound Components (or Compound Controls) )
修改现有组件(Tweaking an Existing Component )
小结(Go Forth and Componentize )
下面的一些步骤都比较概括,教你如何创建自己的组件:
1. 让你的类(Class)继承一个现有的View 类或View的子类。
2. 重载父类的一些方法:需要重载的父类方法一般以‘on’开头,如onDraw(),onMeasure()和 onKeyDown()等等。
o 这个在Activity 或则ListActivity 派生中同样适用,你需要重载一些生命周期函数和一些其他功能性的HOOK函数。
3. 使用你的继承类:一旦你的继承类创建完成,你可以在基类能够使用的地方使用你的继承类,但完成功能就是你自己编写的了。
继承类能够定义在activities里面,这样你能够方便的调用,但是这并不是必要的(或许在你的应用程序中你希望创建一个所有人都可以使用的组件)。
完全自定义组件的方法可以创建一些用于显示的图形组件(graphical components),也许是一个像电压表的图形计量器,或者想卡拉OK里面显示歌词的小球随着音乐滚动。无论那种方式,你也不能单纯的利用组件的结合完成,无论你怎么结合这些现有的组件。
幸运的是,你可以以你自己的要求轻松地创建完全属于自己的组件,你会发现不够用的只是你的想象力、屏幕的尺寸和处理器的性能(记住你的应用程序最后只会在那些性能低于桌面电脑的平台上面运行)。
下面简单介绍如何打造完全自定义的组件:
1. 最为通用的VIEW类的父类毫无疑问是View类,因此,最开始你要创建一个基于此类的一个子类。
2. 你可以写一个构造函数从XML文件中提取属性和参数,当然你也可以自己定义这些属性和参数(也许是图形计量器的颜色和尺寸,或者是指针的宽度和幅度等等)
3. 你可能有必要写自己的事件监听器,属性的访问和修改函数和一些组件本身的功能上的代码。
4. 如果你希望组件能够显示什么东西,你很有可能会重载 onMeasure() 函数,因而你就不得不重载 onDraw() 函数。当两个函数都用默认的,那么 onDraw()函数将不会做任何事情,并且默认的 onMeasure() 函数自动的设置了一个100x100 —的尺寸,这个尺寸可能并不是你想要的。
5. 其他有必要重载的on... 系列函数都需要重新写一次。
onDraw()和onMeasure()
onDraw()函数将会传给你一个 Canvas 对象,通过它你可以在二维图形上做任何事情,包括其他的一些标准和通用的组件、文本的格式,任何你可以想到的东西都可以通过它实现。
注意: 这里不包括三维图形如果你想使用三维的图形,你应该把你的父类由View改为SurfaceView类,并且用一个单独的线程。可以参考GLSurfaceViewActivity 的例子。
onMeasure() 函数有点棘手,因为这个函数是体现组件和容器交互的关键部分,onMeasure()应该重载,让它能够有效而准确的表现它所包含部分的测量值。这就有点复杂了,因为我们不但要考虑父类的限制(通过onMeasure()传过来的),同时我们应该知道一旦测量宽度和高度出来后,就要立即调用setMeasuredDimension() 方法。
概括的来讲,执行onMeasure()函数分为一下几个阶段:
1. 重载的onMeasure()方法会被调用,高度和宽度参数同时也会涉及到(widthMeasureSpec 和heighMeasureSpec两个参数都是整数类型),同时你应该考虑你产品的尺寸限制。这里详细的内容可以参考View.onMeasure(int, int) (这个连接内容详细的解释了整个measurement操作)。
2. 你的组件要通过onMeasure()计算得到必要的measurement长度和宽度从而来显示你的组件,它应该与规格保持一致,尽管它可以实现一些规格以外的功能(在这个例子里,父类能够选择做什么,包括剪切、滑动、提交异常或者用不同的参数又一次调用onMeasure()函数)。
3. 一旦高度和宽度计算出来之后,必须调用setMeasuredDimension(int width, int height),否则就会导致异常。
一个自定义组件的例子(A Customized Component Example)
在 API Demos 中的CustomView提供了以一个自定义组件的例子,这个自定义组件在 LabelView 类中定义。
LabelView例子涉及到了自定义组件的方方面面:
· 首先让自定义组件从View类中派生出来。
· 编写带参数的构造函数(参数可以来源于XML文件)。这里面的一些处理都已经在View父类中完成,但是任然有些Labelview使用的自定义组件特有的新的参数需要处理。
· 一些标准的Public函数,例如setText(), setTextSize(), setTextColor()
· 重载onMeasure()方法来确定组件的尺寸(注意:在LabelView中是通过一个私有函数measureWidth()来实现的)
· 重载onDraw()函数把Lable显示在提供的canvas上。
在例子中,你可以通过custom_view_1.xml看到自定义组件LabelView的用法。在XML文件中特别要注意的是android:和app:两个参数的混合运用,app:参数表示应用程序中被认为是LabelView组件的个体,这些也会作为资源在R类中定义。
如果你不想创建一个完全自定义的组件,而是由几个现有组件的组合产生的新的组件,那么混合组件技术就更加适合。简单的来说,这样把几个现有的组件融合到一个逻辑组合里面可以封装成一个新的组件。例如,一个Combo Box组件可以看作是是一个EditText和一个带有弹出列表的Button组件的混合体。如果你点击按钮为列表选择一项,
在Android中,其实还有其他的两个View类可以做到类似的效果: Spinner和AutoCompleteTextView,,但是Combo Box作为一个例子更容易让人理解。
下面简单的介绍如何创建组合组件:
1. 一般从Layout类开始,创建一个Layout类的派生类。也许在Combo box我们会选择水平方向的LinearLayout作为父类。记住,其他的Layout类是可以嵌套到里面的,因此混合组件可以是任何组件的混合。注意,正如Activity一样,你既可以使用外部XML文件来声明你的组件,也可以嵌套在代码中。
2. 在新的混合组件的构造函数中,首先,调用所有的父类的构造函数,传入对应的参数。然后可以设置你的混合组件的其他的一些方面,在哪创建EditText组件,又在哪创建PopupList组件。注意:你同时也可以在XML文件中引入一些自己的属性和参数,这些属性和参数也可以被你的混合组件所使用。
3. 你也可以创建时间监听器去监听新组件中View类触发的事件,例如,对List选项单击事件的监听,你必须在此时间发生后更新你EditText的值。
4. 你可能创建自己的一些属性,带有访问和修改方法。例如,允许设置EditText初始值并且提供访问它的方法。
5. 在Layout的派生类中,你没有必要去重载onDraw()和onMeasure()方法,因为Layout会有比较好的默认处理。但是,如果你觉得有必要你也可以重载它。
6. 你也可能重载一些on系列函数,例如通过onKeyDown()的重载,你可以通过按某个键去选择列表中的对应的值。
总之,把Layout类作为基类有下面几个优点:
· 正如activity一样,你也可以通过XML文件去声明你的新组件,或者你也可以在代码中嵌套。
· onDraw()函数和onMeasure()函数是没有必要重载的,两个函数已经做得很好了。
· 你可以很快的创建你的混合组件,并且可以像单一组件那样使用。
混合组件的例子(Examples of Compound Controls)
In theAPI Demos project 在API Demos工程中,有两个List类的例子——Example 4和Example6,里面的SpeechView组件是从LinearLayout类派生过来,实现显示演讲显示功能,对应的原代码是List4.java和List6.java。
在某些情况下,你可能有更简单的方法去创建你的组件。如果你应经有了一个非常类似的组件,你所要做的只是简单的从这个组件派生出你的组件,重在其中一些有必要修改的方法。通过完全自定义组件的方法你也可以同样的实现,但通过冲View派生产生新的组件,你可以简单获取一些已经存在的处理机制,这些很可能是你所想要的,而没有必要从头开始。
例如,在SDK中有一个NotePad的例子(NotePad application )。该例子演示了很多Android平台实用的细节,例如你会学到从EditView派生出能够自动换行的记事本。这还不是一个完美的例子,因为相比早期的版本来说,这些API已经感变了很多,但它确实说明了一些问题。
如果你还未查看该程序,现在你就可以在Eclipse中导入记事本例程(或仅通过提供的链接查看相应的源代码)。特别是查看NoteEditor.java 中的MyEditText的定义。
下面有几点要注意的地方:
1. 声明(The Definition)
这个类是通过下面一行代码来定义的:
publicstatic class MyEditText extends EditText
o 它是定义在NoteEditor activity类里面的,但是它是共有的(public),因此如果有必要,它可以通过NoteEditor.MyEditText从NoteEditor外面来调用。
o 它是static类(静态类),意味着不会出现所谓的通过父类访问数据的“虚态方法”, 这样就使该类成为一个可以不严重依赖NoteEditor的单独类。对于不需要从外部类访问的内联类的创建,这是一个很清晰地思路,保证所产生的类很小,并且允许它可以被其他的类方便的调用。
o 它是EditText类的扩展,它是我们选择的用来自定义的父类。当我们完成以后,新的类就可以作为一个普通的EditText来使用。
2. 类的初始化
一般来说,父类是首先调用的。进一步来说,这不是一个默认的构造函数,而是一个带参数的构造函数。因为EditText是使用从XML布局文件提取出来的参数进行创建,因此我们的构造函数也要取出参数并且将这些参数传递给父类。
3. 方法重载
在本例中,仅对onDraw()一个方法进行重载。但你可以很容易地为你的定制组件重载其他需要的方法。
对于记事本例子来说,通过重载onDraw()方法我们可以在EidtView的画布(canvas)上绘制蓝色的线条(canvas类是通过重写的onDraw()方法传递)。该函数快要结束时要调用super.onDraw()函数。父类的方法是应该调用,但是在这个例子里面,我们是在我们划好了蓝线之后调用的。
4. 使用定制组件
现在,我们已经有自己定制的组件了,但是应该怎样使用它呢?在记事本例子中,定制的组件直接在预定义的布局文件中使用,让我们看一看res/layout目录中的note_editor.xml文件。
<view xmlns:android="http://schemas.android.com/apk/res/android"
class="com.android.notepad.NoteEditor$MyEditText"
id="@+id/note"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@android:drawable/empty"
android:padding="10dip"
android:scrollbars="vertical"
android:fadingEdge="vertical" />
o 该自定义组件在XML中是作为一个一般的View类来创建的,并且是通过全路径包来描述的。注意这里内联类是通过NoteEditor$MyEditText来表示的,这是Java编程中引用内联类的标准方法。
o 在定义中的其他属性和参数将传递给定制组件的构造函数,然后才传到EditText构造函数中,因此这些参数也是你使用EditText组件的参数。注意,这里你也可以增加你自己的参数,我们将在下面讨论这个问题。
这就是你全部需要做的,诚然这是一个简单的例子。但问题的关键是:你的需求有多复杂,那么你的自定义组件就有多么复杂。
一个更为复杂的组件可能需要重载更多的on系列函数,并且还要很多特有的函数来充分实现自定义组件的功能。唯一的限制就是你的想象力和你需要组件去执行什么工作。
如你所见,Android提供了一种精巧而又强大的组件模型,让你尽可能的完成你的工作。从简单的组件调整到组件混合,甚至完全自定义组件,灵活的运用这些技术,你应该可以得到一个完全符合你外观要求的的Android程序