好的应用需要好的创意,就像APPstore里下一个最赚钱的付费应用一样. 这里使用一个绘图应用, 我们把它命名为TouchPainter. 该应用简单而全面, 可以展示各种设计模式.
我们将经过几个设计阶段,每个阶段都会提出与设计相关的需求,用例和问题. 在此期间, 将探讨各种可以解决设计问题以满足需求的设计模式. 请读者继续关注我的博文.
设计过程中有3个重要的里程碑:
- 想法的概念化
- 界面外观的设计
- 架构设计
从想法的概念化开始,我们将汇集有关TouchPainter应用的一些基本需求和用例,比如用户应该怎样使用此应用, 以及用户使用此功能时的体验.
有了好的idea后,接下来就是考虑应用的界面外观,界面外观的设计过程,让开发者得以探讨哪些UI要素可以合乎逻辑地组合在一起.这个过程不但让开发人员首先对哪些好看哪些难看有个整体认识,也可以精简一些冗余的UI元素,从而简化并增强用户体验.
上面的工作是一个反复的,渐进的过程,所以你设计的UI界面要易于修改和扩展.很多开发者会先用铅笔画出哪些不同视图可以放在一起.如果草稿看起来令人满意,开发人员或UI设计师可以开始用软件把线框和更具真实效果的UI组件(widget)组合在一起,以细化设计. 若是结果并不如愿,则要返回到纸面设计,或者在屏幕上修改widget. 接下来把界面外观的线框与需求相结合的时候,我们将探讨这一过程.
界面外观完成以后,就该确定一些影响应用程序架构的技术问题了.比如可以向这样的问题:"用户可以怎样打开涂鸦图? "
1.想法的概念化
任何类型的软件开发都要有一些需求,这个应用也不例外. 但并不需要在一开始就确定全部细节. 我们要做的是从最基本的开始.那么,第一个需求是什么呢?
- 1.可以用手指涂鸦的画板
如果只能绘制黑白图形,我觉得任何用户对这种绘图体验都不会满意. 要是有设定不同颜色和线条粗细的选项肯定很不错. 这样又有了一个需求:
- 2.用户可以改变线条颜色和粗细
但只允许用户进行涂鸦并不够,还应该允许用户保存涂鸦图,所以得到了这个需求:
- 3.允许用户保存涂鸦图.
要是用户不能打开保存的涂鸦图并作修改,保存将毫无意义. 所以又有了这需求:
- 4.允许用户打开保存的涂鸦图
要是用户不喜欢自己的作品,想删除重来怎么办?
- 5.允许用户删除当前涂鸦图
要是允许用户撤销与恢复涂鸦肯定很不错.所以又得到了一个需求:
- 6.允许用户撤销和恢复涂鸦
这个清单可以一直列下去,但眼下已经有了可以启动设计的基本需求. 但在进入设计阶段以前,应该确定把握了应用程序的界面外观,以保证我们很清楚应用程序是什么样子.来总结一下这个了不起的绘图应用的第一批需求吧
- 7.可以用手指涂鸦画板
- 8.用户可以改变线条颜色和粗细
- 9.允许用户保存涂鸦图
- 10.允许用户打开保存的涂鸦图
- 11.允许用户删除当前涂鸦图
- 12.允许用户撤销和恢复涂鸦.
2.界面外观设计
有没有这样过体验,就是在最后一刻还要修改应用程序用户界面(或用户体验),是不是很操蛋,感觉很不爽!所以更好的做法是至少在iOS的开发中,是尽早设计接近最终产品的完整界面外观与用户体验. 有人称之为界面外观驱动的(Look-and-Feel-driven)设计. 大家已经听说过数据驱动的设计,事件驱动的设计和测试驱动的设计.但是这些都只是针对技术细节.然而,界面外观驱动的设计让我们能在一开始就专注于用户体验.这样不只是能够为发布前的最后关头省下时间,而且也能够确保利益相关者与开发人员在开发过程中保持一致.即使在开发过程中,做好的应用程序界面外观和UI设计,可以给忙于编码的开发人员很好的视觉提示,告诉他们是在开发什么东西.这样可以提高生产效率,因为可以在初期就发现那些难以定位的bug和设计缺陷.
第一个需求: 可以用手指涂鸦的画板.
如图1-1就是我们的应用程序的第一个界面设计
图2-1.针对第一个需求的主画布视图的线框图
在视图底部有一个工具条,上面有6个按钮,从左至右依次为:删除,保存,打开(保存的涂鸦图),设置(线色和线宽),撤销和恢复(屏幕上所绘的图).
线框图看起来就像一个典型的允许用户用手指绘图的iPhone应用.用户也可以改变其他与绘图相关的设置.目前我们对这个外观很满意.接着我们来看针对其他需求的下一个线框图
这个线框图中,用户可以通过改变各个颜色分量与线宽的滑动条来调节线色与线宽.画面中间的灰色矩形区域将根据所选的RGB值显示当前的线色. 调节画面底部的滑动条可以设定线宽.这个线框图实现了第二需求:用户可以改变线条颜色和粗细.按Done 按钮可以回到图2-1所示的主画布视图.
至此,可以肯定已有的线框图实现了6个需求中的5个.只剩下了打开保存的涂鸦图的第4个需求.对于这个需求,首先要问,用户如何才能知道要打开什么涂鸦图呢? 肯定需要某种浏览器,让用户可以浏览全部的涂鸦图,然后从中选择想要的涂鸦图. 可以把它画成一个缩略图视图.
缩略图视图的粗略线框图如图下图所示:
图2--3.实现第4个需求缩略图视图的线框图
当用户单击主画布视图的打开涂鸦按钮时,会打开如图2-3所示的缩略视图.用户可以通过在画面上下滑动来滚动涂鸦缩略图的列表.用户也可以单击缩略图,在画布视图中打开它,以继续绘图. 或者,通过单击Done按钮来返回到主画布视图.
如果需要的话,可以以后再进一步完善设计,目前我们对界面外观的线框图很满意. 开始进行下一步----框架设计吧.
3.框架设计
我们要列出并考虑通过细化原原始需求而得出的一些问题. 每个问题都有与原问题相关的细化而特定的特征或子问题.得出4个主要问题及其细化特征如下:
- 视图管理
- 从一个视图到另一个视图的迁移
- 使用中介者来协调视图迁移
- 如何表现涂鸦
- 在屏幕上可以画 "什么"
- 用组合结构来表示痕迹(mark)
- 绘制涂鸦图
- 如何表现保存的涂鸦图
- 获取涂鸦图的状态
- 恢复涂鸦图的状态
- 用户操作
- 浏览涂鸦缩略图的列表
- 涂鸦图的撤销和恢复
- 变更线色与线宽
- 删除屏幕上的当前涂鸦图
视图管理
第1章中讨论过模型-视图-控制器模式. 模型表示视图所展示的数据. 控制器在视图与模型之间起协调作用. 因此每个控制器"拥有"一个视图和一个模型. 在iOS开发中,这种控制器称作视图控制器(view controller). 根据前面的线框图,我们清楚TouchPainter需要什么. 共有3个视图,每个视图应该由相应的控制器来维护. 所以基于最初的UI设计,有3个控制器:
- CanvasViewController
- PaletteViewController
- ThumbnailViewController
CanvasViewController包含了图1-1所示的主画布视图,用户可以用手指在该视图中涂鸦.
PaletteViewController管理一组用户控件元素,让用户可以调节线色与线宽,如图1-2所示.新的设定会传递给CanvasViewController的模型.ThumbnailViewController以缩略图的形式展示先前保存的全部涂鸦图,用户可以浏览并单击以打开涂鸦图,如图1-3所示. 有关涂鸦图的全部必需信息会传递给CanvasViewController,以将涂鸦图显示在画布视图.
各个视图控制器之间有交互. 它们彼此紧密依存. 情况会变的一团糟,尤其是在以后要往应用程序中加入更多视图控制器的时候.
1.从一个视图到另一个视图的迁移
当用户单击CanvaseViewController的调色板按钮时,视图会替换为PaletteViewController的视图. 同样, 单击CanvasViewController的打开缩略图视图的按钮, 会打开ThumbnailViewController的视图. 当用户单击导航条上的Done按钮以结束当前操作时,会回到CanvaseViewController的视图.图1-4显示了控制器之间可能的交互.
看着图1-4,读者可能会这么想:"这没什么问题. 我一直这么做,它也总能正常工作."但其实上,对多数应用程序而言这并不是好的设计. 常见的iOS应用程序中,像图1-4那样的视图迁移并不显得很复杂,尽管这些控制器之间有一定的依存关系. 如果视图与其控制器之间彼此依存, 应用程序就不好扩展(scale). 而且,如果修改视图变换的方式,它们的代码变更都将不可避免. 其实,依存关系不仅限于控制器, 也包括了按钮. 按钮间接地与某些视图控制器发生关联. 如果应用程序变大变复杂, 依存关系将无法控制,随之而来的是大量难以理解的视图迁移逻辑. 需要一种机制来减少不同视图控制器与按钮建的交互, 以降低整体结构的耦合,提高其可复用性与可扩展性. 像视图控制器那样的协调控制器会有助于此, 但其作用不是协调视图与模型, 它要协调不同的视图控制器,以完成正确的视图迁移.
图2-4.典型的iOS应用程序设计中, CanvasViewController,PaletteViewController与ThumbnailViewController之间的依存关系图2.使用中介者来协调视图迁移
中介者模式是指用一个对象来封装一组对象之间的交互逻辑. 中介者通过避免对象间显式的相互引用来增进不同对象间的松耦合(loose coupling). 因此对象间的交互可以集中在一处控制,对象间的依存关系会减少. 新交互模式的结构如图1-5所示.
向架构中引入了新成员CoordinatingController之后,以后对架构的变更将会容易很多.各种视图控制器与按钮器与按钮发出视图变换的请求, 而CoordinatingController则封装了协调这些请求的逻辑. 交互不仅限于视图迁移,也可以是信息传递和对操作的调用. 要是以后其他任何视图控制器. 在这种结构下,CoordinatingController认识交互图中的每个对象. 单击按钮会触发对CoordinatingController的调用, 请求视图迁移. 根据在按钮中保持的信息(比如标签),CoordinatingController知道按钮想要打开什么视图. 如果想复用其中任何一个视图控制器,也不必一上来就考虑如何把他们联系在一起.
现在,CanvasViewController, PaletteViewController, ThumbnailViewController以及它们的按钮的行为就像在管弦乐队中演奏的不同乐手一样. 演奏一首乐曲, 乐手之间并不相互依赖, 只是按钮着指挥的指令在什么时候做什么. 你能想象出没有指挥的管弦乐队中演奏的不同乐手一样. 演奏一首乐曲,乐手之间并不相互依赖,只是按着指挥的指令在什么时候做什么.你能想象出没有指挥的管弦乐队是什么样子吗?
图2-5 引入了中介者CoordinatingController之后,视图控制器及其按钮之间的依存关系变少了如何表现涂鸦
当用户触摸画布视图时,位置等触摸信息会被收集.需要某种数据结构来组织屏幕上的触摸,以便将所有触摸聚集为一个实体来进行管理. 尤其是在以后需要解释这些数据的时候, 数据结构必须非常有条理并非可预测.
1.在屏幕上可以画"什么"
在考虑用于保持触摸数据的可能的数据结构之前,先来看看看应该如何定义线条.
如果用户先触摸屏幕然后移动手指,就生成一个线条. 然而,如果手指并未移动而在起始位置就结束了触摸,就应该视为一个点.如下图:
凭直觉会认为点是单一位置, 而线条在轨迹上有多个位置.线条本身是包含了一串触摸位置实体. 这里的问题是如何将两种类型的实体当做单一类型来管理. 是否可用认为线条包含多个点? 如果线条使用数组来保持所有点,那么是否可用线条结构来既表示点又表示线条?可是那样的话,对于点的情况,会由于使用了仅有点的数组而浪费内存. 最好的情况是使用一种设计模式达到两全其美.
在研究如何能够使用一个数据结构来表示线条和点之前,先来考虑一下如何在屏幕上画点和线. 如Cocoa Touch框架包括了一个叫Quartz 2D的框架,它提供了在UIView上画二维图形的API,包括线和各种多变形.
要在UIView上绘图,需要得到运行库提供的绘图上下文(drawing context). 例如,如果想画一个点, 就要把绘图上下文和表示点大小的CGRect----包含宽/高以及位置信息,传给Quartz 2D的函数CGContextFillEllipseInRect(). 如果画线,首先需要通过调用函数CGContextMoveToPoint()让上下文移到第一点. 把第一点赋给上下文之后,要通过对CGContextAddLineToPoint()的连续调用, 添加组成线的其余点. 添加完最后点后,将调用CGContextStrokePath()来完成画线. 这样,这些点就连接为一条线,画在UIView之上. 下图是对此过程的直线描述.
图2-7 使用Quartz 2D画点和线条的直观描述
2.用组合结构来表示痕迹
当然,可以使用所知的任何数据结构来存储线条和点等. 但是,如果全部都用(比如说)多维数组来保存,使用和解析是就需要进行很多类型检查,而且,要保持结构可靠而一致,需要进行大量调试工作. 如果要用一种结构既可以保存独立的点,又可以保存把点(顶点)作为子节点的线条,面向对象的做法是使用树. 树的表现形式,能够把线条与点这样的复杂对象关系组织与局限于一处. 解决这种结构问题的一种设计模式叫做组合模式.
通过组合模式, 可以把线条与点组合到树形结构中,以便统一处理每一节点. 线条(stroke),点(dot)和顶点(vertex)的直观结构如下图所示.
图2-8 点与线条的组合结构
点是叶节点, 是独立的实体. 线条是组合体, 包含了其他点作为顶点,同时,也可以包含其他线条组合. 它们是很不一样的实体. 想要它们每个相同的类型, 那么就需要将其泛化(generalize)为共同的接口. 因此, 不管每个组件实际是什么具体类型, 通用类型还是相同的. 通过这种方案, 在使用它们的时候就可以对其统一对待.
如果组件只是一点, 那么它会表现为一个实心圆, 在屏幕上代表一个点. 但是,如果是应该作为一组实体连接起来的一串点(顶点), 就会被绘制成连接起来的线(线条).这就是两者的区别.
不论线条或点, 其实都是介质上的某种"痕迹"(Mark), 这里的介质是屏幕, 因此我们添加Mark作为Vertex,Dot与stroke的父类型. 它们之间的关系如下图中的类图所示:
图2-9 组合结构的Mark模型的类图,Dot与Stroke为具体类父类型Mark是一个协议. 协议是Objective-C的一个特性, 定义了行为的合约而没有实现. 具体类实现Mark协议声明的接口. Vertex,Dot和Stroke都是Mark的具体类. Vertex与Dot都需要位置信息,而Dot需要颜色和大小的附近属性,所以Dot作为子类继承Vertex.
Mark为所有具体类定义了属性和方法. 所以, 当客户端基于接口来操作具体类的时候, 可以统一对待每个具体类. Mark对象有这样的方法, 可以把其他Mark对象加为自己的子节点, 形成组合体. Stroke实现了把其他Mark对象加为子节点的方法. Mark协议中还定义了与子节点管理有关的一些其他操作, 比如移除子节点/按照序号(index)返回特定子节点, 以及返回子节点数和字节点列表中的最后一个子节点.
3.绘制涂鸦图
现在我们建好了一个数据结构, 它可以让应用程序以一种合理的方式管理由用户触摸生成的点. 在屏幕上显示的时候,触摸的位置信息就非常重要. 可是在上面讨论组合结构并没有在屏幕上描画自身的算法.
大家知道, 在UIView上绘制定制图形的唯一方式是重载其drawRect:实例方法. 这个方法会在请求视图更新时被调用. 例如,可以向UIView发一个setNeedsDisplay消息.然后它就会调用定义了定制绘图代码的drawRect:方法.然后我们就能以当前图形上下文(graphis context)作为CGContextRef, 在此方法调用中绘制想要的图形. 下面说明如何使用这一机制来绘制组合结构.
可以向Mark协议添加一个绘图操作, 比如drawWithContext:(CGContextRef)context,以使每一节点能够按其特定目的来绘制自身. 可以把从drawRect:方法中得到图形上下文传递给代码清单如下:
//Dot 中的drawWithContext:实现
- (void)drawWithContext:(CGContextRef)Context {
CGFloat x = self.location.x;
CGFloat y = self.location.y;
CGFloat frameSize = self.size;
CGRect frame = CGRectMake(x - frameSize / 2.0,y - frameSize / 2.0, frameSize, frameSize);
CGContextSetFillColorWithColor(context, [self.color CGColor]);
CGContextFillEllipseInRect(context, frame);
}
在当前上下文中, 能够按照位置,颜色和大小绘制一个椭圆(点).
至于顶点,它只提供了线条中的一个特定位置. 因此,Vertext对象将只在上下文中使用自身的位置(坐标)往线添加一点(好吧,按照Quartz 2D的说法, 是往点上添加一条线),如代码清单:
//Vertex中的drawWithContext:实现
- (void)drawWithContext:(CGContextRef)context {
CGFloat x = self.location.x;
CGFloat y = self.location.y;
CGContextAddLineToPoint(context, x,y);
}
对于Stroke对象, 它需要把上下文移至第一点,向每个子节点传递同样的drawWithContext:消息和图形上下文, 并设定其线色. 然后以Quartz 2D函数CGContextSetStrokeColorWithColor与CGContextStrokePath结束整条的绘制操作,如代码下面清单:
// Stroke中的drawWithContext:实现
- (void)drawWithContext:(CGContextRef)context {
CGContextMoveToPoint(context, self.location.x, self.location.y);
for (id<Mark> mark in children_) {
[mark drawWithContext:context];
}
CGContextSetLineWidth(context,self.size)
CGContextSetLineCap(context,kCGLineCapRound);
CGContextSetStrokeColorWithColor(context,[self.color CGColor]);
CGContextStrokePath(context);
}
设计Mark协议的主要挑战是以最小的操作集提供可扩充的功能. 为添加新功能而对Mark协议及其子类做手术, 不但有创伤而且易于出错. 最终,对类的理解/扩展与复用变得更加困难. 因此, 关键是要致力于简单而一致的接口的一组充分的基本要素.
扩展如Mark这样的组合结构的行为, 另一种方法是使用称为访问者模式的设计模式. 访问者模式(后续文章会讲到)允许将可应用于组合结构的外部行为定义为"访问者". 访问者"访问"复杂结构中的每一节点,根据被访问的节点实际类型,执行特定的操作.
3.如何表现保存的涂鸦图
内存中一个涂鸦图的表示形式是一个组合对象, 它包含点与线条的递归结构. 但是如何才能把这一表示形式保存到文件系统呢? 两个地方的表示形式应该兼容,也就是说,一种表示形式能够毫无问题地转换为另一种.
如果不使用结构化而简洁的机制保存对象, 代码就会变得一团糟,尤其是在硬编码于一处的时候. 可以把一般的对象保存过程分解为多个步骤,如下所示:
(1)把对象结构序列化成结构化的文件快blob(binary large object,指存储二进制数据的单一实体)
(2)构建要保存blob的文件系统中的路径
(3)像普通文件一样保存blob
至于从文件系统加载同一blob并复原为原先的对象结构,需要执行同样步骤,但是以相反的顺序:
(1)重建文件系统中保存blob的路径
(2)像普通文件一样从文件系统加载blob
(3)反序列化并恢复原先的对象结构
如果把这些步骤都放进一个巨大的函数,让它处理所有琐事,将很难管理和复用.而且,有时关心的不只是整个对象结构的保存,也有部分(比如,只是最后的修改)的保存. 肯定需要某种封装化的操作去处理涂鸦图状态保存中遇到的各种类的特殊情况.
先不管路径的构建之类,只考虑获取内存中复杂对象的状态并转换成封装好的表示形式,以便将其保存到文件系统并在以后恢复.
可以使用成为"备忘录"(Memento)的设计模式来解决这类问题. "备忘录"允许对象按其想要的任何(或者任意复杂的)方式将自己的状态保存为一个对象,根据此模式这个对象称为备忘录对象. 然后某个其他对象,比如看管人(caretaker)对象,将备忘录对象保管在某处,通常是文件系统或内存中. 看管人对象不知道有关备忘录对象任何细节的格式. 一段时间之后,接受到请求时,看管人对象将备忘录对象传回给原来的对象,让它根据在备忘录对象中保存的信息恢复其状态. 如下图显示了他们的交互顺序.
图2-10 anOriginator对象接受aCaretaker的指令,获取自身的状态为aMemento的时序图---aCaretaker管理aMemento并在后来将其传回给anOriginator, 以使anOriginator恢复其状态
1.获取涂鸦图的状态
根据我们的问题,需要将原始的对象结构与其被保存的状态分离. 不过在深入细节之前, 要想好需要归档并在以后反归档哪些东西. 在图2-3中,我们草拟了一个用户界面, 它允许用户浏览已保存涂鸦图的缩略图. 除了要保存Mark对象之外, 存储过程中也需要保存相应的画布视图截屏图.
而且,可以决定归档更多要素,如画布和其他用于绘图的属性. 随着不断推出这个程序的新版本,情况可能会发生变化. 出于灵活性的考虑, 我们决定创建另一个类来管理这些附加信息. 我们把这个新类叫做Scribble. Scribble对象封装了Mark组合对象的一个实例, 作为其内部状态. Scribble的作用就好像第一章介绍的模型-视图-控制器范例中的模型. 在这个问题中,需要Scribble对象参与Mark组合体状态的保存与恢复过程, 而不是直接使用Mark. Mark很难直接使用, 因为它只提供了对组合结构的基本操作.
Scribble系统的模型, 此外,还需要看管人保存Scribble对象的状态. 我们添加一个起此作用的类,并把它加做ScribbleManager. 保存涂鸦图的实际过程可能非常复杂,并且涉及很多其他对象,因此本节的其余部分将只讨论Scribble对象内部状态的保存.
如图2-11所示, CanvasViewController向ScribbleManager发起保存涂鸦图的调用. 然后ScribbleManager请求传进来的Scribble对象创建"备忘录". 接着,Scribble对象创建一个ScribbleMemento的实例并用其保存其内部的Mark引用. 此时, ScribbleManager既可以将返回的备忘录对象放在内存中, 也可以将其保存到文件系统. 此时, ScribbleManager既可以将返回的备忘录对象放在内存中, 也可以将其保存到文件系统. 在这个"保存涂鸦图"的过程中,要让ScribbleManager把备忘录对象保存到文件系统, 所以ScribbleMemento对象需要把自身编码成一种数据形态,即NSData实例. ScribbleManager对此数据一无所知, 只是用自己才知道的路径将其保存于文件系统. 整个过程就像传递装有备忘录对象的黑箱, 箱子只能由发起对象打开和关闭. 整个过程开始于从CanvasViewController到ScribbleManager的一个消息调用----saveScribble:scribble.
选用备忘录模式作为对象归档方案,至少有以下两点好处.
- ScribbleMemento对象的内部结构细节没有暴露出来. 如果以后要修改Scribble对象对其状态的保存内容和保存方式,不用修改应用程序中其他的类.
- ScribbleManager不知道如何访问ScribbleMemento对象的内部表示(及其编码的NSData对象).相反, 它只是在其内部定义的操作之间传递这个对象, 以将其保存到内存或文件系统. 任何对备忘录保存方式的修改都不会影响其他类.
2.恢复涂鸦图的状态
我们知道了如何将scribble对象的状态保存为备忘录对象,那么,怎样通过同样的机制恢复scribble对象呢?加载部分很像倒过来的保存过程。客户端(此处不必是原来的canvasViewControl1er)告诉scribbleManager的实例加载哪个特定的Scribble,比如可以通过使用索引来指定。然后ScribbleManager对象重建用于保存原ScribbleMemento对象的路径,并从文件系统将其加载为一个NsData实例。scribbleMemento类将数据解码并重新生成ScribbleMemento实例。ScribbleManager将备忘录传递给scribble类。Scribble类使用备忘录通过其中存储的Mark引用来恢复原先的Scribble实例. 先前被归档的Scribble对象恢复后, 被返回给客户端. 这样就结束了, 如图2-12所示
此时,对于加载过程可能会有疑问:客户端如何知晓首先要加载哪个涂鸦图呢?因此下一步将创建一个视图通过它用户可以浏览已保存的全部涂鸦图的缩略图。在用户单击之后,其中一个涂鸦图将通过canvasview的控制器CanvasViewcontroller,在canvasView中打开,使用缩略图的对应索引号告诉ScribbleManager要打开哪个涂鸦图。
4用户操作
前面几节讨论的是基础性或者说架构性的问题. 还有些与应用程序的用户体验或用户期望(user expectation)相关的其他问题--比如如何浏览涂鸦缩略图的列表, 涂鸦图的撤销与恢复(undo/redo), 改变线色与线宽以及删除屏幕上的当前涂鸦图. 以下各小节将讨论这些问题.
1.浏览涂鸦缩略图的列表
现在我们知道了如何保存涂鸦图及其缩略图, 那么, 怎样才能浏览并从中选择一个打开呢? 我们来回顾以下图2-3中的缩略图视图的UI线框图; 缩略图视图将全部已保存的涂鸦图显示为一个个的缩略图, 以便浏览. 当很多缩略图争先恐后地在视图上显示时, 会让我们想到它们可能阻碍应用程序的主线程. 那样就会降低用户操作的响应性.
应该采用更好的方法, 让每个缩略图在后台线程加载实际图像, 而不是让主线程去一个接一个地处理大量图像加载操作. 这样, 大批的缩略图各自忙于自身的处理的同时, 响应性得以改善. 但有个问题, 缩略图自身的线程排队等待其他线程结束的时候, 会怎么样呢? 用户何时能看到缩略图呢? 当缩略图在等待加载实际图像是, 需要显示某种占位图像(placeholder image). 一旦实际图像加载完毕, 就会取代占位图像进行显示. 这形成了一致而可预期的用户体验. 图2-13显示了操作中的缩略图视图, 部分填充了已加载的涂鸦图图像.
图2-13 填充了占位图像与加载完毕的涂鸦缩略图的缩略图视图
怎样才能设计一个能做到这一点的类呢? 其实, 有一种叫做代理的设计模式, 代理是实际的资源或对象的占位或替代品. 代理模式的一个特点是通过虚拟代理(virtual proxy)在接到请求时实现重型(heavy-weighted)资源的懒加载(lazy-load).
网友评论