Android环境给Java生态系统增加了另一种图形用户界面(GUI)工具,AWT、Swing、SWT、LWUIT等GUI工具箱又新添了一员。如果你使用过其中一种,那Android UI框架看起来会很熟悉。和这些工具类似,Android环境中新增加的这种工具也是单线程的、事件驱动的,是构建在可嵌入组件库上的。
和其他Java UI框架一样,Android UI框架采用的也是如图6-1所示的常见的MVC模式(Model-View-Controller,模型-视图-控制器)进行组织的。它提供架构和工具来构建控制器以处理用户的输入(如按键和在屏幕上输入tab键),构建视图把图形信息渲染到屏幕上。
模型
模型(Model)是应用的核心,即应用真正要做的事情。例如,它可以是设备上的音乐数据库和播放音乐的代码;它也可以是联系人列表和给这些联系人打电话或发送信息的代码。模型是本书后面章节的一大主题。
图6-1:MVC(模型-视图-控制器)概念
虽然特定应用的视图和控制器必然反映了它们所基于的模型,但一个模型可能会在多个不同的应用中使用。例如,MP3播放器及把MP3文件转换成WAV文件的应用。对于这两种应用,模型都包括MP3文件格式。但是,前一个应用还包含了用户熟悉的Stop、Start和Pause控制按钮,还可以控制音调。后一个应用可能不会播放任何声音。相反,它包含对如比特率这类特征的控制。数据是模型的全部。
视图
视图(View)是模型的可视化形态。更通俗地说,视图是应用中负责渲染界面、播放音频、触摸反馈等功能对应的部分。Android UI框架的图形部分,在第8章将详细描述,其组成是一组视图类的子类,这些类按照树的形式被组织在了一起。在图形上,每个对象代表屏幕上的一个矩形区域,完全包含在其父节点的矩形区域中。树的根节点是应用窗口。
举个例子,一个MP3播放器可能有一个组件,这个组件会显示正在播放的音乐的专辑的封面,第二个视图显示当前播放的音乐的名字,而第三个视图则包含多个子视图,如播放、暂停和停止按钮。
UI框架通过遍历视图树来渲染屏幕,按照“前序遍历”的顺序逐个访问所有组件并请求各个组件对自己进行渲染。换句话说,每个视图都自己执行渲染操作,并要求其子孙也执行同样的渲染操作。当整棵树都执行完渲染后,树的叶子节点,即较小的、嵌套的组件,也是执行顺序较为靠后的渲染,会将处在根节点附近的组件压在底下,因为根节点附近的节点会先渲染,后渲染的组件会覆盖先渲染的组件。
Android UI框架实际上比上面这种简单的模式更为高效。如果它确定某个子节点会渲染某块区域,它就不会再渲染父节点。在不透明对象下面再执行渲染将是浪费的。对视图中没有变化的部分重新渲染,也将是浪费的。
控制器
控制器(Controller)是应用中负责响应外部动作的部分:按键、屏幕触摸、来电等。它是使用事件队列(event queue)实现的。每个外部动作都会被作为一个唯一的事件插入到事件队列中。框架按序从队列中取出事件并分发下去,同时也会从队列中把事件删除掉。
例如,当用户在手机上按下一个按键,Android系统会生成一个KeyEvent事件并把它加到事件队列中。最后,在之前已经插入队列的事件被处理完后,这个KeyEvent事件就会被从队列中删除并作为呼叫参数传递给当前选中的视图的dispatchKeyEvent方法。
一旦某个事件被分发到焦点组件,该组件会采取适当的动作改变程序的内部状态。例如,在MP3播放器应用中,当用户触摸屏幕上的Play/Pause按钮时,该事件会分发给该按钮对象,处理器方法可以更新模型,恢复播放之前选中的音乐。
本章后面会对如何构建Android应用的控制器进行介绍。
小结
现在,已经介绍了UI系统相关的所有概念。当触发外部动作时,如用户执行滚动、拖曳和单击按钮动作;来电;MP3播放器到达其播放列表的最后一个,Android系统会在事件队列中插入表示该动作的事件。最后,事件会被从队列中删除,遵循“先进先出”原则,并被分发到相应的事件处理器。事件处理器通常就是应用中的代码,对事件的响应方式是,通知模型某个状态发生了某种变化。模型会执行适当的动作。几乎所有的模型状态的变化都需要在视图中也有相应的变化。例如,要响应按键,EditText组件必须在插入点显示刚按下的字符。同样,在电话簿应用中,单击某个联系方式会高亮显示该联系方式,而之前选中的联系方式会被取消高亮显示。
当模型更新其状态时,几乎肯定会改变当前显示,以显示模型内部的变化。为了对显示进行更新,模型必须通知UI框架有些显示部分已经过期了,必须重新渲染。重新渲染请求实际上就相当于在该框架的事件队列中插入另一个事件,该事件队列即之前保存控制器事件的那个队列。重新渲染事件与其他UI事件被处理的顺序一样。
最后,重新渲染事件会被从队列中删除并被分发。重新渲染事件的事件处理器就是视图。视图树会重新渲染;每个视图负责渲染它当前的状态。
为了使得以上说明更具体,我们可以以一个MP3播放器应用为例来说明一下应用执行周期:
1.当用户触摸屏幕上的Play/Pause按钮图标时,Android GUI框架会创建一个新的MotionEvent,它包含该单击的屏幕坐标。框架会在事件队列的尾部插入新的事件。
2.正如P165“控制器”一节所描述的,当事件被插入到队列中后,Android GUI框架会删除该事件,并把该事件作为叶子节点插入到视图树中,该单击的屏幕坐标位置落在该叶子节点的矩形区域中。
3.因为按钮组件表示Play/Pause按钮,故应用按钮处理代码会告诉GUI框架核心(即模型)它应该重新播放该音乐。
4.应用模型代码开始播放选中的音乐。此外,它给UI框架发送重新渲染请求。
5.重新渲染请求被加入到事件队列中,按照P164“视图”一节所描述的步骤执行处理。
6.屏幕进行了重新渲染,Play按钮处于播放状态,一切都又同步完成。
UI组件对象,如按钮和文本框,实际上同时实现了视图和控制器。这是很有意义的。当你给应用的UI添加按钮时,你希望它在屏幕上显示,并且当用户单击时,能够执行某些操作。虽然UI的两个逻辑元素(视图和控制器)是在同一个对象中实现的,但应该注意的是,它们没有直接交互。例如,控制器方法永远都不应该改变显示方式,而是把改变显示留给改变状态并请求重新渲染的代码,相信在后面调用的渲染方法会使组件显示新的状态。这种编码方式最大限度地减少了同步问题,有助于保持程序健壮性并预防Bug的出现。
在Android UI框架中,还有很重要的一点是:它是单线程的。单线程从事件队列中删除事件,执行控制器回调并渲染视图。这一点非常有意义。单线程UI最直接的影响是,它不需要使用同步机制来协调视图和控制器之间的状态。这是一个很有意义的优化。
UI单线程的另一个优点在于,确保在应用的事件队列中,每个事件都会执行完毕,并且执行顺序和其入队列的顺序一致。这个优点不言而喻,它会使得UI编码简单得多。当调用UI组件处理事件时,UI框架会确保在该组件执行完毕之前,不会执行任何其他UI组件的处理。这表示如果一个组件多次发送程序状态变化请求(每个变化都会发送相应的屏幕重新刷新的请求),则会确保在程序完成处理、执行更新并返回结果之前,不会执行屏幕刷新。简而言之,UI回调是原子性的。
要谨记,负责从UI事件队列中删除和分发事件的线程只有一个的第三个原因是,如果你的代码由于某个原因长期占用该线程,则会导致UI僵死!如果组件对事件的响应是简单的,如仅仅改变变量的状态、创建新的对象等,则在主事件线程上执行该处理是完全没有问题的。但是,如果处理器必须返回远程网络服务的响应或者运行复杂的数据库查询,就会造成整个UI响应迟钝,直到请求结束为止。这绝对不会是一个良好的用户体验!长时间运行的任务必须委托给另一个线程,正如P186“高级连接:聚集和线程化”一节所述。