论文第3章:移动绘图平台的架构设计
Design and Implementation of Mobile Device-oriented Vector Drawing Platform
引用本论文: 张云贵. 面向移动设备的矢量绘图平台设计与实现[D]. 北京:北京理工大学软件学院, 2013.
本论文的相似度为0%,是源创论文。欢迎评阅讨论,请勿抄袭,如需更多资料请在博客留言。
如果在研究或论文中使用到,欢迎回复或私信你的学校、姓名、研究领域,并在论文中添加引用或致谢。感谢你对开放成果的尊重和鼓励。
第3章 移动绘图平台的架构设计
本章根据移动设备的特点,设计了跨平台绘图内核及设备接口。为了在iOS和Android上实现矢量绘图平台,本章设计了适配器的实现方式。后面两章将基于本章内容具体阐述在iOS和Android上适配器的实现方式。
3.1 绘图应用的跨平台设计
相对于Web应用,本地应用通常开发成本高、难以实现跨平台。iOS和Android的主要编程语言分别是ObjC(即Objective-C)和Java,相互移植很困难。本文提出了基于虚函数多态行为的跨平台扩展机制。该机制将主要功能在跨平台内核中使用C++实现,而在不同的移动平台上实现适配器模块,使其易于扩展。
3.1.1 基于多态的回调扩展机制
对于iOS,界面封装层采用ObjC和C++实现(实现文件的后缀名为.mm)。ObjC类可以调用C++类和全局函数,但不能从C++类继承。国内外的相似软件通常使用ObjC实现所有绘图功能,不能跨平台。
本文针对iOS设计了如图3‑1(a)所示的扩展机制:与设备平台相关的类从绘图内核的C++接口派生,以虚函数多态方式实现定制。其优点是跨平台内核可以调用与设备平台相关的C++类,C++类再调用ObjC类。创新点是可以在跨平台内核中使用C++实现易于扩充的功能,使用ObjC对内核进行扩展。
对于Android,界面封装层采用Java实现,在内核层与界面封装层之间提供JNI封装层,实现C++到Java的衔接转化。通常的实现方案是使用Java实现所有绘图功能,很少从C++类继承和扩展。本文设计了如图3‑1(b)所示的调用和回调机制:界面封装层通过JNI调用内核,内核通过SWIG所生成的Director类回调到JNI的Java类,界面封装层再从Java类派生,以虚函数多态方式实现定制。创新点是可在跨平台内核中使用C++实现易于扩充的绘图功能,通过SWIG在Android上实现定制。
基于多态的回调扩展机制的创新在于实现了“一次编写,多处应用”,解决了目前在移动平台之间代码移植困难的问题。在跨平台内核中不断扩充功能不影响设备平台适配器模块,从而增强了扩展性,减少了移植工作。
3.1.2 跨平台编码的策略
跨平台内核采用C++实现。C++适合iOS、Android、Windows Phone、Windows Desktop等操作系统。本文试验总结了下列跨平台编码策略:
(1)使用标准C/C++函数库(libc、libstdc++)进行数学计算、字符串操作、动态内存管理,不使用C++异常类型(exception)以避免跨编程语言的异常未捕获问题。
(2)避免使用各个平台的保留关键字与已有名称作为变量和函数名。例如,在iOS上不使用id作为变量名。为了在iOS上避免宏定义、枚举定义及变量的同名冲突,不使用命名空间来区分名称,而是加上命名前缀,对枚举值的名称以k开头。
(3)不使用复杂的多线程管理函数和锁类型。将多线程管理放在设备相关的模块中,少用全局变量和共享数据。使用轻量级的原子计数加减函数[1]进行访问保护。
(4)在iOS和Android上,使用泛型编程和标准模板库(STL)的容器类,不使用专用的容器等数据结构类型。
(5)因为iOS和Android不支持宽字符(wchar_t)类型,字符串默认采用UTF-8编码,所以在跨平台内核中采用UTF-8编码、char*类型。
3.2 适应多种设备的绘图模型
3.2.1 移动平台的异同分析
目前,主流的移动操作系统是iOS和Android。本文通过对这两个平台的特性分析得出下列与绘图相关的结论:
(1)使用多点触摸手势的交互方式。单指和双指手势具有普遍适用性,设备平台已提供这些手势的识别器,不需要重新开发手势识别器。
(2)界面主要采用二维图形引擎显示。图形引擎内部可以使用OpenGL ES加速绘制,使用层(矩形纹理)进行显示内容的缓存和动画合成。官方推荐采用离屏渲染技术加速绘图,在层上缓存渲染内容。
(3)交互式动态绘图的刷新方式主要有两种:标记视图的无效区域,依靠消息机制延后刷新内容;在后台线程中预渲染,完成后直接提交到视图的层中。
(4)与绘图相关的设备平台差异,主要是使用了不同的编程语言、本地图形库、界面框架和消息处理方式。视图和层采用树状层次结构显示内容和传递消息事件。
3.2.2 跨平台绘图模型
根据iOS和Android的异同分析,本文提出了一种如图3‑2所示的内核跨平台的交互式绘图模型。
本绘图模型包含六大元素,具体描述如下。
(1)应用程序。集成绘图平台,定制界面布局和效果,应用绘图数据。
(2)视图适配器。使用界面框架实现占位视图,响应绘制和触摸消息,将显示请求和识别出的手势委托内核视图处理。允许内核以回调方式刷新内容。
(3)画布适配器。将画布接口的显示原语映射到特定图形库的绘制函数。
(4)内核视图。管理图形,将显示和手势请求转发给交互命令和图形实体。
(5)交互命令。将手势请求转换为绘图步骤,实现交互逻辑。对显示请求,以动态图形回显交互效果。扩充交互命令可实现更多绘图功能。
(6)图形实体。在内核中管理和显示图形数据,扩充类型实现更多绘图功能。
本绘图模型的关键设计点:抽象出画布接口和视图接口,采用适配器模式解决平台差异问题,基于委托模式实现图形显示和触摸交互功能的跨平台。
3.3 总体设计
3.3.1 分层架构
为了降低模块耦合性、在不同的设备平台上提高可复用性,本论文的矢量绘图平台(TouchVG)按图3‑3所示的分层架构设计。总体上分为两大层:跨平台绘图内核层和设备平台相关的界面封装层。在跨平台绘图内核层中实现设备平台无关的功能。在界面封装层中提供设备平台特有的通用功能,供顶层的各种应用程序使用。
对于iOS,界面封装层采用ObjC和C++实现。对于Android,界面封装层采用Java实现。在绘图内核层与界面封装层之间提供JNI封装层,允许Android的Java代码与内核的C++代码相互调用。实现相互调用的关键技术是使用SWIG进行编程语言的转换和集成。在Windows桌面应用中,可以使用C#和WPF开发界面封装层及程序(也可以使用MFC和GDI+实现)。SWIG也应用在C++与C#之间的衔接转换上。
TouchVG在各个操作系统上有相应的绘图平台,这些平台有相同的内核。例如,图3‑4所示的iOS和Android绘图平台。这些绘图平台都是本地应用形式,能实现更大程度的融合和性能优化。在绘图内核中实现主要的绘图功能,通过在不同平台上编译同一代码的方式适应多种平台、避免重复开发、提高可复用性。
为iOS提供的绘图平台以静态库的形式供各种绘图应用程序使用,如图3‑4(a)所示。应用程序无需使用绘图内核的接口,只需使用界面封装层所提供的简易接口,从而降低了应用开发的工作量。界面封装层和应用程序都基于iOS平台的SDK开发,可以实现统一的界面风格,降低实现难度。
为Android应用程序提供的绘图平台包含一个JAR包(内核JNI和界面封装层的Java类)和一个绘图内核的本地动态库,如图3‑4(b)所示。应用程序通过使用界面封装层所提供的简易接口降低了集成难度。界面封装层和应用程序都基于Android 平台的SDK开发,可以最大程度地利用平台SDK的特性,实现紧密融合。
3.3.2 MVC架构
TouchVG平台采用如图3‑5所示的MVC架构模式设计动态行为。
(1)在绘图视图中响应重绘消息,使用当前的绘图上下文初始化画布适配器,将此画布适配器以抽象画布接口对象传入内核视图,请求绘图。
(2)内核视图将静态绘图和动态绘图的请求分别转发给图形实体和当前绘图命令。图形实体和绘图命令调用画布接口对象绘图。在绘图时画布适配器将被回调。
(3)在绘图视图中响应触摸消息,识别出手势后委托内核视图处理,由内核视图转发给当前绘图命令进行交互式绘图。
(4)绘图命令根据触摸位置改变临时图形,调用抽象视图接口的重绘函数触发重绘消息,在下次重绘时显示该临时图形,从而实现动态绘图。在一次触摸结束后改变图形实体,调用视图接口的重新生成函数,这样就会显示新的图形实体内容。
3.3.3 系统组成
为各种iOS绘图程序提供的TouchVG绘图平台以静态库的形式出现。为Android程序提供的TouchVG绘图平台包含JAR包和绘图内核的本地动态库。TouchVG绘图平台包含的模块如图3‑6所示。其中,refine表示有某个类实现了指定的接口。
TouchVG有下列六个核心模块,除了GraphView模块外都是跨平台的模块。
(1)GraphView:设备平台相关的适配器模块,包含视图适配器和画布适配器。
(2)CoreView:内核视图模块,为上层适配器提供访问接口,管理图形数据,将显示和手势请求转发给图形实体或交互命令。
(3)Command:交互命令模块,实现选择命令及多个绘图命令。这些命令用于绘制和修改各种图形,为上层界面提供动作消息接口以便接受触摸手势或鼠标消息数据。交互命令模块在内部将动作消息分发给相应的命令对象,实现交互式绘图。
(4)Shape:图形实体模块,主要功能是常见图形的存储、渲染和交互计算。
(5)Graphics:图形接口模块,实现显示坐标系变换、放缩平移计算、多种曲线形状的显示输出。在图形接口库中不应用任何图形库,只是提供了适合多种平台的画布接口。由界面封装层的画布适配器类使用某一种图形库来实现该画布接口。
(6)Geometry:数学几何模块,实现点、矢量、变换矩阵、矩形框、基于Bezier的曲线、路径、剪裁、最短距离等几何计算功能及方程组求解等数学算法。
其他模块或类:
(1)JsonStorage:用JSON实现的一种序列化类,实现了MgStorage接口。
(2)Test:跨平台的单元测试模块,供设备平台的TestCanvas等程序使用。
3.3.4 代码目录结构
TouchVG平台的代码目录结构如图3‑7所示,各个文件夹节点的含义见表3‑1。
主目录 |
文件夹名 |
含义 |
core |
callback |
画布接口和视图接口 |
view |
供视图适配器调用的内核视图分发接口 |
|
test |
画布接口的单元测试类 |
|
command |
交互命令模块 |
|
shape |
图形实体模块 |
|
json |
JSON序列化器模块 |
|
graph |
图形接口模块 |
|
geom |
数学几何模块 |
|
ios |
view |
视图适配器和画布适配器 |
tests |
iOS绘图测试程序 |
|
lib |
iOS绘图平台的静态库工程 |
|
android |
mk |
编译脚本 |
demo/jni |
本地动态库的配置文件和自动生成的源文件 |
|
touchvg/jni |
SWIG生成的JNI类文件 |
|
touchvg/view |
视图适配器和画布适配器 |
|
vgdemo |
Android绘图测试类、测试程序 |
3.4 跨平台绘图内核的实现方式
本节描述与设备平台显示相关的实现策略和跨平台绘图内核相关的实现方式,为在iOS和Android上实现了画布适配器和视图适配器设计接口。
3.4.1 矢量图形显示的实现方式
内核跨平台的矢量图形显示方式如表3‑2所示。在设备平台上实现可自定义绘图的视图类(例如MyDeviceView),响应视图的重绘消息,将绘图上下文信息传入画布适配器(例如CanvasAdapter)进行初始化,然后将此画布适配器传入内核视图(GiCoreView)请求绘图,内核视图再分发给相应的对象进行显示。
在内核中调用抽象画布接口(GiCanvas)的绘图函数时,画布适配器的对应实现函数将自动使用特定的图形库完成绘制。因为设备相关的视图类与显示哪些内容无关,所以在跨平台内核中扩充图形结构后不影响视图类,实现了跨平台绘图。
3.4.2 交互式绘图的实现方式
交互式绘图在上述静态图形显示的基础上,根据多点触摸动作的位置信息,动态改变和显示图形。如表3‑3所示,其执行步骤如下:
(1)在绘图视图类中识别出多点触摸手势动作,委托内核分发手势动作。在绘图视图类(例如MyDeviceView)中利用系统内置的手势识别器从多点触摸信息中识别出某种触摸手势,然后转换手势动作参数并委托内核处理(第8~10行)。
(2)内核视图(GiCoreView)将手势动作分发给当前的绘图命令(第21~27行),由绘图命令根据触摸位置来改变临时图形的形状。
(3)绘图命令改变临时图形后,调用视图接口(如表3‑4所示的GiView)的redraw函数,视图适配器自动设置重绘区域和触发重绘消息(第28、13~14行)。
(4)视图类在重绘响应函数中委托内核显示动态图形(第1~4、18~19行),内核的绘图命令将调用抽象画布接口(GiCanvas)绘制动态图形(例如拖曳效果)。
函数名称 |
接口函数定义 |
对应的GiCoreView函数 |
重新构建显示 |
void regenAll() |
void drawAll(GiCanvas& canvas) |
追加显示新图形 |
void regenAppend() |
bool drawAppend(GiCanvas& canvas) |
更新显示 |
void redraw() |
void dynDraw(GiCanvas& canvas) |
根据iOS和Android的手势识别特点(例如Android需要自行实现放缩和旋转手势的识别算法),设计了如表3‑5所示的绘图命令的手势原语。其中,click、doubleClick、longPress是单指手势,touchBegan、touchMoved、touchEnded是单指拖动手势(使用最多,故分解为三个),twoFingersMove对应于双指捏合、旋转和拖动手势(三种手势合并为一个手势原语是为了避免在设备平台识别具体手势类型)。
手势原语 |
接口函数定义 |
点击 |
bool click(const MgMotion* sender) |
双击 |
bool doubleClick(const MgMotion* sender) |
长按 |
bool longPress(const MgMotion* sender) |
开始拖动 |
bool touchBegan(const MgMotion* sender) |
正在拖动 |
bool touchMoved(const MgMotion* sender) |
拖动结束 |
bool touchEnded(const MgMotion* sender) |
鼠标掠过 |
bool mouseHover(const MgMotion* sender) |
双指触摸 |
bool twoFingersMove(const MgMotion* sender, int state, const Point2d& pt1, const Point2d& pt2) |
MgMotion包含当前触点、起始触点、触摸动作状态和视图GiView对象等数据,通过MgMotion的view成员变量将设备平台的视图适配器对象传递到内核。
3.4.3 画布接口
TouchVG平台参考HTML5 Canvas标准[2]设计了跨平台的画布接口,定义了表3‑6中所列出显示原语,适合多数图形库。
(1)基于路径的矢量图形显示。
画布接口支持子路径,包含beginPath、moveTo、lineTo、bezierTo、quadTo、closePath及drawPath等7种显示原语。设计要点:a、避免使用数组、点等复合数据类型,没有折线和多边形等需要可变数量坐标的绘图函数,主要使用浮点数和整数等简单类型,以免在JNI中生成复杂的参数类型。b、坐标使用单精度浮点数而不是整数,与iPhone 4、iPad 3等高像素密度(PPI)的显示屏相适应。
因为矩形、线段、圆、椭圆是较简单且经常使用的图形,大多数图形库都提供了这些图形的绘制函数,所以在画布接口中提供了drawRect、drawLine、drawEllipse绘图函数。其他矢量图形可以用这些路径显示原语表示。例如使用三次贝塞尔曲线表示任意角度的圆弧、四段贝塞尔曲线表示椭圆、将B样条曲线和三次参数样条曲线分解为连续的三次贝塞尔曲线。
显示原语 |
接口函数定义 |
设置画笔 |
void setPen(int argb, float width, int style, float phase) |
设置画刷 |
void setBrush(int argb, int style) |
清除区域 |
void clearRect(float x, float y, float w, float h) |
显示矩形 |
void drawRect(float x, float y, float w, float h, bool stroke, bool fill) |
显示椭圆 |
void drawEllipse(float x, float y, float w, float h, bool stroke, bool fill) |
显示线段 |
void drawLine(float x1, float y1, float x2, float y2) |
开始新的路径 |
void beginPath() |
添加子路径 |
void moveTo(float x, float y) |
添加线段 |
void lineTo(float x, float y) |
添加贝塞尔曲线 |
void bezierTo(float x1, float y1, float x2, float y2, float x, float y) |
添加抛物线 |
void quadTo(float cpx, float cpy, float x, float y) |
闭合当前路径 |
void closePath() |
绘制路径 |
void drawPath(bool stroke, bool fill) |
保存剪裁区域 |
void saveClip() |
恢复剪裁区域 |
void restoreClip() |
矩形作为路径 |
bool clipRect(float x, float y, float w, float h) |
区域作为路径 |
bool clipPath() |
显示控制点 |
void drawHandle(float x, float y, int type) |
显示图像 |
void drawBitmap(const char* name, float xc, float yc, float w, float h, float angle) |
单行文字 |
float drawTextAt(const char* text, float x, float y, float h, int align) |
(2)设置画笔和画刷。
包含setPen和setBrush设置函数。设计要点:a、颜色值使用整数(int argb),在Android上可直接应用该颜色值,在iOS等平台上按字节顺序提取各个颜色分量并转换为平台特有的颜色类型。b、在跨平台内核的GiGraphics类中保存线宽等绘图参数,自动判断和调用画布接口的setPen和setBrush函数,避免在各个设备平台的画布适配器中重复实现该功能。
(3)图形剪裁。
使用图形库的剪裁功能,包含saveClip、restoreClip、clipRect、clipPath函数。
(4)显示图像。
为了简化实现、隔离平台的差异性,图像在设备平台中管理,在画布接口中只使用名称来标识图像对象。drawHandle(x, y, type)显示原始大小的控制点图标等点状图像,drawBitmap(name, xc, yc, w, h, angle)在一个矩形框内显示图像。对于drawBitmap,使用实际宽高表达图像放缩信息、角度表达图像旋转信息、图像的中心位置表达位移信息,这三者共同表达图像的矩阵变换信息,以任意角度和大小矢量化显示图像。
3.4.4 交互式绘图命令
在跨平台内核中使用命令模式和模板方法模式设计交互命令体系结构,如图3‑8所示,每个交互式绘图命令都对应于一个命令类,支持表3‑5所示的手势原语接口。每个命令类对应有一个唯一标识的命令名称,所有命令对象都在MgCmdManager命令管理器类中缓存。
内核中提供常用的基本绘图命令,外界模块可以实现更多的交互命令类,将命令名称和类工厂函数登记到命令管理器,在实际使用该命令时才创建命令对象。
在设备平台相关的模块中可以不直接访问交互命令,通过传递命令名称启动相应的交互命令(例如view.commandName=”circle”),由GiCoreView类将显示请求和手势动作转发给当前命令。
3.4.5 矢量图形的仿射变换
TouchVG平台采用二维变换矩阵实现矢量图形的仿射变换(例如无级放缩显示、平移显示、旋转变形),使用三种坐标系:模型坐标系、世界坐标系和显示坐标系。其中,模型坐标系和显示坐标系都参照世界坐标系使用一个变换矩阵表示,分别记为M和D。模型坐标系到显示坐标系的变换矩阵为M×D-1。显示坐标系的变换矩阵计算涉及下列变量:
(1)视图中心点的世界坐标(xc,yc),单位为毫米。
(2)视图显示比例(scale)。为1.0时表示按世界坐标系等比显示。
(3)视图的宽度(width)和高度(height),单位为点,与设备相关。
(4)视图显示时每英寸的点数(dpi),与设备相关。
按如下式(3-1)计算显示坐标系相对于世界坐标系的变换矩阵:
3.5 本章小结
本章提出了一种适合多种移动平台的基于虚函数多态行为的跨平台扩展机制,将主要功能在跨平台内核中使用C++实现,在不同的移动平台上开发适配器模块,易于扩展和实现。在Android上使用SWIG实现C++与Java的调用和扩展。
通过对移动平台的差异分析,总结了iOS和Android在屏幕显示和触摸交互方式等方面的特点。提出了一种跨平台绘图模型,设计了跨平台交互式绘图内核,关键点是抽象出画布接口和视图接口,采用适配器模式解决平台差异问题,基于委托模式实现图形渲染和触摸操作功能,具备跨平台特性。
TouchVG平台采用分层和MVC架构,主体功能在跨平台内核中实现。为移动平台上的适配器实现工作描述了关键实现方式、画布接口和视图接口的设计意图。
[1] 采用条件编译方式实现原子计数加减函数,在Android上使用 __sync_add_and_fetch和 __sync_sub_and_fetch函数,在iOS上使用OSAtomicIncrement32和OSAtomicDecrement32函数。
[2] Canvas 2D Context:http://dev.w3.org/html5/2dcontext/ 。