概述
安卓为构建你自己的UI提供了一个成熟且强大的自定义模型,基于基本的布局类:
View
和
ViewGroup
. 开始使用它之前,平台包含了许多预先构建好了的view和viewgroup的子类——分别叫做组件(widgets )和布局(layouts )——你可以使用它们来构建你的UI。
如果这之中没有你所需要的组件或布局,你可以创建你自己的view子类。如果你只需要小范围的调整一个已经存在的组件或布局,你可以简单的继承该组件或布局,并且覆盖它的方法。
创建你自己的view子类让你精确的控制屏幕的外观和功能元素。你在自定义view上给定的一些控制想法,这里有一些例子来说明你可以用他们来做什么:
- 你可以创建一个完整的自定义显示的view类型,例如使用2D绘图显示一个“音量控制”按钮,和类似的模拟电子控制。
- 你可以组合一组view组件成为一个新的单一组件,或许像一个ComboBox (一个组合了浮动列表和自由输入文本框的控件),一个双面选择控制器(dual-pane selector contro,左和右窗格都有一个列表,你可以重新组织哪个item在哪个列表中),等等。
- 你可以通过覆盖的方法让一个EditText组件呈现在屏幕上( Notepad Tutorial 是该方法的一个成功案例,创建了一个线性的记事本页)。
- 你可以捕获其它事件并且用一下自定义方法来处理他们(如游戏一样)
接下去的章节展示了如何创建自定义view 并且在你的app中使用它们。更多有用的消息,请查看
View
类中的介绍。
基本方法( Basic Approach)
这你是一个抽象的概述,在开始创建视图组件时你需要了解的东西:
为你自己的类继承一个已经存在的view类或者子类
使用你新继承的类。一旦完成,你新继承的类可以被用来替代基类的view 。
技巧:扩展类可以作为一个activity 中的内部类被定义并且使用它。这是很有用的,因为它控制访问权限,但是不是必须的(或许你想为你的应用创建一个公共的view)
完全的自定义控件(Fully Customized Components)
完全自定义控件可创建你希望如何显示的生动的组件。或许一个图形音量表(
graphical VU meter
)看起来像一个老式的模拟仪表,或者一个一起唱歌的长文本视图(
sing-a-long text view
),当一个光标沿着歌词移动时你可以跟着卡拉ok机器一起唱。无论哪种方式,都是让你内置组件做他不会做的事,不论你是怎么组合他们的。
幸运的是,你可以很容易的以任何方式创建一个你喜欢的外观和行为的组件。限制它的或许仅仅只有你的想象力,屏幕的大小和可用的处理能力(记住,你的应用最终或许运行在一些低性能的平台上)
为创建一个完全的自定义控件:
- 你可以继承的最平常的类,view,这样你通常会以继承该类来创建你的新的上级组件开始。
- 你可以提供一个构造器,该构造器可以从xml文件中取得属性和参数,并且你可以使用你自定义的属性和参数(也许是声量器的颜色和范围,或者是宽度和针的电阻,等等)
- 你或许想要在你的自定义类中创建你自己的事件监听者,属性访问器和修改器,和更多成熟的行为。
- 如果你想要这个组件显示一些什么东西,你几乎肯定想覆盖onMeasure() 和onDraw()。 当这些都没有被覆盖时,默认的
onDraw()
将不什么都不做,默认的onMeasure() 将大小设置为100*100——这可能不是你想要的。
- 其它on...方法可以在需要时被覆盖
Extend onDraw()
and onMeasure()
onDraw()
method
方法传递了你的
Canvas(画布),该画布可以实现任何你想要的:2D绘图,其它标准或者自定义控件,字体样式,或者任何你能想到的东西。
注:它不支持3D绘图,如果你想使用3Dh绘图,你必须继承
SurfaceView来代替view,并且使用单独的线程来进行绘制操作。查看
GLSurfaceViewActivity
示例获取更多信息
onMeasure()
稍微复杂一些。
onMeasure()
是你的组件呈现在容器上的关键。
onMeasure()
应该被覆盖用来提高效率并且提供准确的测量值。这是一个来自它父视图的稍微复杂的需求限制(该父视图通过
onMeasure()
方法传递了过来)并且 一旦计算完成 便会根据需求调用 含有测量的宽度和高度的
setMeasuredDimension()
方法。如果你不在
onMeasure()
方法中调用该方法,结果将会在运行时抛出错误(exception)。
概括的说,实现
onMeasure()
方法会看起来像这样:
- 覆盖的onMeasure() 方法被调用时需传递宽度和高度两个测量规范(
widthMeasureSpec
和heightMeasureSpec
参数,两个都是int 类型来表示尺寸 dp)它应该被当作限制你测量的宽度和高度的要求来对待。一个完整的参考这些规范需要的限制能够在View.onMeasure(int, int)引用文档中找到(该文档很好的说明了整个测量操作流程)
- 你组件的onMeasure() method 方法应该计算测量的宽度和高度值,这两个值将会被要求给予该组件。它应该试图保持在传入的规范之中,尽管可以选择超过规范(在该案例中,父视图可以选择这样做,包括裁剪,卷动,抛出一个错误或者请求再次调用
onMeasure(),或许有不同的测量规范
)。
- 一旦宽度和高度被计算出来,setMeasuredDimension(int width, int height) 方法一定会被调用,并且传递计算出来的测量值。如果不这么做将会抛出一个错误。
这里总结了一些框架在view 中调用的一些标准方法:
类别 | 方法 | 描述 |
---|
Creation | 构造函数 | 第一个构造函数在view创建时会被调用。第二个构造函数试图解析并应用任何在布局文件中定义的属性(attributes ) |
onFinishInflate()
| Called after a view and all of its children has been inflated from XML. |
Layout | onMeasure(int, int)
| 在确定view及其所有子节点的大小时 调用 |
onLayout(boolean, int, int, int, int)
| 当view需要为其所用的子节点分配位置和大小时 调用 |
onSizeChanged(int, int, int, int)
| 当view的大小改变时 调用 |
Drawing | onDraw(android.graphics.Canvas)
| 当view需要绘制其内容时 调用 |
Event processing | onKeyDown(int, KeyEvent)
| 当新的按键按下事件发生时 调用 |
onKeyUp(int, KeyEvent)
| 当按键弹起事件发生时 调用 |
onTrackballEvent(MotionEvent)
| 当轨迹球移动事件发生 调用 |
onTouchEvent(MotionEvent)
| 当屏幕点击事件发生 调用 |
Focus | onFocusChanged(boolean, int, android.graphics.Rect)
| 当view获得或失去焦点时 调用 |
onWindowFocusChanged(boolean)
| 当包含view的window获得或失去焦点时 调用 |
Attaching | onAttachedToWindow()
| 当view附加到window上时 调用 |
onDetachedFromWindow()
| 当view从window上分离时 调用 |
onWindowVisibilityChanged(int)
| 当包含view的window的可见性改变时 调用 |
一个自定义view的例子
- 继承自view类
- 参数化构造器可以拿到view展开时的参数(定义在xml文件中的参数)。一些参数v传递了进来并且给了父view ,但是更重要的是,这里有一些自定义的属性并且在labelview中使用了。
- 你希望看到一个标签组件的标准公共方法类型,例如
setText()
, setTextSize()
, setTextColor()之类的。
- 一个被覆盖的
onMeasure
方法用来决定和设置呈现出来的组件大小。(注意,在该例子中,真正工作的是私有的measureWidth()方法)
- 一个被覆盖的onDraw() 方法 来在提供的画布上绘制标签
你可以看到一些使用示例,在labelview 的
custom_view_1.xml
中。另外,你可以看到一个混合使用了
android:
命名空间参数和自定义的
app:
命名空间参数。这些
app:
参数是自定义的仅被LableView 识别和起作用的,它在
样式(styleable )内部类
中被定义,该内部类
在R.resources 中定义。
复合组件(Compound Controls )
如果你不希望创建一个完全的自定义控件,而是希望放在一起。一个 由一组现有的控件组成一个可用组件,然后创建一个混合组件(
Compound Component or
Compound Control)或许适合你的要求。在一个极小容器中,汇集了更多的原始控件(或者视图)到一个逻辑上的项目组(
group of items),该组可作为一个单一事物来对待。例如,一个组合框(
Combo Box
)可以被看作是一个简单的线性Edittext和一个相邻的按钮贴在一个弹出列表(
PopupList
)上。如果你按下按钮并且从列表中选择一些东西,它填充到Edittext 字段中,但是用户仍然可以直接在Edittext中输入东西。
为创建一个组合控件:
- 通过从某种类型的布局(Layout)开始,这样,继承一个layout创建一个类。或许在组合框的案例中我们或许使用了水平的线性布局。记住,其它布局可以被嵌套在里面,因此该组合控件可以任意的组合和构造。就像用activity一样,你可以使用声明(基于xml)来创建组件,或者在代码中嵌套它。
- 在新类的构造器中,获取父类期望的任何参数,并且首先通过父类的构造器将其传递过去。然后你可以在你新的构件中建立其它视图;这就是你创建edittext和popuplist 的地方。注意,你或许需要引入你自己的属性和参数到xml中,这些属性和参数可以在别处使用 并且被你的构造器使用。
- 你也可以为你的view可能发生的事件创建监听者,例如,为一个列表项点击(List Item Click )创建一个监听者来更新edittext 的内容。
- 你或许需要创建你自己的属性的存取器和修改器 ,例如,允许edittext 值可以被设置初始值并且在需要时查询内容。
- 继承一个layout 时,你不需要覆盖
onDraw()
和onMeasure()
方法,因为布局会有默认的行为或许会工作得很好。然而,你仍然可以在需要的时候覆盖他们
- 你或许会覆盖其它on...方法,例如onKeyDown(),或许时从该组合框的popup list 中选择一个确定的值,当一个确切的按钮被按下时
综上所述,使用layout 作为组合控件的基础有如下优点:
- 你可以在声明性的xml文件中使用它,就像使用一个activity屏幕一样,或者用代码创建一个视图并且将其嵌套进布局中。
onDraw()
和 onMeasure()
方法(加上其他的on...方法)将会有适当的行为,因此你不需要覆盖他们。
- 最后,你可以十分快速的构建组合控件视图,并且作为一个单一控件重用他们
组合控件实例
在API Demo 工程中,有两个列表示例——在
Views/Lists之下的
Example 4 and Example 6
展示了一个
SpeechView
,它继承自
LinearLayout来组合展示了
Speech
引用。在示例代码中的对应代码是
List4.java
和
List6.java
.
更改一个已经存在的视图类型
这是一个创建自定义布局的更为简单的选择,如果有一个与你想要的组件十分相似的组件,你可以直接继承它并且覆盖你想要改变的行为。在完全的自定义控件中,你可以做你任何想做的事情,但是通过直接继承一个指定类开始的方法中,你可以获取到很多行为但或许都不是你想要的。
例如,SDK中的
NotePad application
案例。这展示了许多使用安卓平台的许多方面,其中的一个是继承一个Edittext 视图来构造一个线性记事本。这并非一个完美的示例,在早期的api上运行可能会看起来有所不同,但它确实展示了原理。
如果你还没有这么做,将这个按钮添加到Eclipse (或者直接使用记事本查看源码)。尤其要注意
NoteEditor.java
文件中的
MyEditText定义。
需要注意的一些点
1.定义
该类是这么定义的:
public static class MyEditText extends EditText
- 它作为NoteEditor activity 的内部类被定义的,但是它是public 这样可以使用NoteEditor.MyEditText从NoteEditor类的外部被访问到。
- 它是static 意味着它不会产生所谓的“合成方法”来允许它从父类中存取数据,这也叫意味着它是作为一个独立的类而不是依赖于
NoteEditor
。
这是一个简洁的方法来创建内部类,如果它不需要从外部类中获取数据。使生成的类更小,并且允许在其它类中更易使用。
- 它继承自
EditText
,这是在该案例中我们选择的来进行自定义的view.当我们完成时,这个新的类将可作为传统EditText
,视图的替代品。
2. 类的初始化
同往常一样,首先调用父类。此外,这不是默认的构造函数,但是一个参数化的。该Edittext 使用这些参数被创建,这些参数来自于xml布局文件,至此,我们的构造器需要获取并且将它们传递给父类的构造器。
3.覆盖方法
在该案例中,只有一个方法被覆盖了:
onDraw()—
but
但这样很容易被误认为是创建一个你自己的自定义控件。
对于
NotePad
案例,覆盖
onDraw()
方法允许我们在Editext 视图画布上绘制蓝色的线条(该画布通过
onDraw()
方法传递了过来
)。
super.onDraw()
方法在该方法的最后被调用。父类方法应该被调用,但是在该案例中,我们在绘制了线条之后才这么做。
4.使用该自定义控件
现在,我们有了自己的自定义控件,但是如何去使用他呢?在
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文件中被创建,并且它的class 使用了具体的全部包名。注意我们定义的内部类是使用如下形式引用的
NoteEditor$MyEditText ,
该标记是在java语言中引用内部类的标准方法。
如果你的自定义视图没有作为内部类定义,你可以选择如下两项,在xml元素名中声明该控件的名称,或者包含class 属性在该属性中指明:
<com.android.notepad.MyEditText
id="@+id/note"
... />
注意,现在的
MyEditText
是一个独立的类。当该类嵌套在
NoteEditor
类中时,这样做是无效的。
其它的属性和参数都会传递到该自定义组件的构造函数中,之后传递给
EditText的构造器,因此它和你使用
EditText视图时有着相似的参数。注意,可以添加你自己的参数,并且将会在下面提及。
这就是所有的关于如何自定义控件的方法。不可否认这是一个简单的情况,但重点是——创建自定义组件只需要这么复杂。
一个更加成熟的组件或许会覆盖更多的on...事件并且引入一些自己的帮助类,大体上,自定义它的属性和行为。唯一限制你的就是你的想象力和你期望该组件要做什么。