论文第5章:Android绘图平台的实现

时间:2022-04-15 03:46:01

面向移动设备的矢量绘图平台设计与实现

Design and Implementation of Mobile Device-oriented Vector Drawing Platform

引用本论文: 张云贵. 面向移动设备的矢量绘图平台设计与实现[D]. 北京:北京理工大学软件学院, 2013.

本论文的相似度为0%,是源创论文。欢迎评阅讨论,请勿抄袭,如需更多资料请在博客留言。

如果在研究或论文中使用到,欢迎回复或私信你的学校、姓名、研究领域,并在论文中添加引用或致谢。感谢你对开放成果的尊重和鼓励。

第5章 Android绘图平台的实现

本章阐述了Android绘图平台的实现方法,主要是在跨平台内核的基础上实现Android画布适配器和视图适配器,对图形显示优化技术进行了实验研究。

5.1 开发环境

5.1.1 SWIG的工作原理分析

如图5‑1所示,借助于SWIG实现Android程序的Java代码通过JNI访问C++的类。在编译阶段SWIG工具从C++生成JNI的Java类文件和相应的C++实现文件,该实现文件与原有的C++实现文件一起通过NDK编译为本地动态库。

论文第5章:Android绘图平台的实现

图5‑1 Android程序调用C++类的原理

如图5‑2所示,利用SWIG的Director特性,指定某个具有虚函数的C++类可重定位,然后再生成C++导出函数文件和JNI的Java类文件。在应用层中从对应的JNI类继承并实现其函数,在执行该C++类的虚函数时对应的Android函数就被执行。

论文第5章:Android绘图平台的实现

图5‑2 Android类从C++类的虚函数重载的原理

图5‑2中,SwigDirector_SomeClass从C++的SomeClass派生,Android类从JNI的SomeClass类派生,在SwigDirector_SomeClass中通过调用JNIEnv类的CallStaticVoidMethod等函数实现在C++中调用Java的类函数,这样Android类中相应的重载函数便得到调用,实现使用Android SDK的Java类来扩展C++类。

5.1.2 SWIG的运行性能分析

TouchVG平台使用SWIG实现Android程序的Java类与内核的C++类之间的双向调用,即Java类通过JNI调用C++类、C++类利用Director特性回调Java类。

本文对这两种调用方式进行评测,结果见图5‑3。

论文第5章:Android绘图平台的实现

图5‑3 SWIG在Android中的性能评测结果

图5‑3包含下列四个评测项目:

(1)回调绘图:Android程序通过JNI调用跨平台内核的一个测试函数,在该测试函数中多次回调画布适配器的绘制直线段的函数,在该绘制函数中使用android.graphics包绘图。其性能影响因素有JNI调用、回调和绘图。

(2)回调不绘图:与上一项目的差别是将画布适配器的绘制函数改为空实现,不受图形库的影响。其性能影响因素有JNI调用和回调。

(3)直接绘图:Android程序直接调用绘制直线段的函数,与JNI无关。

(4)正向调用:Android程序通过JNI多次调用跨平台内核的一个测试函数。其性能影响因素是JNI调用,与JNI回调及绘图无关。

评测结果表明,基于虚函数重定位技术的回调方式的性能与普通的JNI调用方式的差别较小,SWIG所增加的封装函数并不会使绘图性能明显下降。

5.1.3 开发方式

Android绘图平台的实现方式如图5‑4所示,编译得到的绘图平台JAR包和内核本地动态库可供应用程序使用。借助于SWIG的Director机制,使用Android SDK实现了画布适配器和视图适配器,实现对内核功能的扩展。

将跨平台内核使用NDK编译到本地动态链接库中,接口形式为JNI和封装类库。采用SWIG将C++类转换为JNI的Java类,SWIG所生成的C++导出函数文件与跨平台内核的代码文件一起编译为动态库。编译过程中使用Python脚本自动修正SWIG所生成的代码中的缺陷,并自动将包含中文字符的文件由UTF8临时转换为GBK编码以便正常编译转换。

