基于基本布局类:View和ViewGroup,android提供了一个先进而又强大的组件模型,方便你构建UI。首先,该平台预制了很多View 和ViewGroup的子类——就是那些widget和Layout,你可以用这些预制的子类构建你的UI。
可用widgets比如有Button
, TextView
,EditText
, ListView
, CheckBox
, RadioButton
, Gallery
, Spinner
, 具有特定用途的有AutoCompleteTextView
, ImageSwitcher
, 和TextSwitcher
.
可用的布局有LinearLayout
, FrameLayout
, RelativeLayout
等等,Common Layout Objects章节有更多的示例。
如果这些预制的widget或Layout类都不能满足你的需求,你可以创建自己的View子类。如果你仅仅需要对已有的wigget或者Layout做些微调,那么你简单的继承这个widget或者布局类并覆写它的方法就可以了。
创建自己的View子类可以让你对屏幕中元素的外观和功能进行精确的控制。你可以参照下面的示例,这些示例对你自定义View有一些建议:
- 你可以创建一个完全自定义绘制的View,例如用2D渲染一个音量控制按钮,绘制出外观像模拟电子控制的按钮。
- 你可以把多个View组合为一个全新的单个View,比如做出一个像组合框的View(一个弹出列表和文字输入框的结合体),或者是一个双重选择控制器(左右两边都有一个列表,你可以重新分配列表中的item在哪个列表中),等等……
- 你可以覆写EditText组件在屏幕上绘制的方法(Notepad Tutorial教程就是用这种方法创建了一个有条纹效果的页面)。
- 你可以捕获更多的事件,例如key presses,并用你自定义的方法来处理这个事件(比如在游戏设计中)。
下面的部分是展开介绍如何创建一个自定义View,如何用在你的程序用使用这些自定义View。参看View类以获取更多的详细信息。
The Basic Approach
下面是对如何创建自己的View组件的一个简要概括:
- 创建自己的类,让它继承一个既有的View类或其子类
- 覆写父类的某些方法。被覆写的方法都以“on”开头,例如,
onDraw()
,onMeasure()
, 和onKeyDown()
.这和你处理生命周期时覆写Activity或者ListAtivity的onXXX方法或者其它回调方法一样。 - 使用这新的扩展类。这些完成后,你这个新的类就可以用在其父类出现的任何地方。
Tip:子类可以作为内部类定义在使用它的Activity类里面。这有利于控制外部对它们的访问,但是并不是一定要这么干(或许你想创建一个public View,可以再你的APP中广泛使用它)。
Fully Customized Components
完全自定义控件可以创建出你想要的图形。或许是一个像老式模拟仪表的音量显示器,或者是一个伴唱字幕,上面有一个跟随字幕文字移动的球便于你通过卡拉OK机伴唱。也就是说无论你怎么组合内置的那些控件都无法满足你的要求。
还好,你可以很容易地创建出你想要的控件,你可以控制它的外观和行为,这只受限于你的思维、屏幕的尺寸、可用电量(要知道,你的程序最终要在电量比桌面电脑少的多的设备上运行)。
要完全创建一个自定义控件:
- 毫无疑问,你最可能继承的类就是View,所以通常需要从继承这个类来开始创建你那牛B的控件。
- 你要提供一个能从XML文件获取属性和参数的构造函数,并且你也要使用这些属性和参数(或许是颜色和音量仪表所占的空间,或者是指针的宽度和振幅,等等……)
- 也许你还要创建自己的时间监听器,属性访问器和修改器,或许你的控件中还有更多复杂的操作。
- 你很可能要去覆写onMeasure(),如果想让你的控件显示一些东西,同样要覆写onDraw() 。虽然这两个方法都有缺省操作,但是缺省onDraw什么都不干,缺省onMeasure()通常设置View的Size为你不想要的100×100大小。
- 如果需要,你还要覆写其它onXXX()方法。
Extend onDraw()
and onMeasure()
这个onDraw()方法传进一个Canvas参数,你可以在它上面实现任何你想要的2D图形,还有标准的或者自定义的控件(译注:这里指标准图形:直线、点圆……和自定义图形:利用标准图形或者图片绘制自己想要的图形),有样式的文本,或者你能想的到的任何东西。
注意:这并不适用于3D图形绘制。你如果你想绘制3D图形,你必须继承SurfaceView类而不是View类,而且在一个单独的线程中进行绘制动作。详情参看GLSurfaceViewActivity示例。
onMeasure()方法用的多一点。onMeasure()方法是子控件与其父控件之间绘制规则的关键环节。应该覆写onMeasure()以高效、精确的报告它所占用的尺寸。这有点小复杂,因为父控件会对子控件有一个限制(作为onMeasure的参数传递进来的),并且一旦计算好measured width和measured height后,必须调用setMeasuredDimension()方法,如果你在覆写onMeasure()方法的时候没有调用这个方法,就会在测量过程中抛出一个异常。
简而言之,执行onMeasure()的过程如下:
- 调用被覆写的onMeasure()方法,并用宽度和高度的测量限制条件(就是
widthMeasureSpec
和heightMeasureSpec这两个参数,它们都是
代表测量尺寸的限制条件的整形编码)作为参数传入,它们都是对你计算measuredWidth和measuredHeight的限制条件,可用限制条件种类的完整说明在View.onMeasure(int,int)的说明文档中(该文档还对整个测量过程做出了非常好的描述)。 - 你的控件的onMeasure()方法要预测出一个测量宽度和测量高度,这两个尺寸将用于绘制这个控件。尽管它可以选择无视测量限制条件(在这种情况下,父控件有权选择如何做,包括裁剪,滚动,抛出异常或者再次调用该onMeasure()方法),但是测量值最好符合传入的限制条件。
- 一旦计算出宽和高,必须调用setMeasuredDimension(int width,int height),并传入测量结果。否则将会抛出异常。
下面是View中一些标准方法的摘要,这些方法都会被系统框架调用:
种类 | 方法 | 描述 |
构造 | 构造函数 | 一种构造函数是在从代码里创建View时被调用,另一种是从一个布局文件中inflate视图时被调用。第二种应该解析并运用任何定义在布局文件中的属性。 |
onFinishInflate() | 一个View及其子View被inflated后,就调用此方法。 | |
布局 | onMeasure(int, int) | 调用该方法来计算该View及其子View的尺寸需求。 |
onLayout(boolean, int, int, int, int) | 当该View给它的所有子View分配大小和位置时调用该方法。 | |
onSizeChanged(int, int, int, int) | 当该View的大小发生改变后调用该方法。 | |
绘制 | onDraw(Canvas) | 当该View绘制其内容时调用该方法。 |
事件处理 | onKeyDown(int, KeyEvent) | 当有新的按键事件时调用该方法。 |
onKeyUp(int, KeyEvent) | 当有按键释放事件时调用该方法。 | |
onTrackballEvent(MotionEvent) | 当有轨迹球事件时调用该方法。 | |
onTouchEvent(MotionEvent) | 当有触屏事件时调用该方法。 | |
焦点 | onFocusChanged(boolean, int, Rect) | 当该View获得或失去焦点是调用该方法。 |
onWindowFocusChanged(boolean) | 当包含该View的window获得或是去焦点是调用该方法 。 注意:这和View的焦点是分开的:要能接收按键事件,View和它的window必须都是已经获得焦点的。如果有一个window显示在你的window之上那么它就获取了焦点,而你的window就失去了焦点,但是(你的window中的)View的焦点还是没有变。 | |
Attaching | onAttachedToWindow() | 当该view被附加到一个window上时调用该方法。 |
onDetachedFromWindow() | 当该View从window分离时调用该方法。 | |
onWindowVisibilityChanged(int) | 当包含该View的window显示或者隐藏时调用该方法。 |
A Custom View Example
API Demos中的 CustomView 示例提供了一个自定义控件的例子。自定义的控件创建在 LabelVie 类中。
LabelView 简单演示了自定义控件的方方面面:
- 为一个完全自定义控件扩展View类。
- 采用有参数的构造函数,该方法接受View的inflation参数(这些参数定义在XML中)。其中一些参数传给父类(的构造函数处理),但是重点是LabelView定义并使用了几个自定义属性。
- 定义了一些public方法,这些方法可以让这个控件有你想要的样子,例如
setText()
,setTextSize()
,setTextColor()等等。
- 覆写onMeasure的方法来测量和保存这个控件的绘制大小。(注意:在LableView中,实际工作是由一个叫measureWidth()的private方法来做的。)
- 覆写onDraw()方法来在提供的canvas上绘制label。
你可以在示例的custom_view_1.xml中看到自定义LabelView的一些用法,重点是,你可以看到android:命名控件参数和自定义的app:命名控件参数混合使用。那些app:参数是LabelView识别并使用的自定义参数,并且这些参数定义在示例程序的资源定义类R类的一个样式内部类中。
Compound Controls
如果你不想创建一个完全自定义的控件,而是想用现有的控件组装一个可重用的控件,那么做一个Compound Component(或者叫Compund Control)或许正好满足需求。在一个小容器内(nutshell),把一些更小的控件(或者说View)组成一个逻辑分组,这个逻辑分组可以作为一个整体使用。例如,一个Combo Box 可以看作是由一个单行文本编辑框和旁边的一个按钮组成,这个按钮附加了一个弹出列表。如果你点击这个按钮并从列表中做出选择,那么选择的东西就会显示在文本编辑框中,如果用于愿意,也可以直接在文本编辑框中输入。
在Android中,其实另外两个现在成View是这么做的:Spinner
和AutoCompleteTextView
,但是,Combo Box的概念更容易理解。
创建一个组合控件:
- 通常开始于某种布局,所以创建一个继承Layout的类。拿Combo box来说,我们可能要用一个LinearLayout,采用水平排列。记住里面可以嵌套其它的布局,所以一个组合控件可以任意组合构造。注意,就像用Activity一样,你即可以用声明(XML-based)的方法去创建包含的控件,也可以用代码构建它们。
- 在新类的构造函数中,接收任何父类想要的参数,并且第一时间把它们传给父类构造函数。接下来你可以安装新控件要用的其它View,就是你要在这里创建文本编辑框和弹出列表。你可能还要在XML中描述自己的属性和参数,这些属性参数都将被提取出来为你的构造函数所用。
- 你还可以创建监听器,监听器可以监听你所包含View所产生的事件,例如,当对列表进行选择操作时,一个List Item Click Listener 的监听方法可以用来更新文本编辑器的内容。
- 你可能还要用访问器和修改器来创建自己的属性,例如,允许该控件中的文本编辑框的值可以初始化,并且可以在需要是查询其内容。
- 在继承了Layout的情况下,你不需要覆写onDraw()和onMeasure()方法,因为Layout的缺省行为已经做的很好了。但是如果需要你还是可以覆写它们。
- 你可能还要覆写其它onXXX方法,比如onKeyDown,当某些键被按下时可以取出Combo box中弹出列表的某些默认值。
总而言之,基于Layout创建自定义控件的优势包括:
- 你可以像Activity屏幕那样用声明性的XML文件来指定布局,也可以用代码创建View,并把它们嵌入到这个布局里面。
- onDraw()和onMeasure()方法(还有其他很多onXXX方法)很可能已经有了合适的行为,所以你不必覆写它们。
- 最后,你可以快速、任意地把多个控件组合起来并且把它们当成单个控件那样复用。
Examples of Compound Controls
随SDK发布的Demos工程中,在Views/List下有两个List的示例-Example4和Example6,做了一个SpeechView的演示,该控件扩展了LinearLayout显示演讲摘要。演示代码相应的类是List4.java和List6.java。
Modifying an Existing View Type
在某些情况下有更简单更实用的方式去创建自定义控件。如果现有控件中有和你的需求非常相近的控件,你可以简单的扩展这个控件并只改变你想改变的行为就行了。通过完全自定义控件你当然可以做任何事,但是从View树种的一个特定类作为起点你同样可以做很多事,并且很可能只用做你想做的事。
例如,SDK的示例中有个NotePad application的工程。该工程演示了android平台很多方面的应用,很多都是扩展EditText View来制作一个有条纹效果的记事本。这并非完美的例子。并且所用的早期API现在可能有变,但是它演示了这种基本思路。
如果你没有这样干过,把这个NotePad示例导入Eclipse吧(或者通过这里的链接看一下源代码)。其中特别留意NoteEditor.java中MyEditText的定义。
一些要注意的要点:
1.定义
这个类用下面一行代码定义:
public static class MyEditText extends EditText
- 它是做为NoteEditor Activity的一个内部类定义的,但是它是publict的,所以如果需要可以在NoteEditor的外部通过NoteEditor.MyEditText来访问。
- 它是static的,就是说它不会产生访问外部类数据的所谓“synthetic methods”,也就意味着它是一个独立的类而不是和NoteEditor有很深的关联。如果它不需要访问外部类的状态,那么用这种方法创建内部类比较简洁,并且使内部类短小精悍,也可以被其它类放边使用。
- 它继承自EditText,我们选择基于EditText来定制控件。完成后,新的类将代替普通的EditText View。
2.类的初始化
通常,要先调用父类(的构造函数)。此外,这里用的不是默认构造函数而是由参数的构造函数。当通过inflat xml文件创建EditText的时候用的是有参数(的构造函数),因此我们的构造函数也需要接收这些参数并把它 们传给父类的构造函数
3.覆写方法
在这个例子中,只覆写了一个方法:onDraw()-但是你创建自己的控件时很可能需要覆写其它的方法。
对于NotePad示例,覆写onDraw()方法可以让我们在EditText View的Canvas(这个Canvas是通过被覆写的onDraw()方法传递进来的)绘制蓝色的条纹。父类的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文件中创建,用完成包名来指定其对应的类。还要注意的是:用NoteEditor$MyEditText来引用我们定义的内部类,这是java编程中引用内部类的标准方法。如果你的自定义控件不是内部类,那么你就可以在xml文件中用element名称来声明控件,不需要class属性。例如:
<com.android.notepad.MyEditText id="@+id/note" ... />
注意这种情况是MyEditText是一个独立的类文件。如果是嵌套类的话这种写法就不会正常工作。
- 其它的属性和参数是用来传递给自定义控件的构造函数的,然后再传给EditText的构造函数,所以他们和你使用EditText时传递的参数一样。同样,也可以加入你自己的参数,我们下面会讲到这些。
这就是所有的内容。当然,这是比较简单的case,但是自定义控件的复杂度只和你的需求有关。
更复杂的控件需要覆写更多的onXXX方法,也需要引入它的辅助方法,大量的定制它的属性发方法。这决定于你的思维和你的需求。