Google向Android的java环境中添加了自己的GUI(GraphicalUser Interface)框架,以下称为Android GUI框架。这个框架跟java其他的GUI框架很类似,都是单线程、事件驱动、嵌套与继承机制下的窗口组件类库。我们先前已知的java GUI框架有:AWT,SWING,SWT,J2ME等。所以,如果你熟悉这些框架,那么也应该对Android的GUI框架很容易上手。
Android GUI框架采用了MVC(Model-View-Controller)的设计模式,见下图所示:
MVC模式下,系统框架的类库被划分为3种:模型(Model)、视图(View)、控制器(Controller)。模型对象负责建立数据结构和相应的行为操作处理。视图(View)对象负责在屏幕上渲染出相应的图形信息展示给用户看。控制器(Controller)对象负责截获用户的按键和屏幕触摸等事件,协调Model对象和View对象。
下面详细解释什么是MVC模式。
Model
Model是一个应用系统的心脏,它代表了这个系统实际要完成的所有功能处理。比如:在音乐播放器软件中,Model代表一个音乐数据库以及播放音乐的程序函数代码;在一个电话应用中,Model代表一个电话号码簿,以及拨打电话和发送短信的程序函数代码。
一个具体应用中的View和Controller一定会操纵Model来实现系统各项功能。反过来,一个Model类库可以被多个应用程序来使用。举一个例子来讲,有两个应用程序,一个是MP3播放器,一个是MP3转换WAV的音乐文件转换器。我们可以提供同一个Model类,这个Model类包括MP3的数据格式和编解码函数方法。MP3播放器可以用这个Model来控制MP3音乐的播放、暂停、恢复等,音乐文件转换器可以用这个Model来设置音乐的比特率等。
ViewView是软件应用传送给用户的一个反馈结果。它代表软件应用中的图形展示、声音播放、触觉反馈等职责。Android GUI框架中的图形展示部分使用了View类及其子类来实现。这些类代表屏幕中的一个矩形区域,这个区域也同时位于其父View类的矩形区域中。View的根节点是应用程序的自身窗口。
举例来说,MP3播放器中可能包含了一个当前播放曲目的唱片封面图。这个图就是一个View。另一个View组件可能是该曲目的文字标题。再一个就是一些播放按键,比如:Stop、Start、Pause等按钮。
窗口类之间的关系是一个树形关系,GUI框架遍历这棵树来绘制这些窗口对象到屏幕上。GUI框架按照前序遍历方式,依次调用各个View组件绘制自身的方法。这里的所谓前序遍历方式,也就是每个View组件先绘制自身,然后再调用各个子View组件绘制自身。这样,整个View树绘制完成,最接近根节点的View先绘制,接近叶节点的View晚绘制,因此叶节点的View最终展示在窗口的最上面。
Android GUI框架的执行效率非常高,它不会绘制被子View遮挡过的部分,也不会绘制没有发生变化的View。
Controller
Controller在软件应用负责对外部事件的响应,包括:键盘敲击、屏幕触摸、电话呼入等。Controller实现了一个事件队列,每一个外部事件均在事件队列中被唯一标识。框架依次将事件从队列中移出并派发出去。
例如,当用户按下一个键,Android系统将生成一个KeyEvent,并把它加入到消息队列中。最后,当前一个事件被处理完毕了,这个KeyEvent被从消息队列中移出,并传递给当前选择的View,作为一个参数来调用该View下的dispatchKeyEvent函数。
当事件传递给获得焦点的View时,该View组件将执行适当的操作来改变软件应用中的某些状态。在MP3播放器软件应用中,比如,当用户触摸Play/Pause 按钮时,该事件将被传给相应的按钮对象,触发的Handler函数可能会更新Model,以播放当前选择的歌曲。
整合
我们现在已经熟悉了上面的概念。这些概念可以用来描述一个UI系统。当一个外部事件(例如用户滚动、拖拽和按键;来电呼入;或者MP3播放器到达终点)发生时,Android 系统将一个Event事件放入事件队列中,最后按照“先入先出”的顺序,依次从队列中取出,并分发给相应的Event Handler。该Handler的程序代码是整个应用的一个组成部分,负责响应此Event,具体的响应过程会通知相应的Model,告知其有一个状态改变了。对应的Model会执行相应状态改变的操作。
几乎Model的任何变化都将会要求相应的View做改变。对于一个按键事件,EditText组件会在插入光标点显示新键入的字符。对于一个通信簿应用,点击一个通信条目,会使该条目高亮显示,而先前高亮显示的条目会取消高亮显示状态。
为了更新界面显示,Model对象必须通知UI框架“一些显示区域的内容已经过时了,应该刷新”。这个redraw重画请求跟同一UI框架消息队列中的前一时刻的Event没有什么不同。redraw事件被按次序依次处理,跟其他UI事件没什么不同。
最后,此redraw事件被从队列中取出,并分发出去。相应的View提供了该事件的Handler。View树重新被绘制。View树中的每一个View对象都根据当前时刻的状态来绘制自身的屏幕区域。
为了更具体地说明上述内容,我们来分析一下一个MP3播放器应用。
1. 当用户点击Play/Pause 按钮图片时,框架产生一个新的MotionEvent,包含一些其他的数据,以及屏幕的点击坐标,然后框架将该Event放入事件队列的尾部。
2. 当此Event出现在事件队列的头部时,框架将其移出,然后把其传递给View树,遍历View树的各个View控件,最终将此消息传递给包含该点击位置的矩形区域的相应组件对象,也即Play/Pause widget按钮。
3. Play/Pause按钮的EventHandling程序代码告诉Core(Model)应该resume Playing a tune。
4. 应用程序的Model代码播放当前选择的曲目,同时,它发送一个redraw请求给UI框架。
5. redraw请求作为一个Event放入框架的消息队列中,供UI框架随后处理。
6. redraw消息处理完成后,屏幕被重新刷新,其中Play按钮将按照新的状态进行绘制。事件就是这样一个循环过程。
像Button和TextBox等UI组件对象实际上同时实现了View和Controller的方法。这仅仅是为了直观感。当你将一个Button加入到应用的UI中,你是希望该Button实现点击事件处理。尽管Button等UI对象同时实现了View和Controller,你仍然需要注意这部分View和Controller并不是直接交互的。Controller方法事实上决不应直接修改显示。将修改显示的部分留给这样的代码:实际更改状态,然后请求redraw,相信后继的屏幕刷新方法会使该Button反映它新的状态。这样的编码最大限度减少了同步问题,并且帮助你的程序更稳定。
对于Android UI框架,我们需要知道一点:它是单线程的。有一个单独的UI线程负责将Event从事件队列中移出,并且调用Controller的回调函数,渲染该View。这点有多种意义。
单独的UI线程同时也是一种最简单的并发处理机制,它不需要调用同步块去协调View和Controller的状态。这是很有价值的优化措施。
另一个单独的UI线程的好处是,它可以确保事件队列的先后处理次序,只有上一个事件处理完成了,才处理下一个事件。这听起来是想当然的事情,但是它的确使UI的代码更容易。当UI组件捕获了Event之后,它可以确认没有其他的UI处理线程会发生,直到它的处理结束。总而言之, UI回调函数是原子调用(不可中断)。
第三个好处是,只有一个线程负责事件队列的消息移出和分发。如果你的代码在该线程的执行中停止,不论任何原因,你的UI屏幕将会冻结。如果事件的响应是简单处理的话,比如:更新变量状态,生成新的对象等,最好最正确的做法是把相关代码放到主event线程中。如果这样,另一方面,Handler必须从某远程网络服务端获得应答,或者执行一个复杂的数据库查询,则全部UI将无法响应,直到请求完成。这绝对不是一个好的用户体验!长时间的任务操作必须单独放到另外一个线程中。