论文第5章:Android绘图平台的实现

图5‑4 Android绘图平台的实现方式

对于Android本地动态链接库的调试定位难题,利用NDK提供的日志输出C函数和库文件,通过输出日志文字的方式来解决。用该方法诊断出了SWIG引起的JNI内存问题所在位置,最终采用Python脚本自动修正SWIG所生成的文件缺陷。

5.1.4 开发工具

使用了下列工具分别在Mac OS X 10.7和Windows 7上开发Android绘图平台:

(1)Android开发包(the ADT Bundle)r21.1,可以在Eclipse中调试本地动态库。

(2)Android NDK r8e,用于开发本地动态库。

(3)SWIG 2.0.10,用于从C++头文件生成JNI的类文件和C++封装文件。

(4)Python 2.7,用于运行Python脚本自动修正SWIG所生成的文件缺陷。

(5)MSYS(Minimalist GNU for Windows)1.0,用于在Windows上模拟UNIX环境,执行Shell编译脚本。

5.1.5 SWIG编译配置

在touchvg.swig文件[1]中配置SWIG编译选项,主要配置内容如下:

(1)在文件前面定义下面两个宏:

SWIG_JAVA_NO_DETACH_CURRENT_THREAD

SWIG_JAVA_ATTACH_CURRENT_THREAD_AS_DAEMON

定义前者以便在每次调用本地代码后不与当前线程断开(使用SWIG的Director机制后,在本地函数调用结束时一些JNI对象还需要继续有效,不能与当前线程断开)。定义后者将JNI环境附加在守护线程上(默认是附加在界面主线程上,在Activity退出时可能会崩溃)。

(2)输出JNI_OnLoad函数。Dalvik虚拟机要求必须实现JNI_OnLoad函数,本平台仅简单返回JNI_VERSION_1_6,由SWIG生成的代码自动注册本地函数。

(3)指定GiCanvas和GiView需要生成重定向类,并导出相应的头文件。

(4)输出要在Android代码中使用的内核接口。例如,GiCoreView类。

(5)添加TmpJOBJ辅助类,在析构函数中自动释放JNI本地引用对象。SWIG生成的Director类中某些形参的本地引用对象没有释放,会因超出256个JNI引用对象的限制而溢出崩溃。例如,在GiCanvas的drawBitmap函数中,name字符串对象所对应的本地引用对象“jstring jname”在调用了NewStringUTF函数后没有调用DeleteLocalRef函数。

本文针对该问题提出的解决方法:将SWIG所生成的封装文件中的“jstring jname = 0”替换为“jstring jname = 0; TmpJOBJ jtmp(jenv, &jname)”,通过TmpJOBJ的析构作用自动调用DeleteLocalRef函数释放引用。使用Python脚本[2]自动进行替换SWIG生成的封装文件中的这类问题。

编写了Shell脚本(mk/swig.sh),用于运行SWIG工具生成JNI导出函数的封装文件(touchvg_java_wrap.cpp)和JNI类文件。JNI类的包名为touchvg.jni,其文件输出到工程的src/touchvg/jni目录下,将与视图适配器的代码(src/touchvg/view目录)共同生成为一个JAR文件。

5.1.6 NDK编译配置

Android绘图平台的代码目录结构见第20页的图3‑7。在工程的jni/Android.mk文件[3]中配置本地动态库的NDK编译选项,主要有:

(1)基于绝对地址$(LOCAL_PATH)/../../../core/include在LOCAL_C_INCLUDES中指定内核的头文件路径,基于相对地址 ../../../core/src 在LOCAL_SRC_FILES中指定跨平台内核的实现文件(*.cpp,使用绝对的路径无法编译)。

(2)因为SWIG的Director代码使用了RTTI运行时类型信息,所以在LOCAL_CFLAGS中指定-frtti选项。

(3)为了使用STL,在jni/Application.mk中指定“APP_STL := stlport_static”。

