Design and Implementation of Mobile Device-oriented Vector Drawing Platform
引用本论文: 张云贵. 面向移动设备的矢量绘图平台设计与实现[D]. 北京:北京理工大学软件学院, 2013.
本论文的相似度为0%,是源创论文。欢迎评阅讨论,请勿抄袭,如需更多资料请在博客留言。
如果在研究或论文中使用到,欢迎回复或私信你的学校、姓名、研究领域,并在论文中添加引用或致谢。感谢你对开放成果的尊重和鼓励。
第4章 iOS绘图平台的实现
本章阐述了iOS绘图平台的实现方法,主要是在跨平台内核的基础上实现iOS上的画布适配器和视图适配器,对图形显示优化技术进行实验研究。
4.1 基于Quartz 2D实现画布适配器
4.1.1 画布原语与Quartz 2D的映射
在iOS上(Xcode开发环境)基于Quartz 2D图形库实现了画布适配器类GiQuartzCanvas。该类实现了画布接口GiCanvas,将其画布原语映射到Quartz 2D的绘图函数,如表4‑1和表4‑2所示。
这些画布原语多数直接使用Quartz 2D的函数(CG开头的函数)实现,图像和文字使用了UIKit框架的UIImage、UIFont类以及Foundation框架的NSString文字显示函数。因为UIKit封装了图像和文字的常用功能函数,所以使用该框架能简化实现。
表4‑1 画布原语映射到Quartz 2D
画布原语 |
测试号 |
Quartz 2D函数映射 |
drawRect |
b |
CGContextFillRect、CGContextStrokeRect |
drawEllipse |
c |
CGContextFillEllipseInRect、CGContextStrokeEllipseInRect |
beginPath |
多个 |
CGContextBeginPath |
moveTo |
多个 |
CGContextMoveToPoint |
lineTo |
e |
CGContextAddLineToPoint |
bezierTo |
f |
CGContextAddCurveToPoint |
quadTo |
g |
CGContextAddQuadCurveToPoint |
closePath |
e |
CGContextClosePath |
drawPath |
多个 |
CGContextDrawPath |
drawHandle |
i |
CGContextDrawImage、CGContextConcatCTM、[UIImage CGImage] |
drawBitmap |
i |
CGContextDrawImage、CGContextConcatCTM、[UIImage CGImage] |
注:其中的测试号为图4‑4中的测试子图号。
表4‑2 其他画布原语与Quartz 2D的映射
画布原语 |
测试号 |
Quartz 2D函数映射 |
setPen |
多个 |
CGContextSetRGBStrokeColor、CGContextSetLineWidth CGContextSetLineDash、CGContextSetLineCap |
setBrush |
多个 |
CGContextSetRGBFillColor |
clearRect |
a |
CGContextClearRect |
saveClip |
h |
CGContextSaveGState |
restoreClip |
h |
CGContextRestoreGState |
clipRect |
h |
CGContextClipToRect |
clipPath |
h |
CGContextClip |
drawLine |
d |
CGContextBeginPath、CGContextMoveToPoint CGContextAddLineToPoint、CGContextStrokePath |
drawTextAt |
j |
[NSString drawAtPoint:]、[UIFont systemFontOfSize:] |
在实现这些函数时,本文对下列内容进行了特殊处理。
(1)设置虚线模式(CGContextSetLineDash)时需要与线宽(如果大于1)成正比例,以保持线型与形状的比例。针对手绘应用,本文将线型与线端类型按图4‑1搭配使用:a、为了让线宽较大的短线更像一个圆点,对于实线的线型使用圆端的线端类型;b、为了让点划线等虚线类型的线条的空白间隙整齐,对于所有的虚线类型搭配使用平端的线端类型。
图4‑1 线型与线端搭配的效果
(2)使用CGContextDrawPath函数进行绘制并描绘闭合路径时,不能分两次调用分别描边和填充,因为该函数执行完后会清空当前路径(CGContextClip也一样),需要以参数kCGPathFillStroke或kCGPathEOFill同时填充和描边。
(3)使用CGContextDrawPath进行填充时,参数应设置为kCGPathEOFill或kCGPathEOFillStroke(以奇偶规则填充),不能设置为kCGPathFill或kCGPathFillStroke,否则难以在教材页面上显示如图4‑2所示的中间透明的环形。原因是一个路径的子路径是分别填充的,为了避免环形中心被填充,需要采用kCGPathEOFill或kCGPathEOFillStroke填充路径。
图4‑2 以奇偶规则填充环形
4.1.2 画布适配器的跨平台单元测试
本文采用测试驱动开发方式逐步实现了画布适配器,设计结构如图4‑3所示。单元测试类TestCanvas是跨平台内核中的C++类。
图4‑3 画布适配器的测试结构
本设计结构的创新点:(1)经实验证明在跨平台内核中可以使用C++在iOS上绘图。(2)TestCanvas类可以替换为图形实体类或绘图命令类,替换后不影响设备平台相关的画布适配器和视图类。因此,本设计结构易于扩展和移植。
在单元测试类TestCanvas中使用随机函数绘制各种图形,在iPad上的测试图形效果如图4‑4所示。其中,井字格的背景用于表示绘图视图为透明视图。因为通常图文批注等绘图应用需要在宿主页面上显示图形,所以将绘图视图设置为透明视图。各个子图分别为相应的画布原语的单元测试结果,对应关系见4.1.1节。
图4‑4 iPad上的画布适配器的测试效果
4.1.3 图像的矢量化显示
使用UIKit框架的UIImage类显示图像,图像的显示接口函数定义为:
void drawBitmap(const char* name, float xc, float yc, float w, float h, float a)
其中,使用名称name标识图像对象,(xc,yc)为图像的中心显示位置,w和h为显示目标宽高,a为旋转的角度(世界坐标系中的逆时针方向)。
按照图4‑5所示的矩阵变换过程使用Quartz 2D显示图像,算法过程如下:
(1)Y上下颠倒,原点移到(xc,yc),即计算矩阵:
af = CGAffineTransformMake(1, 0, 0, -1, xc, yc) (4-1)
(2)以原点为中心旋转a角度,即计算矩阵:
af = CGAffineTransformRotate(af, a) (4-2)
(3)使用CGContextConcatCTM应用变换矩阵af;
(4)显示图像充满到矩形CGRectMake(-w/2, -h/2, w, h);
(5)还原矩阵,即再应用(3)中矩阵的逆反矩阵。
图4‑5 图像显示的矩阵变换过程
4.1.4 图像资源的管理
由应用程序添加UIImage对象,在GiViewController类(见4.3.1的说明)中缓存UIImage对象及标识名称。应用程序在切换到后台或接收到内存紧张通知时,调用绘图平台的释放缓存函数,这些图像对象就释放掉。
在需要显示时由GiViewController根据标识名称调用getImageShapePath函数从标识名称得到实际图片文件的地址,重新加载图像。应用程序可重载GiViewController的getImageShapePath函数指定不同的存放地址。
4.1.5 控制点的图像显示
iOS绘图软件通常使用图像显示控制点,本文按下面方式实现图4‑6的效果。
图4‑6 控制点图像显示效果
(1)在跨平台内核中调用drawHandle函数显示控制点,该函数的定义为:
void drawHandle(float x, float y, int type)
其中,(x,y)为控制点坐标,type为控制点的图像类型。例如,“1”表示普通点的蓝色圆点图像,“2”表示热点的红色圆点图像。
(2)在iOS画布适配器的drawHandle实现函数中,根据type自动从程序资源中加载和缓存圆点图像(UIImage对象)。
(3)因为绘图上下文的单位为点,所以将图像宽高转换到点单位,记为w和h。
(4)按照图4‑7所示的矩阵变换过程使用Quartz 2D显示图像,应用变换矩阵:
af = CGAffineTransformMake(1, 0, 0, -1, x – w/2, y + h/2) (4-3)
图4‑7 控制点显示的矩阵变换过程
(5)显示图像充满到矩形CGRectMake(0, 0, w, h)。
(6)还原矩阵,即再应用(4)中矩阵的逆反矩阵。
4.2 显示优化技术研究
4.2.1 基于位图的双缓冲绘图
本节通过实验证明了基于位图的双缓冲绘图技术不适合iOS设备。双缓冲绘图技术是传统的绘图优化技术,主要涉及两种位图的用法:(1)缓存位图(Cached Bitmap),是图形显示内容的快照,下次视图重绘时直接显示该位图,以免重新显示图形费时。(2)缓冲位图(Buffered Bitmap),用于避免直接在目标上下文中绘图引起的逐步显示闪烁问题,先绘制图形到缓冲位图,然后一次性复制到目标上下文。
本文使用Quartz 2D进行双缓冲绘图技术实验,按照是否有缓冲位图、缓存位图重建和重绘、图形规模差异进行组合实验,在iPad 3上的实验结果见表4‑3,其中的六列的含义如下。
表4‑3 在iPad 3上的双缓冲绘图时间(毫秒)
无缓冲 重建缓存 |
无缓冲 4倍图形 |
无缓冲 重绘缓存 |
有缓冲 重建缓存 |
有缓冲 4倍图形 |
有缓冲 重绘缓存 |
|
创建位图上下文 |
- |
- |
- |
0.1 |
0.1 |
0.1 |
清除背景 |
- |
- |
- |
21 |
20 |
20 |
显示缓存位图 |
- |
- |
46 |
- |
- |
46 |
重建缓存位图 |
33 |
32 |
- |
1 |
1 |
- |
应用缓冲位图 |
- |
- |
- |
64 |
76 |
64 |
重新显示图形 |
71 |
260 |
- |
71 |
261 |
- |
drawRect总计 |
108 |
297 |
47 |
168 |
411 |
138 |
(1)无缓冲、重建缓存:直接在当前图形上下文(视图上下文)上绘制图形,缓存位图需要重新生成,当视图第一次显示或图形改变后刷新显示时属于该条件。
(2)无缓冲、4倍图形:在上面(1)显示条件的基础上,显示4倍的图形量。
(3)无缓冲、重绘缓存:直接在当前视图上下文上绘制,一次性显示缓存位图,不重新显示图形。
(4)有缓冲、重建缓存:使用缓冲位图绘图,缓存位图需要重新生成,当视图第一次显示或图形改变后刷新显示时属于该显示条件。
(5)有缓冲、4倍图形:在上面(4)的显示条件基础上,显示4倍的图形量。
(6)有缓冲、重绘缓存:先在缓冲位图上下文中显示有图形内容的缓存位图,不重新显示图形,然后将缓冲位图显示到视图上下文。
表4‑3中的各个评测参数解释如下:
(1)创建位图上下文:使用CGBitmapContextCreate函数创建缓冲位图上下文,需用将坐标系由默认的LLO坐标系改为ULO坐标系、计算位图宽高需用考虑到屏幕放大比例(即将视图的点单位转换为像素单位)。
(2)清除背景:使用CGContextClearRect函数将显示区域填充为透明背景。
(3)显示缓存位图:使用CGContextDrawImage函数显示已创建的缓存位图。因为Quartz 2D内部坐标系是LLO类型,图像的Y轴正方向朝上,显示前需用将绘图上下文的当前转换矩阵上下临时颠倒为LLO坐标系。
(4)重建缓存位图:使用CGBitmapContextCreateImage函数对绘图上下文创建一个快照图像,图像包含了各种图形的显示内容。
(5)应用缓冲位图:首先使用CGBitmapContextCreateImage函数从缓冲位图上下文生成快照图像,然后显示到视图上下文中,显示前将当前转换矩阵上下颠倒。
(6)重新显示图形:使用画布接口显示所有图形。
(7)drawRect总计:以上显示工作都是在视图的drawRect函数中进行的,此处统计了所有显示工作的时间。
本文对表4‑3的显示时间进行对比分析,得出下列结论:
(1)在iOS设备上无需使用基于缓冲位图的显示技术,直接在当前图形上下文上绘图更快,原因是iOS内部使用了基于矩形纹理的缓冲显示技术。
(2)清除背景较耗时。使用UIGraphicsBeginImageContextWithOptions函数用时约9毫秒,相对较快,并能自动背景透明和设置当前变换矩阵,因此在需要位图上下文绘图时要用该函数,不使用更底层的CGBitmapContextCreateImage函数。
注:清除背景(CGContextClearRect)较耗时,占用内存较多,创建位图上下文则很快,不影响内存。推测其原因是创建位图上下文时并没有分配绘图缓冲区,在实际绘制时才分配缓冲区。(2014-2-19)
(3)缓存位图较大,显示较慢。应当在重新显示图形所需时间超过缓存位图的显示时间时才使用缓存位图。可动态记录该阀值和决定是否缓存图形内容。
本文经实验发现图形数量与显示时间成如图4‑8所示的线性比例关系。绘图上下文内显示图形前的初始化时间与图形数量无关,在iPod Touch 4上约为40毫秒。
本文对在视图上下文和位图上下文上的图形显示速度进行评测,在iPod Touch 4上的实验结果如图4‑8所示。可见显示时间与图形数量成线性比例关系,在视图上下文上的显示略慢。结合iOS显示原理的分析结论,本文做出下列推测:
(1)两者的矢量图形显示速度接近,视图上下文没有使用硬件加速能力。
(2)在视图上下文的内部显示流程中,先将矢量图形以PostScript指令缓存到显示列表,然后使用OpenGL ES渲染这些指令,增加了图形缓存时间,所以比位图上下文略慢。缓存PostScript指令的优点是在动画显示和放大显示时保持高质量。
图4‑8 位图上下文和视图上下文的显示速度对比
4.2.2 快速手绘的增量绘图技术
在快速手绘原笔迹图形时,每次增加了一段笔迹图形(内部为多段贝塞尔曲线)都需要在视图显示新的内容。如果每次都全部重新显示所有图形就会越来越慢,影响快速手绘应用的回显体验。本文设计了一种增量绘图技术的实现方式,使得图形显示时间由递增趋势变为常量时间(通常能保持在60毫秒以下)。实现方式如下:
(1)在触摸过程中设置当前临时图形的几何参数,动态显示该图形:调用视图接口的redraw函数,在视图适配器的redraw实现函数中调用视图的setNeedDisplay函数,在下次系统调用视图的drawRect函数时调用内核的dynDraw函数显示图形。
(2)一次触摸完成后,在内核中将该临时图形提交到图形列表,记下新图形,然后调用视图接口的regenAppend函数。如果调用regen函数则会显示所有图形。如果触摸太快来不及显示,就记下更多的新图形,后续批量显示。
(3)在视图适配器的regenAppend实现函数中,先得到视图的当前快照图像(使用 [CALayer renderInContext:] 函数),然后通知视图重绘。
(4)在视图重绘消息响应函数drawRect中,先显示并销毁该快照图像,然后调用内核的drawAppend函数,由后者显示(2)中记录的新图形。
(5)继续触摸绘图,转到步骤(1),实现快速增量绘图。
本文所实现的增量绘图技术的关键点在于每次新增图形后只需要显示快照图像和新增图形,无需显示之前已有图形。以iPad 3为例,使用renderInContext生成快照图像约36毫秒,显示快照图像约46毫秒,生成和显示是在不同的消息响应函数中进行的,整体显示很流畅,不受图形数量影响。
4.2.3 快速动态绘图的多层绘图技术
动态绘图的过程:在触摸过程中改变临时图形,调用视图接口的redraw函数,由视图适配器设置视图无效区域并触发重绘消息,在重绘消息响应函数中显示这些临时图形,最终实现了及时回显的交互式动态绘图。
在一个视图中同时显示静态图形和动态临时图形的问题是动态绘图相对于当前触摸位置有短暂延迟,会产生拖尾现象。虽然可以使用快照图像避免重新显示所有图形,但也需要几十毫秒进行图像显示。
本文设计了如图4‑9所示的多层视图结构,将静态图形和动态图形分别在单独的视图中显示。静态图形视图是应用宿主视图中的子视图,设置为背景透明以便显示出阅读器等应用宿主视图中的内容。动态图形视图与静态图形视图大小相同,动态图形视图通常在所有视图的顶端。因为iOS中每个视图都有独立的层,能够独立绘制,所以分离视图后可以让动态图形视图不显示静态图形内容,改善回显体验。
图4‑9 绘图视图的层次结构
从静态图形视图获取快照图像(例如用于预览)是较常用的功能。本文比较了下列获取快照的方法,结论是调用层的renderInContext函数是最快的方法。
方法1:在位图上下文中绘制所有图形,与在视图显示图形的时间相近,慢。
方法2:在位图上下文中调用层的drawInContext函数,比方法1慢10毫秒。
方法3:在位图上下文中调用视图的层的renderInContext函数,最快,与图形数量无关。例如,在iPad 3上用时36毫秒,在iPod Touch 4上用时11毫秒。与表4‑3对比还能得出结论:renderInContext比显示缓存位图更快。
对静态图形视图调用层的renderInContext函数会得到该层及所有子层的快照图像,包含了动态临时图形的显示内容,不满足实际需要。为了仅得到静态图形视图的显示内容,本文的解决方法是将这两类图形所在的视图设置为应用宿主视图的同级子视图。但在静态图形视图中响应触摸消息进行绘图时发现一个问题:第一次触摸能正常工作,后续触摸不能工作。本文试验了两种方法解决该问题:一是在动态图形视图中也响应触摸消息并转发给静态图形视图;二是将动态图形视图设置为禁止交互(userInteractionEnabled = NO)。第二种方法更简单。
4.2.4 动态绘图的参数优化
由于动态交互式绘图需要频繁更新显示内容(静态图形较少更新),提高动态绘图的性能就比较关键。本文除了上述显示优化技术外,还针对一些关键的绘图参数进行了优化分析,在iPad 2上显示100个椭圆的测试结果见图4‑10。
图4‑10 绘图参数对显示时间的影响
图4‑10中的测试参数有:
(1)平滑度。Quartz 2D内部的拟合折线与实际曲线的最大偏差距离,屏幕像素值,小于1时高精度渲染、费时。
(2)线宽。Quartz 2D采用填充方式实现线宽效果,一个图形内部的重叠部分不会重复填充。
(3)反走样(糙,不反走样。反,反走样)。iOS在单独的离屏位图上对图形渲染实现反走样效果。
(4)填充(空,不填充。填,填充)。在闭合形状的区域中填充单一颜色。
(5)线型(实,实线。虚,虚线)。指定线条阵列描绘形状路径。
从图4‑10可以得到下列结论:
(1)平滑度小于3像素时较费时,超过3后显示时间变化较小、影响美观性。
(2)线型影响较大,虚线的显示时间是实线的两倍以上。
(3)反走样所需时间是不反走样的接近两倍。
(4)填充对显示性能影响较大,显示时间是不填充的两倍以上。
(5)线宽影响较小。
因此,平滑度可以设置为3,快速显示时不使用虚线等线型、不反走样、不填充能显著加快显示速度。可以采用逐步渲染技术(例如,在子线程中分精度等级渲染、先批量描边后批量填充)提高响应速度。
4.3 iOS绘图平台的结构
4.3.1 静态结构
根据4.2节显示优化技术的研究结果,iOS绘图平台按图4‑11设计静态类结构(省略了跨平台内核的结构),相应类的说明如下。
图4‑11 iOS绘图适配模块的结构
(1)GiViewController。面向应用程序的绘图封装接口类,提供常用API,从UIViewController派生。
(2)GiGraphView。显示静态图形的视图类,负责触摸手势识别,委托内核的GiCoreView实现图形显示和手势操作。
(3)DynDrawView。显示动态图形的视图类,不负责触摸手势识别,委托内核的GiCoreView显示动态图形。
(4)GiViewAdapter。视图适配器,允许内核回调iOS视图,通知刷新显示。
(5)GiQuartzCanvas。使用Quartz 2D实现的画布适配器。
(6)GiCoreView。跨平台内核的视图分发器,托管图形对象,分发显示请求和手势信息给图形列表和当前命令。
4.3.2 应用效果
在iOS绘图平台中应用多层绘图技术分离GiGraphView和DynDrawView视图,提高了动态交互式绘图的回显速度。在GiGraphView视图中应用增量绘图技术,连续绘制*曲线等图形时不出现明显的拖尾现象。在旋转屏幕和动态放缩过程中应用绘图参数优化技术,提高显示反馈速度。采用这些技术后,绘图体验较流畅。
在跨设备平台的内核中使用绘图命令可以显示各种图形,在内核视图中使用仿射变换实现放缩显示,图4‑12展示了实际绘图效果[1]。
图4‑12 iOS综合绘图效果
目前,iOS绘图平台已在数字教育等领域应用,降低了应用开发的工作量。图4‑13(a)和(b)展示了在阅读器页面上进行批注式教学的效果。图4‑13(c)展示了第三方公司基于TouchVG开发的“脑力风暴”iPad软件的图文笔记效果[2]。
图4‑13 绘图平台在数字教育等领域的应用效果
4.4 本章小结
本章描述了基于Quartz 2D实现画布适配器的方式,实现了图形和图像的矢量化显示,针对手绘应用设计了虚线模式和线端类型的匹配规则。画布适配器的单元测试使用了跨平台内核自动绘制图形,证明了在跨平台内核中可以使用C++在iOS上交互式绘图。
本章对双缓冲技术进行了实验,结论是基于缓冲位图的显示技术不适合iOS等移动设备,需要根据时间阀值动态决定是否使用缓存位图。总结了图形数量与显示时间的线性比例规律。
本章设计了适合连续手绘的增量绘图技术的实现方式和快速动态绘图的多层绘图技术的实现方式,通过利用缓存位图和层加快了交互式绘图的回显速度,总结了绘图属性的优化方法。
最后,描述了iOS绘图平台的结构和应用效果。
[1] 手绘曲线采用三次参数样条曲线模型,本文对曲线模型和数据点采样法不做研究和论述。
[2] 已在AppStore发布,地址为 https://itunes.apple.com/us/app/nao-li-feng-bao/id565836136?mt=8 。