基于布局类View和ViewGroup的基本功能,Android为创建自己的UI界面提供了先进和强大的定制化模式。首先,平台包含了各种预置的View和ViewGroup子类---Widget和layout,可以使用它们来构造自己的UI界面。
部分的可以利用的widget包括:Button、TextView、EditText、ListView、CheckBox、RadioButton、Gallery、Spinner、以及比较特殊用途的AutoCompleteTextView、ImageSwitcher和TextSwitcher。
其中可利用的布局是:LinearLayout、FrameLayout、RelativeLayout以及其他的布局。
如果遇到了没有预置的widget或layout的需求,可以创建自己的View子类。如果只需要对既存的widget或layout进行小的调整,那么只需简单的继承widget或layout,并且重写它们的方法。
创建自己的View子类,以便能够精准的控制屏幕元素的外观和功能。以下是用定制View对象来实现这种控制想法的一些例子:
创建一个完全的定制化渲染的View类型,如用类似模拟电子控制的2D图形来渲染的音量控制按钮。
把一组View组件组合成一个新的单一组件,制作一些像ComboBox(一个下拉列表和文本输入域的组合)、双面板选择器(左右两个列表面板,右边的列表面板中的项目与左边列表面板中的一个项目相关联)等组件。
重写一个EditText组件在屏幕上的渲染的方法。
捕获一些像按键一样的事件,并在某些定制的方法中处理它们(如游戏)。
以下是创建自定义View组件需要要了解基本概要:
自定义的View类要继承一个既存的View类或其子类;
在子类重写父类的一些方法。要覆写的父类方法是用‘on’开头的,例如,onDraw()、onMeasure()和onKeyDown()等,这有点类似于重写Activity或ListActivity的生存周期回调的on…事件。
使用新的扩展类,一旦完成,新扩展的类就能被用于替换基本的View对象。
提示:扩展类能够作为使用它们的Acticity的内部类来定义。这样对控制对它们的访问是有益的,当然可以创建一个新的公共的View类,这样就可以在应用程序范围内来使用。
完全定制化的组件能够用于创建你所期望的显示效果的图形化组件。可以是看上去像旧的模拟仪表的图形化VU仪表,或者是一个长的歌词视图,有一个跳动的球沿着歌词移动,以便跟着这卡拉OK机歌唱,这两种情况,无论如何组织内置的组件都无法满足要求。
幸运的是,能够使用任意自己喜欢的方法来创建组件的外观和行为,唯一的限制就是你的想象力、屏幕的尺寸和可利用的处理能力(因为应用程序最终可能运行在比桌面工作站处理能力要弱的设备上)。
以下是创建完全定制组件的步骤:
毋庸置疑,能够扩展的最通用的视图是View类,因此通常是继承这个View类来创建自己的新的组件;
提供一个能够从XML中获取属性和参数的构造器,并且也能够使用自己属性和参数(如VU仪表的颜色和范围,指针的宽度和阻尼等);
创建组件中可能的事件监听器、属性访问器和修饰符以及尽可能准确的行为等;
覆写onMeasure()回调方法,如果想要组件显示一些东西,也要覆写onDraw()回调。虽然它们都有默认的行为,onDraw()回调默认什么也不做,onMeasure()方法默认的要设置组件的尺寸为100x100;
覆写其他的需要on…方法。
onDraw()方法会把能够实现的任何想要的东西放到一个Canvas对象上,如2D图形、标准或定制的组件、样式化的文本、或其他任何能够想到的东西。
注意:View类不能使用3D图形。如果要使用3D图形,必须继承SurfaceView类,而不是View类,并且要在一个独立的线程中描画。
onMeasure()方法有点复杂,它是组件和它的容器之间的渲染约束的关键部分。覆写onMeasure(),以便准确高效的报告组件被包含部分的尺寸。由于来自父容器限制的要求,使得尺寸的测量有些复杂,并且组件的尺寸一旦被计算完成,就要调用setMeasureDimension()方法来保存测量的宽度和高度。如果在onMeasure()方法中调用setMeasureDimension()方法失败,这个结果在测量时将是一个异常的值。
在上层看,实现onMeasure()方法的步骤如下:
要用父容器的宽度和高度的计量规格来调用被覆写的onMensure()方法(widthMeasureSpec和heightMeasureSpec参数都是代表了尺寸的整数),这两个参数应该作为生成组件的宽度和高度的约束要求。对于这些规格约束类型的完整说明可以在View类说明的View.onMeasure(int,int)方法中找到。
组件的onMeasure()方法应该计算用于渲染组件所需的尺寸(宽度和高度)。组件应该尽量保留在被传入的规格范围内,尽管它能够选择超出规格范围(在这种情况下,父容器能够选择做的事情包括:裁剪、滚动、抛出异常、或者要求onMeasure()方法用不同的尺寸规格再试)。
一旦组件的宽度和高度被计算完成,就必须调用setMeasuredDimension(int width, int height)方法来保存计算结果。不这样做就会抛出一个异常。
下表是framework调用View类的其他标准方法:
分类 |
方法 |
说明 |
Creation |
Constructors |
构造器的调用有两种类型:1.在代码中创建View对象;2.用布局文件填充View对象。第二种类型应该解析和应用布局文件中的任何属性定义。 |
onFinishInflate() |
View对象和它的所有子对象都用XML填充完之后,调用这个方法。 |
|
Layout |
onMeasure(int, int) |
调用这个方法决定View对象及其所有子对象的尺寸要求。 |
onLayout(boolean,int,int,int,int) |
当View对象给它的所有子对象分配尺寸和位置时,调用这个方法。 |
|
onSizeChanged(int,int,int,int) |
当View对象的尺寸发生改变时,调用这个方法。 |
|
Drawing |
onDraw(Canvas) |
当View对象渲染它的内容时,调用这个方法。 |
Event |
onKeyDown(int,KeyEvent) |
当一个键的按下事件发生时,调用这个方法 |
onKeyUp(int,KeyEvent) |
当一个键弹起事件发生时,调用这个方法 |
|
onTrackballEvent(MotionEvent) |
当鼠标轨迹球滚动事件发生时,调用这个方法。 |
|
onTouchEvent(MotionEvent) |
当触屏事件发生时,调用这个方法。 |
|
Focus |
onFocusChanged(boolean,int,Rect) |
当View对象获取或失去焦点时,调用这个方法。 |
onWindowFocusChanged(boolean) |
当包含View对象的窗口获得或失去焦点时,调用这个方法。 |
|
Attaching |
onAttachedToWindow() |
当View对象被绑定到一个窗口时,调用这个方法。 |
onDetachedFromWindow() |
当View对象被从它的窗口中分离的时候,调用这个方法。 |
|
onWindowVisibilityChanged(int) |
当包含View对象的窗口的可见性发生改变时,调用这个方法。 |
在API Demos中提供了一个定制的View对象的例子:CustomView。这个定制的View定义在LabelView类中。
LabelView示例展示了很多定制组件的不同特征:
继承View类的完全定制化的组件;
参数化的带有View填充参数(在XML中定义的参数)方式构造View对象。有一些填充参数使用通过这个View的父类传递过来的,还有一些用于labelView对象而定义的定制的属性;
你所期望看到的标准的公共类型的方法,如setText()、setTextSize()、setTextColor()等等;
一个重写的onMeasure()方法,它决定和设置了组件的渲染尺寸。(注意:在LabelView类中,实际的工作是由一个私有的measureWidth()方法来做的。)
一个重写的onDraw()方法,它在提供的Canvas上描画标签。
从这个示例的custom_view_1.xml中,能够看到一些LabelView定制View的用法。实际上,可以看到android:命名空间参数和定制的app:命名空间的组合。这些app:参数是LabelView类所承认的并用于工作的一些定制化的属性,并且这些参数在示例的R资源定义类的styleable内部类中被定义。
如果不想创建一个完全定制的组件,而是想要把一组既存的控件放到一起,形成一个可重用的组件,那么创建一个复合组件(或复合控件)是一个合适的选择。在一个容器中,复合组件把多个原子化的控件(或View)组合成一个逻辑组,它能够处置一件单一的事情。例如,ComboBox控件,就是通过一个单行文本域、一个按钮和一个弹出列表组合而成的,如果按下按钮并且从列表中选择了一项,那么选择项就会填入单行文本域,而且如果用户喜欢,也可以直接把内容输入到文本域中。
在Android中,实际上有另外两View可以很容易的完成ComboBox的工作:Spinner和AutoCompleteTextView,但是不管怎样,使用ComboBox的例子会使得概念更容易理解。
以下是创建复合组件的步骤:
通常的起点是用某种类型布局,创建一个扩展布局的类。在ComboBox的例子中,可以使用水平方向的LinearLayout布局。其他布局也可以嵌套到内部,以便复合组件能够随意的组合。注意,跟Activity一样,既可以使用基于XML声明的方法来创建一个复合组件,也可以编程的方式把它们嵌入到代码中。
在新类的构造器中,可以带入任何子类需要的参数,并且首先要通过子类的构造器来传递它们。然后,就可以创建新组件中使用的其他的View了;ComboBox组件就是在这个时候创建了文本域和弹出列表。注意,也可以把自己的属性和参数引入到XML声明中,这些属性和参数能够被构造器使用。
还可以给被包含的View所可能产生的事件创建监听器,例如,针对列表项的Click监听器的方法,如果一个列表项被选择了,那么这个监听器方法就会更新文本域的内容。
创建自定义带有访问器和编辑器的属性,例如,ComboBox组件中针对文本域的get…和set…方法。
在扩展一个Layout的情况下,不需要重写onDraw和onMeasure()方法,因为布局的默认行为就可以很好的工作。但是,如果需要你依然可以重写它们。
还可以重写其他的on…方法,如可以重写onKeyDown()方法,当某个键被按下时,可以从ComboBox的弹出列表中选择某些默认值。
总结,使用布局作为定制控件的基础有以下好处:
能够像Activity一样使用声明的XML文件来指定布局,也能够用编程的方式创建View,并且从代码中把它们嵌入到布局中。
onDraw()和onMeasure()方法(还有其他的大多on…方法)都有适当的行为,因此不用重写它们。
最后,可以快速的构造任务复杂的复合View,并且可以把它们作为一个单一的组件来重用。
在SDK的API Demos工程中,有两个List的例子---在Views/Lists目录下示例4和示例6演示了一个SpeechView组件,它扩展了LineraLayout布局让组件显示语音查询结果。对应的类在List4.java和List6.java的示例代码中。
在某些情况中,对于创建有用的定制的View甚至有更容易的选项。如果有一个既存的组件与想要的非常相似,就可以简单的扩展这个组件,并且重写想要改变的行为。可以用完全定制的组件做所有想做的事情,但是通过从View层次树中的一个更特殊的类开始,还能够获得更多的你所期望的已经实现的行为。
例如,在SDK包含的NotePad应用程序的示例中,有许多使用Android平台的特征,其中有扩展EditText控件的单行记事本。这不是一个完美的例子,但是它演示了组件定制的一些原则。
使用下面链接,把NotePad示例的代码导入到Eclipse中,实际看一下NoteEditor.java文件中LinedEditText类的定义。
http://developer.android.com/resources/samples/NotePad/index.html
需要注意的一些问题:
定义
使用下面方法来定义类:
public static class LinedEditTextextends EditText
它是作为NoteEditorActivity的一个内部类来定义的,但是它是公共的,这样如果需要,就可以在NoteEditor类的外部用NoteEditor.MyEditText方式来访问。
它是静态的,这就;意味着不会产生所谓的允许访问来自父类的数据的合成方法,反过来意味着它实际是作为一个独立的类,而没有跟NoteEditor进行强制的关联。如果不需要从类的外部访问状态,要保持生成小的类,并且还要允许在其他类中能够容易的使用它,那么这是一种创建内部类的比较清晰的方式。
它扩展了EditText类,它是我们选择的要定制的View。定制完成后,新的类可以替代普通的EditText类。
类初始化
始终首先要调用的是super方法,它不是默认的构造器,而是参数化的一种方式。EditText是用XML布局文件中的这些参数来创建的,因此,我们的构造器需要带入这些参数,并把它们传递给父类的构造器。
重写方法
这个例子中,仅重写了一个方法:onDraw().
对于NotePad示例,重写onDraw()方法允许我们在EditText的画布上画蓝线(画布是有重写的onDraw()方法传入的)。在这个方法的最后,调用了super.onDraw()方法。父类的方法应该被调用,但是在这个例子中,我们在画完我们想要的线之后调用了父类的onDraw()方法。
使用定制组件
组件定制完之后,如何使用它呢?在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" />
定制的组件是用XML中的一个一般的View来创建,并且使用完整的包名来指定类。还要注意定义中引用内部类的方法:NoteEditor$LinedEditText,这是Java编程语言标准的引用内部类的方法。如果定制View组件没有作为内部类来定义,那么就要选择用XML元素名来声明View组件,并且要去除类的属性,如:
<com.android.notepad.LinedEditText
id="@+id/note"
... />
注意,LinedEditText类是一个独立的类文件,当这个类被嵌套在NoteEditor类中时,这种技术是不会工作的。
定义中的其他属性和参数被传入定制组件的构造器中,然后传递给EditText类的构造器,因此这些属性和参数与EditText类使用的是相同的。注意,也可以添加自己的参数。