本文编写了Shell脚本(ndk.sh),在其中进入android\demo\jni目录自动运行ndk-build编译出本地动态链接库libtouchvg.so,在编译过程中自动应用Android.mk中的配置信息。Onur Cinar[4]介绍了在Android.mk中包含脚本的方法,可自动运行脚本。

本文在多个平台编译时发现Shell脚本文件应使用Unix行结束符(LF),不能是DOS结束符或Mac结束符,尽可能避免使用中文字符。

5.2 基于Android Canvas实现画布适配器

在第12页的2.2.3节介绍了Android二维绘图主要涉及的框架。本文主要基于两种视图类设计绘图视图类:android.view.View和android.view.SurfaceView,在绘图视图类中使用Android Canvas画布类(使用android.graphics包)渲染。

5.2.1 画布原语与Android Canvas的映射

本文基于android.graphics包设计画布适配器类touchvg.view.CanvasAdapter,该类实现touchvg.jni.GiCanvas中的画布原语函数,后者是通过SWIG从跨平台内核的GiCanvas接口自动生成的。在内核中调用画布接口GiCanvas的函数时,画布适配器将被回调执行,从而允许使用Android Canvas渲染。

画布适配器主要使用了android.graphics包中这些类:Canvas画布类访问绘图函数接口,Paint类指定颜色等绘图属性,Path类构建路径,Bitmap指定位图数据。在绘图视图的onDraw函数中将Canvas画布对象传入画布适配器,后续绘图将在该画布对象上进行。在离屏位图上渲染时,从位图构建画布对象,接着传入画布适配器。

因为Paint对象只能指定一个颜色,无法区分画笔颜色和画刷颜色,所以画布适配器针对画笔、画刷和文字显示分别使用一个Paint对象:mPen、mBrush、mTextPen。以显示一个红边蓝底的椭圆为例,先设置mPen的颜色为红色、mBrush的颜色为蓝色,然后分别使用mPen和mBrush作为参数绘制椭圆。为了让文字颜色和图形颜色同步,在setPen函数中同时设置画笔mPen和文字属性mTextPen的颜色。

这三种Paint对象的参数设置见表5‑1。

表5‑1 Paint对象的参数设置

画笔

画刷

文字

mPen.setAntiAlias(true)

 

mTextPen.setAntiAlias(true)

mPen.setDither(true)

 

mTextPen.setDither(true)

mPen.setStyle(STROKE)

mBrush.setStyle(FILL)

 

mPen.setPathEffect(null)

mBrush.setColor(0)

 

mPen.setStrokeCap(Cap.ROUND)

   

mPen.setStrokeJoin(Join.ROUND)

   

表5‑1中,画刷默认填充颜色为透明色,即不填充。画笔的默认线型为实线,线端为圆端,这样在绘制短线时更像一个圆点。为了让点线等虚线类型的空白间隙整齐,在setPen函数中对所有虚线类型设置平端的线端类型。

与iOS绘图平台的实现类似,Android画布适配器按表5‑2所示的映射方法实现了画布原语函数。由跨平台内核中的TestCanvas类生成和显示矢量图形和图像。

在实现这些函数时,本文对下列内容进行了特殊处理或总结。

(1)在View中调用画布适配器的clearRect函数,无法使指定区域透明,如图5‑5(i)所示。只能在原有图形基础上填充颜色,指定透明色将填充为黑色。在SurfaceView中调用画布适配器的clearRect函数,可以擦除指定区域内的图形,变为透明区域,如图5‑5(k)所示。

(2)当程序和视图使用了硬件加速特性后,调用clipPath会崩溃,其原因是在硬件加速时不支持clipPath函数。解决方法是在UnsupportedOperationException异常出现后将对应的视图的层类型设置为软件实现方式(LAYER_TYPE_SOFTWARE)。

(3)在SurfaceView视图中使用渲染线程连续绘图能够达到48~56FPS的更新速度,实验效果如图5‑5(n)所示。测试用例为绘制不断延长的三次贝塞尔曲线,测试条件为MOTO MZ606平板电脑(Android 4.0.3,1280×800)。

