创建自定义组件Building Custom Components
Android提供了一个精致而强大的组件化模式来创建你的用户界面,基于基础的布局类:视图 View和视图组ViewGroup。平台包含了多种预定义视图和视图组子类-分别称为部件和布局-这些可以用来构造你的用户界面。
一部分可用部件包括按钮Button,文本视图TextView,编辑文本框EditText,列表视图ListView,组合框CheckBox, 单选按钮RadioButton, 画廊Gallery, 微调器Spinner, 以及一些用于特定场合的自动补全文本视图AutoCompleteTextView, 图片切换器ImageSwitcher, 和文本切换器TextSwitcher.
可用布局有线性布局LinearLayout, 框架布局FrameLayout, 相对布局RelativeLayout, 以及其他。更多例子,参见常用布局对象Common Layout Objects.
如果这些预定义的部件或布局不能满足你的需求,你可以创建你自己的视图类。如果你只需要在现有的部件或布局上做些调整,你只需要子类化这个部件或布局并重写它的方法。
创建自己的视图子类让你可以精确控制界面元素的外形和功能。为了对这种控制有个大概的印象,下面是一些例子说明你可以用它们做什么:
· 你可以创建一个完全自定义绘制的视图类型,比如一个用2D图形绘制的“音量控制”旋钮,使它看上去像一个模拟电子件。
· 你可以把一组视图组件组合进一个单独的组件里,也许像一个组合框(是弹出列表和自由输入的文本段的组合),一个双窗格选择器控件(一个左窗格和右窗格,里面各有一个列表,你可以选择哪个项应该在哪个列表中),如此等等。
· 你可以重写一个编辑文本EditText组件的绘制方式(记事本指南Notepad Tutorial中使用这个方法创建了一个条纹状的记事本页面效果)。
· 你可以捕获其他事件如按键然后以自定义的方式处理。(比如一个游戏所做的那样)
下面的章节解释了怎么去创建自定义视图和在你的应用程序中使用它们。详细的参考信息,请查看视图View 类。
基本方法The Basic Approach
下面是关于怎么样开始创建自定义视图组件的一个概要性的总体描述:
1. 扩展一个现有的视图类然后子类化它。
2. 重写父类中的一些方法。这些方法以“on”开始,比如onDraw(), onMeasure(),和 onKeyDown()。这和活动或列表活动中为生命周期和其他功能钩子重写on…事件类似。
3. 使用你的新扩展类。这些完成后,你的扩展类就可以替代那个基础视图了。
提示: 扩展类可以被定义成使用它们的活动的内部类。这样活动可以控制对它们的访问,这很有用但并非必须如此(也许你想在应用程序里创建一个使用更广的公共视图)。
完全自定义组件Fully Customized Components
完全自定义组件可以用来创建你想要的图形组件。也许是一个图形VU表看起来像一个老式的模拟计量器,或者是一个伴唱字幕,上面有一个弹球随着文字移动这样你就可以在卡拉OK机上跟唱。无论哪种方式,你想要的东西都是内置组件所不能完成的,不管你怎么组合它们。
幸运的是,你可以简单的按照你的意愿来创建组件,除非你想不到,或者受限于屏幕尺寸,以及可用电源(记住,最终你的应用程序得运行在比桌面工作站电源少得多的设备上)。
要创建一个完全自定义组件:
1. 毫无意外,你可以扩展的最通用的视图是View, 因此你通常从扩展它开始创建你的超级组件。
2. 你可以供应一个构造器从XML中读取属性和参数,你也可以消费你自己的属性和参数(也许是VU表的颜色和范围,或者指针的宽度和阻尼,等)
3. 你可能也想在你的组件中创建自己的事件侦听器,属性访问和修改器,以及其它可能更复杂的行为。
4. 你将几乎肯定要重写onMeasure() 而且也很可能需要重写onDraw()。如果你希望这个组件显示一些东西。两者都有缺省行为,缺省的onDraw()方法不做任何事情,而缺省onMeasure()方法将总是设置一个100x100的尺寸-这或许不是你想要的。
5. 其他on...方法也可以按照要求重写。
扩展onDraw()和onMeasure() Extend onDraw() and onMeasure()
onDraw()方法传给你一个画布Canvas对象,你可以在上面实现任何你想要的东西:2D图形,其它基础或自定义组件,风格文本,或其他任何你能想到的。
注意: 这不适用于3D图形。如果你想使用3D图形,你必须扩展SurfaceView而不是View,并且从一个单独的线程中绘制。参见GLSurfaceViewActivity示例以了解更多细节。.
onMeasure()会被用得更多一点。onMeasure()是你的组件和它的容器之间绘制约定的关键部分。onMeasure()应该被重写来有效和准确的报告它所包含部分的尺寸。但是由于父类的一些限制性要求(被传递给onMeasure()方法)和以宽度和高度(一旦已经被计算出来)调用setMeasuredDimension()方法的要求,这将变得稍微复杂一些。如果你从一个重写的onMeasure()方法中调用这个方法失败,结果将返回一个测量时异常。
概要而言,实现onMeasure()看起来如下:
1. 给定宽度和高度测量规格(widthMeasureSpec 和heightMeasureSpec,两者都是代表维度的整数编码)来调用重写的onMeasure方法,这应该被当作度量上的限制性要求。完整的参考在View.onMeasure(int, int)文档中,这篇参考文档还很好的描述了整个测量操作)。
2. 你的组件的onMeasure()方法应该计算一个宽度和高度用来绘制这个组件。它应该尽可能留在传递的规格所指定的范围里,尽管它可以选择超出它们(这样的话,父类可以选择处理方式,包括裁剪,滚动,抛出异常,或者要求onMeasure()再试一次,也许会使用不同的度量规格。)
3. 一旦宽度和高度被计算出来,这个setMeasuredDimension(int width, int height)方法必须以计算出来的度量来调用。如果调用不成功,则将抛出异常。
下面是框架在视图上调用的一些其他基本方法的汇总:
类别Category | 方法Methods | 描述Description |
创建 | 构造器 | 有一种构造器形式是在从代码里创建视图时被调用,另一种是从一个布局文件中扩充视图时被调用。第二种形式应该解析并运用任何定义在布局文件中的属性。 |
当一个视图及其所有子项已经在XML中扩充好时被调用。 | ||
布局 | 用来决定这个视图及其所有子项的尺寸要求。 | |
当这个视图应该为它所有的子项分配一个尺寸和位置的时候调用。 | ||
当这个视图的尺寸被改变时被调用。 | ||
绘画 | 当视图需要绘制其内容时被调用。 | |
事件处理 | 当一个按键事件发生时被调用。 | |
当一个按键释放事件发生时调用。 | ||
当一个跟踪球动作事件发生时被调用。 | ||
当一个触摸屏动作事件发生时被调用。 | ||
Focus | 当视图获取或丢失焦点时被调用。 | |
当包含视图的窗口获取或丢失焦点时被调用。 | ||
Attaching | 当视图被附着到一个窗口时被调用。 | |
当视图从一个窗口拆分开时被调用。 | ||
当包含视图的窗口的可见性发生改变时被调用。 |
一个自定义视图示例A Custom View Example
这个在API Demos中的CustomView例子提供了自定义视图的示范。这个自定义视图定义在 LabelView 类中。
LabelView例子说明了自定义组件的很多不同方面:
· 为一个完全自定义组件扩展视图类。
· 参数化的构造器,采用视图扩充参数(这些参数定义在XML中)。其中一些被传递给视图超类,但更重要的是,有一些自定义属性被LabelView所用。
· 基本的公共方法来设置一个label组件你所期望的类型,比如setText(), setTextSize(), setTextColor() 等等。
· 一个重写的onMeasure方法来决定和设置这个组件的绘制尺寸。(注意在LabelView中,实际工作是通过一个私有的measureWidth()方法来完成的。)
· 一个重写的onDraw()方法来把标签(label)画到提供的画布canvas中。 method to draw the label onto the provided canvas.
你可以在custom_view_1.xml中查阅一些使用范例。特别是,你可以看到android:命名空间参数和自定义app: 命名空间参数的一个混合。这些app:参数是LabelView认识和使用的,并被定义在R资源类的一个可格式化的内部类里。
复合控件Compound Controls
如果你不想创建一个完全自定义的组件,但希望由一组已有控件来组装成一个可复用组件,那么创建一个复合组件(或复合控件)可以满足要求。在一个小容器里,把一些更原子的控件(或视图)整合进一个逻辑项目组中,从而可以被当作单个控件来对待。比如,一个组合框,可以被看作是一个单行文本编辑控制和一个附有弹出列表的相邻按钮的组合。如果你按下这个按钮并从列表中选择一些项,它将被生成到文本编辑域中,不过用户也可以直接在文本编辑域进行输入如果愿意的话。
在Android里,事实上有另外两种视图也是这样做的: Spinner和AutoCompleteTextView,不过,组合框的概念比较容易理解。
为了创建一个复合组件:
1. 通常的起点是某种类型的布局,因此创建一个扩展布局的类。可能在组合框例子中,我们会使用一个水平方向的线性布局LinearLayout。记住其它布局可以被嵌套在里面,这样这个复合组件可以任意复杂和结构化。注意就像一个视图,你既可以使用声明(基于XML)方式来创建这个包含的组件,也可以通过编码在程序中嵌入它们。
2. 在这个新类的构造函数中,采用任何超类期望的参数,并首先传递给超类构造函数。然后你可以建立用于新组件的其它视图;这就是你将创建EditText域和PopupList的地方。注意你还可能需要引入你自己的属性和参数到XML中,这些将被提取出来并为你的构造函数所用。
3. 你还可以为包含的视图可能产生的事件创建侦听器,比如,一个列表项点击侦听器的方法,用来在一个列表项被选中时更新相应的文本编辑框内容。
4. 你可能还想通过访问和修改函数来创建自己的特性,比如,允许EditText值可以被初始化并在需要时被查询到。
5. 在扩展一个布局时,你不需要重写onDraw()和onMeasure()方法,因为布局会有缺省行为。不过,如果需要的话,你同样可以重写它们。
6. 你可能会重写其它on... 方法,比如onKeyDown(),当一个特定键被按下时可能会从弹出列表中选择某个特定的默认值。
总之,基于布局创建自定义控件有如下一些好处,包括:
· 你可以像活动屏幕一样使用声明性的XML来指定布局,或者通过编程来实现视图嵌套。
· onDraw()和onMeasure()方法(加上大多数其它on... 方法)将可能有合适的行为,这样你就不需要重写它们。
· 最后,你可以很快地构建任意复杂的复合视图以及把它们当作单个组件来重用。
复合控件的例子Examples of Compound Controls
在随SDK发布的API Demos项目中,有两个列表例子-例4和例6,在Views/Lists下面,演示了一个从线性布局扩展的SpeechView,该组件用来显示演讲摘录。相应的类在List4.java 和List6.java里。
修改一个已有视图类型Modifying an Existing View Type
在某些情况下有更简单实用的方法来创建自定义视图。如果有一个视图已经和你想要的很相似,你只需要在这个组件上直接扩展并重写相关行为方法即可。通过完全自定义的组件你当然可以做任何事,不过通过一个视图层次图中的特定类,你也一样可以做很多事来满足需求。
比如,SDK在这些例子中包含了一个记事本应用程序NotePad application 。该程序演示了Android平台应用的很多方面,其中之一就是扩展一个EditText视图来创建一个条纹记事本。这并非一个完美的例子,所使用的APIs可能有所变化,但它的确说明了原理。
如果你没有这么做过,导入记事本例子到Eclipse中。特别是看一下NoteEditor.java文件中的MyEditText定义。
下面是一些值得注意的地方:
1. 定义The Definition
这个类以下面的代码行定义:
public static class MyEditText extends EditText
o 它被定义为NoteEditor活动的内部类,但它是公共的,因此它可以在NoteEditor类之外以NoteEditor.MyEditText来访问。
o 它是静态的,这意味着它不产生允许它从父类中访问数据的所谓的“伪方法”,这就意味着它的行为更像一个独立的类而不是和NoteEditor有着很强的关联。这是一个用来创建内部类的简洁方式,如果这些类不需要从外部类中访问状态,这使得产生的类小巧且很容易从被其它类所使用。
o 它扩展了EditText,我们选择基于这个视图来自定义组件。这些结束后,新的类将能够被用来代替通常的EditText视图。
2. 类初始化Class Initialization
通常,先调用超类。此外,这并非缺省构造器,而是一个参数化的构造器。这个EditText通过扩充在XML布局文件中的这些参数创建,这样,我们的构造器同样需要采用它们并传递给超类构造器。
3. 重写方法Overridden Methods
这个例子中,只有一个方法需要被重写:onDraw()- 但是当创建你自己的组件时,可能需要重写其它方法。
对于记事本NotePad例子,重写onDraw()方法可允许我们在EditText视图画布上绘制蓝线(这个画布canvas被传给这个重写的onDraw()方法)。这个super.onDraw()方法在这个方法结束前被调用。这个超类方法应该被调用,但在这个例子里,我们是在画好线后才调用它。
4. 使用自定义组件Use the Custom Component
现在我们已经有了自定义组件,但我们该怎么用它呢?在记事本NotePad例子中,这个自定义组件直接在布局声明中使用,所以看一下res/layout目录下的note_editor.xml文件。
<view
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中以一个通用视图创建,而这个类用包全名来指定。还需要注意到这个我们定义的内部类通过NoteEditor$MyEditText 标记来引用,这在Java编程语言中是一个引用内部类的标准方式。
如果你的自定义视图组件不是定义为一个内部类,那么你可以,有选择的,以XML元素名来声明这个视图组件,并且不需要包含class属性。比如:
<com.android.notepad.MyEditText
id="@+id/note"
... />
注意MyEditText 类现在是一个独立的类文件,当这个类被嵌入到NoteEditor类里时,这个技术无效。
o 定义中的其它属性和参数被传递给自定义组件的构造函数,然后传递给EditText的构造函数,因此这些是和你用于EditText视图的相同的参数。注意你同样可以添加自己的参数,下面会再谈到这一点。
这就是所有的内容。我承认这只是一个简单的例子,但关键是-创建自定义组件的复杂度只和你的需求有关。
一个更复杂的组件可以重写更多的on... 方法并引入一些它自己的帮助(helper)方法,大量的定制它的特性和行为。唯一的限制在于你的想象力和你想让这个组件做什么。