表5‑2 画布原语与android.graphics的映射

画布原语

测试号

Android函数对应关系

clearRect

i k

mCanvas.drawColor(mBkColor, Mode.CLEAR),需要设置剪裁区域

drawRect

a

mCanvas.drawRect,使用mPen和mBrush

drawEllipse

b

mCanvas.drawOval,宽高不超过1时使用drawPoint

beginPath

多个

创建路径对象mPath

moveTo

多个

mPath.moveTo

lineTo

c

mPath.lineTo

bezierTo

e

mPath.cubicTo

quadTo

f

mPath.quadTo

closePath

c

mPath.close

drawPath

多个

mCanvas.drawPath(mPath, mBrush)、.drawPath(mPath, mPen)

drawHandle

g

mCanvas.drawBitmap,在指定点显示

drawBitmap

g

mCanvas.drawBitmap,指定Matrix矩阵变换对象

drawTextAt

h

mTextPen.setTextSize、mCanvas.drawText,用到FontMetrics

setPen

多个

mPen.setColor、mPen.setStrokeWidth、mTextPen.setColor

mPen.setPathEffect、mPen.setStrokeCap

setBrush

多个

mBrush.setColor

saveClip

m

mCanvas.save(CLIP_SAVE_FLAG)

restoreClip

m

mCanvas.restore()

clipRect

m

mCanvas.clipRect

clipPath

m

mCanvas.clipPath,硬件加速时需要将视图的层改为软件实现类型

drawLine

d

mCanvas.drawLine

注:其中的测试号为图5‑5中的测试子图号。

论文第5章:Android绘图平台的实现

图5‑5 Android画布适配效果

5.2.2 图像的显示和管理

在绘图视图中管理图像对象,画布适配器从视图获取图像。显示接口函数为:

void drawBitmap(String name, float xc, float yc, float w, float h, float angle)

其中,使用名称name标识图像对象,(xc,yc)为图像的中心显示位置,w和h为显示目标宽高,angle为旋转的角度(世界坐标系中的逆时针方向)。

图像绘制的基点在图像的左上角,画布的坐标系为ULO类型,绘制过程为:

(1)根据name从绘图视图获取Bitmap对象;

(2)计算变换矩阵:将显示基点由图像的左上角平移到中心,反向旋转angle角度(弧度转换为度),将宽高分别放缩到w和h,最后平移到(xc,yc)。

(3)使用此矩阵显示图像对象。

本文实验发现在显示大图片时,加载图片所需时间远大于显示图像的时间,因此减少图片加载次数能加快显示速度。本文采用下面两种方法进行图像管理:

(1)在绘图视图类中使用LruCache缓存图片。定义LruCache<String, Bitmap>类型的成员变量,以图像标识串(drawBitmap中的name)为键值管理图像对象。

(2)加载图片前先检查图片的宽高,如果太大就以降低采样率方式加载图片。

5.3 绘图视图的设计和实验

为了提高视图的显示质量和性能,本文针对View、SurfaceView进行了实验。

5.3.1 实现方式

绘图视图使用画布适配器CanvasAdapter绘图,由跨平台内核中的GiCoreView和TestCanvas类自动显示测试图形。绘图视图类的关系见图5‑6,实现方式说明如下。

(1)GraphView。从View派生,在onDraw中绘图,调用invalidate()重绘。在onDraw中调用内核视图的drawAll函数显示所有图形。在触摸响应函数中调用内核视图的onGesture函数传递手势动作,由后者在某个交互命令中调用视图的redraw等函数,这将回调到GraphView的视图适配器(ViewAdapter),后者调用视图的invalidate()标记需要重绘。

(2)面板表面视图。从SurfaceView派生。调用setZOrderOnTop(true)设置为面板窗口,显示于宿主窗口之上。调用getHolder().setFormat(TRANSPARENT)设置其Surface背景透明,以显示宿主窗口的内容。在Surface就绪和刷新显示时启动渲染线程,在绘图线程中获取画布绘图,由内核视图的drawAll函数显示所有图形。

(3)媒体表面视图。从SurfaceView派生。默认就是媒体窗口,显示于宿主窗口之下,自动在宿主窗口上设置透明区域以便让SurfaceView上的内容可见。在Surface就绪和刷新显示时启动渲染线程,在绘图线程中获取画布绘图。

(4)GraphViewCached。从View派生,使用一个位图缓存图形内容,在onDraw函数中显示该位图。

内核调用regenAll函数时销毁该位图,下次onDraw函数执行时重新生成位图。应用增量绘图技术,添加新图形后调用regenAppend函数,直接在该位图上绘制新图形,下次onDraw函数执行时显示有新内容的位图。

论文第5章:Android绘图平台的实现

图5‑6 Android渲染视图的类关系

(5)静态View + 动态View。在布局视图中创建两个基于View的视图类(GraphView和DynDrawStdView),分别显示不变的图形和经常改变的内容,前者渲染的内容通常较多。

(6)面板表面视图 + View。在布局视图中创建GraphSfView和DynDrawStdView视图。在GraphSfView中显示静态图形,GraphSfView位于窗口顶端。在DynDrawStdView中显示动态图形。

(7)静态View + 面板表面视图。在布局视图中创建GraphView和DynDrawSfView视图。在GraphView中显示静态图形,在DynDrawSfView中显示动态图形。DynDrawSfView是面板表面视图,在子线程中获取画布绘图,每次刷新显示时启动渲染线程。

(8)面板表面视图 + 面板表面视图。在两个位于窗口顶端的视图类中分别显示静态图形和动态图形。

(9)媒体表面视图 + View。在GraphSfView中显示静态图形,GraphSfView位于根视图层次的底端,在DynDrawStdView中显示动态图形。

(10)媒体表面视图 + 面板表面视图。在两个基于SurfaceView的视图类中分别渲染静态图形和动态图形,静态图形在窗口底端渲染,动态图形在顶端渲染。

以上的视图类按表5‑3调用内核视图的显示函数,由GiCoreView显示图形。

表5‑3 Android视图类与内核显示函数的对应关系

视图类

对应的GiCoreView显示函数

视图类

GiCoreView函数

GraphView

drawAll

DynDrawStdView

dynDraw

GraphSfView

drawAll

DynDrawSfView

dynDraw

GraphViewCached

drawAppend、dynDraw、drawAll

   
5.3.2 实验结果

本文针对View、SurfaceView进行了上述十组实验,实验结果见图5‑7和表5‑4。实验条件为:Android 3.0、模拟器(320×480),其中使用较小分辨率是便于在本文中插入屏幕截图。在MOTO MZ606平板电脑(Android 4.0.3,1280×800)上实验后也得出相同的结论。

从这十组实验得到下列结论:

(1)普通的绘图方式基于View实现定制视图,在onDraw函数中使用Canvas进行绘图。该方式使用简单,适合绘制简单图形。缺点是绘图速度较慢,刷新一个视图会使同级的其他视图被动刷新,容易引起显示性能下降问题。

(2)交互式绘图显示速度快的方式有:使用增量绘图技术的普通视图(GraphViewCached);在SurfaceView中绘制动态图形的双层绘图视图。使用增量绘图技术的优点是可以只需要一个绘图视图,双层绘图视图的优点是可以在子线程中绘图,能提高刷新帧率。可以将两者的优点结合起来,在GraphViewCached中显示静态图形,在SurfaceView中绘制动态图形。

(3)如果要在SurfaceView中异步绘制静态图形,合适的使用条件有:a、与其他内容视图没有重叠区域;b、在窗口顶端透明显示,不要在此区域显示按钮等临时界面控件;c、在窗口底端显示,窗口里没有不透明的大面积界面元素。

论文第5章:Android绘图平台的实现

图5‑7 Android渲染视图效果

表5‑4 Android渲染视图的组合实验情况

图号

视图搭配类型

显示速度和问题

a

GraphView

b

GraphSfView(面板表面视图)

慢,图形遮挡按钮

c

GraphSfView(媒体表面视图)

d

GraphViewCached

动态和静态绘图都很快

e

静态View + 动态View

慢,另一视图被动刷新

f

面板表面视图 + View

快,静态图形遮挡按钮和动态图形

g

静态View + 面板表面视图

h

面板表面视图 + 面板表面视图

快,动态绘图拖尾明显,遮挡按钮

i

媒体表面视图 + View

快,不透明视图会遮挡静态图形

-

媒体表面视图 + 面板表面视图

快,不透明视图会遮挡静态图形

注:其中的图号为图5‑7中的子图号。

5.4 Android绘图平台的结构

5.4.1 静态结构

根据绘图视图的实验结果,Android绘图平台按图5‑8设计静态类结构(省略了跨平台内核的内部结构和SWIG的Director类),相应类的说明见表5‑5。

论文第5章:Android绘图平台的实现

图5‑8 Android绘图适配模块的结构

表5‑5 Android绘图适配模块的类

含义和职责

GraphViewHelper

面向应用程序的绘图封装接口类,提供常用API

GraphViewCached

显示静态图形的视图类,使用了基于缓存位图的增量绘图技术,负责触摸手势识别,委托内核的GiCoreView实现图形显示和手势操作

DynDrawSfView

显示动态图形的SurfaceView视图类,委托GiCoreView显示动态图形

ViewAdapter

视图适配器,允许内核回调Android视图,通知刷新显示

CanvasAdapter

使用Android Canvas实现的画布适配器

GiCoreView

跨平台内核的视图分发器,托管图形对象,分发显示请求和手势信息给图形列表和当前命令

应用程序使用绘图视图有两种方式:(1)仅使用GraphViewCached视图,适合图形量不太多的场合。(2)通过GraphViewHelper创建一个布局视图,自动创建GraphViewCached和DynDrawSfView视图,适合动态绘图帧率要求较高的场合。

5.4.2 应用效果

在Android绘图平台(属于TouchVG框架)中应用多层绘图技术分离静态图形视图和动态图形视图,提高了动态交互式绘图的回显速度。在静态图形视图中应用增量绘图技术,在连续绘制曲线图形时没有明显的拖尾现象。因此,绘图体验较流畅。

在跨设备平台的内核中使用绘图命令可以显示各种图形,在内核视图中使用仿射变换可以实现放缩显示。图5‑9展示了在不同Android版本的模拟器和平板电脑上的实际绘图效果。

论文第5章:Android绘图平台的实现

图5‑9 Android综合绘图效果

5.5 本章小结

本章详细描述了SWIG在Android中的应用方法和扩展机制,针对出现的本地引用问题提出了修正方法。实验表明,SWIG所增加的封装函数并不会使绘图性能明显下降。描述了基于Android Canvas实现画布适配器的方式,实现了图形和图像的矢量化显示。画布适配器的单元测试使用了跨平台内核自动绘制图形,证明在内核中可以使用C++在Android上交互式绘图。

对普通视图、面板表面视图和媒体窗口进行了组合实验,总结出交互式绘图显示速度快、不出现遮挡问题的两种方式:使用增量绘图技术的单一视图方式;在SurfaceView中绘制动态图形的双层视图方式。

最后给出了Android绘图平台的设计结构和应用效果。


[1] 详细的SWIG编译选项见文件:https://raw.github.com/rhcad/vglite/master/android/demo/jni/touchvg.swig 。

[2] Python脚本见文件:https://raw.github.com/rhcad/vglite/master/android/demo/jni/replacejstr.py 。

[3] NDK编译配置文件见:https://raw.github.com/rhcad/vglite/master/android/demo/jni/Android.mk 。

[4] Onur Cinar. Pro Android C++ with the NDK. Berkeley: Apress, 2012