高毅 王昕 丁勇 涂小琴
摘 要: 在数据可视化方面,Android系统提供的组件不能满足开发人员的需求,而第三方的图表组件技术不够成熟,本文提出了一种基于Android的图表组件的实现方法,着重讨论了图表组件的布局空间设计、类设计、单位转换、绘制流程、图表绘制。该组件自定义程度高,使用方便,布局整齐,动画效果良好,大大增强了用户体验,能满足大多数Android应用软件开发的需求,具有一定的创新性和很好的实用价值。
关键词: 图表;自定义;Android;数据可视化
中图分类号: TP317 文献标识码: A DOI:10.3969/j.issn.1003-6970.2019.09.009
本文著录格式:高毅,王昕,丁勇,等. 基于Android自定义图表组件的关键技术研究[J]. 软件,2019,40(9):40-44
Research on Key Technologies Based on Android Custom Chart Component
GAO Yi, WANG Xin, DING Yong, TU Xiao-qin
(College of Arts and Sciences, Yunnan Normal University, Kunming 650222,China)
【Abstract】: In terms of data visualization, the components provided by Android system can not meet the needs of developers, and the third-party chart component technology is not mature enough. This paper proposes an Android-based chart component implementation method, focusing on the layout space design of chart components, class design, unit conversion, drawing process, chart drawing. The component has a high degree of customization, convenient use, neat layout, good animation effect, greatly enhances the user experience, can meet the needs of most Android application software development, has certain innovation and good practical value.
【Key words】: Chart; Custom; Android; Data visualization
在数据的分析和展示过程中,数据可视化是非常重要的手段之一,而各种类型的图表,又是数据可视化中最重要和最常用的工具[1]。图表可以以简洁的方式和最清晰的视觉效果,高效地将有价值的信息传递给用户。所以,在应用软件开发中正得到越来越广泛的应用。在Android应用软件开发中,虽然系统提供了大量的组件用于界面设计,但是没有图表组件,因此需要开发者来创建自定义的图表组件,以实现用户的特殊需求[2]。
然而,Android系统中的图表组件的开源方案并不多,第三方的图表组件技术又不够成熟。本文通
过设计一套基于Android的圖表组件,包含了散点图、折线图、柱状图、条形图、饼图和雷达图六种基本图表,实现了数据的可视化展示,该组件的实现通过继承View类,重写了onMeasure、onDraw等多个方法,加入了好多的组件属性作为类的数据成员,并编写了get方法和set方法,丰富了图表组件的显示样式,通过了ValueAnimator类的相关技术来加入动画效果,增强了用户体验。
1.1 图表
图表泛指在屏幕中显示的,可直观展示统计信息属性(时间性、数量性等),对知识挖掘和信息直观生动感受起关键作用的图形结构,是一种很好的将对象属性数据直观、形象地"可视化"的手段[3]。合理的数据图表,会更直观的反映数据间的关系,比用数据和文字描述更清晰、更易懂。将工作表中的数据转换成图表呈现,可以关注我们更好地了解数据见的比例关系及变化趋势,对研究对象做出合理的推断和预测。
1.2View
Android应用的绝大部分UI组件都放在android.widget包及其子包、android.view包及其子包中,Android应用的所有UI组件都继承了View类,View组件非常类似于Swing编程的JPanel,它代表一个空白的矩形区域[4]。
1.3Paint类
要实现绘图功能,首先需要画笔工具,Paint类便是Android的画笔,它包含了绘制几何图形、文本和位图所需的一些风格和颜色信息,如线宽、字体和大小等。通过Paint类提供给用户的公共方法,可以对其属性进行设置。
1.4Canvas类
各类图形是要在一张画布上绘制的,Canvas类则实现了画布这一功能,在绘制图形之前,需要对Canvas设置一些画布的属性,如画布的颜色、尺寸等。
1.5Path类
在进行划线等操作时还需要连接路径,这个工具由Path提供,Path类中包含一些直线或曲线连接到指定点的方法。Android提供的Path是一个非常有用的类,它可以预先在View上将N个点连成一条“路径”,然后调用Canvas的drawPath方法即可沿着路径绘制图形。
2.1布局空间设计
移动端设备的屏幕相对于计算机显示器尺寸相对较小,移动端应用开发的特点之一就是可用来显示的空间小,要让图表有好的显示效果,一定要合理分配利用有限的空间,所以,在实现图表组件时,布局空间的设计尤为关键。图表组件的布局空间设计如图1所示,分为图表标题区、图表绘制区和系列标题区三个部分[5-6]。其中,图表标题区用来显示图表的标题,本文实现的图表组件可以设置图表标题的文本大小,图表标题的文本颜色;图表绘制区用来显示图表及图表相关元素,包括坐标轴、坐标刻度值、背景线条等,本文实现的图表组件可以设置坐标线条颜色、坐标线条粗细、坐标刻度值文本大小、坐标刻度值文本颜色、背景样式等,这是一个核心区域;系列标题区用来显示图表的系列标题,本文实现的图表组件可以显示多个系列数据,所以,图表的系列标题一般会存在多个,为了更好的利用布局空间,本文设计的方案是每一行显示两个系列标题,依次从左到右,从上到下。该图表组件的布局空间的设计,需要进行计算,首先计算该图表在移动设备端的显示大小,再计算系列标题区所占布局空间的大小,最后得到图表绘制区的大小。
2.2单位转换
在Android应用开发中,在设置组件的大小和文字的大小时都要用到单位,Android中常用的单位有px、dip、dp和sp等。其中,px(像素),每个px对应屏幕上的一个点。dip或dp(设备独立像素),一种基于屏幕密度的抽象单位。sp(比例像素),主要处理字体的大小,可以根据用户的字体大小首选项进行缩放。一般情况下,用dp来表示距离大小,用sp表示字体大小。
图表组件在实现时,使用的单位为像素。由于移动端设备的尺寸大小和分辨率各式各样,各种屏幕密度不同导致同样像素大小的长度在不同密度的屏幕上的显示长度不同,相同长度的屏幕高密度屏幕包含更多像素点,为了在不同大小的屏幕上都有好的显示效果,该组件在实现的过程中需要进行单位转换,需要把dp和sp转换为px。
(1)dp转px
在图表组件实现中,编写了把dp单位转换为px单位的方法,代码如下:
private float dpTopx(float dp) {
return TypedValue.applyDimension(Type d V alue. CO MPLEX_UNIT_DIP,
dp, getResources().getDisplayMe t rics());
}
(2)sp转px
在图表组件实现中,编写了把sp单位转换为px单位的方法,代码如下:
private float spTopx(float sp) {
return TypedValue.applyDimension(Ty pedValue. COMPLEX_UNIT_SP,
sp, getResources().getDisplayM e t rics());
}
2.3 類设计
图表组件在实现的过程中,涉及到FColor、DataItems、ChartEntity、View、ChartView5个类,除了类View是系统类,其它的类都是为了实现该图表组件而编写的。类及类关系如图2所示,类ChartView继承于View类,类ChartView依赖于FColor类,类ChartView和类ChartEntity的关系是组合,类ChartEntity和类DataItems的关系是组合。下面就这几个类做详细描述。
(1)FColor类
在Android程序设计中,我们可以在xml布局文件中使用井号加6位十六进制(形如:#XXXXXX)或者井号加8位十六进制(形如:#XXXXXXXX)来表示颜色值,而在java代码中不行。用这种形式来表示颜色值还是非常直观明了的,为了在java代码中也能够这样表示,特地编写FColor类来实现此功能。
FColor类的数据成员由a、r、g、b构成。其中a表示透明度的值,r表示红色分量的值,g表示绿色分量的值,b表示蓝色分量的值。它们数据类型为int,取值范围介于0到255之间。
FColor类中的关键方法public void setColor(String color),是把字符串表示的颜色值分割并转换到a、r、g、b四个分量上面。
8位十六进制转换代码如下:
this.a=Integer.parseInt(color.substring(1, 2+1), 16);
this.r=Integer.parseInt(color.substring(3, 4+1), 16);
this.g=Integer.parseInt(color.substring(5, 6+1), 16);
this.b=Integer.parseInt(color.substring(7, 8+1), 16);
6位十六进制转换代码如下:
this.a=255;
this.r=Integer.parseInt(color.substring(1, 2+1), 16);
this.g=Integer.parseInt(color.substring(3, 4+1), 16);
this.b=Integer.parseInt(color.substring(5, 6+1), 16);
(2)DataItems类
DataItems类是用来表示一个序列数据的,有7个数据成员,其中,seriesName表示系列名,XItemValues表示X项的值(数组),XItemValuesSize表示X项个数seriesValues表示Y项的值(数组),seriesValuesSize表示Y项个数,seriesValuesMax表示Y项最大值,seriesValuesMin表示Y项最小值,为了方便构造图表Y轴坐标刻度,特别地加了最后2个数据成员。DataItems类除了构造方法、数据成员的get/set方法外,关键方法有3个。其中, addSeriesNam方法的功能是用来添加数据序列的名称;addData方法的功能是用来添加1个数据项,1个数据项由2个部分组成,分别为数据项的名称和数据项的值;clearData方法的功能是用来清空数据序列值的。
(3)ChartEntity类
ChartEntity类是用来表示图表的数据源的,可以存储多个系列数据,有2个数据成员,其中,ChartTitle表示系统标题,Series是DataItems类的数组对象,用来存储多个系列的数据。ChartEntity类除了构造方法、数据成员的get/set方法外,有一个关键方法setData,该方法是用来加载数据的。
(4)ChartView类
ChartView类是用来实现图表组件绘制的,该类有几十个数据成员,用来表示图表的数据源、标题文本、标题文本大小、标题文本颜色、系列标题文本、系列标题文本大小、系列标题文本颜色、坐标轴相关属性、内外边距、动画相关属性、背景相关属性等。ChartView类除了构造方法、数据成员的get/set方法外,最为重要的就是onDraw方法了。ChartView類作为View类的子类,需要去重写多个方法来实现图表组件的绘制。
2.4图表的绘制
(1)绘制流程
在Android系统中实现图表组件,需要继承View类,重写其中的一个或者多个方法。本文描述的图表组件是有动画效果的,在绘制过程中把背景的绘制和图表区的绘制分开,这样有利于控制图表区的动画效果。图表组件绘制流程的算法描述如算法1所示。
算法1
第1步:根据用户设置计算图表系列数据;
第2步:根据用户设置计算图表属性值;
第3步:根据系列数据和图表属性值计算相应的图表参数,用于后面绘制背景和图表;
第4步:绘制背景;
第5步:初始属性动画值,animatedValue=0;
第6步:判断animatedValue <= 1 是否成立,若成立,继续下一步,否则,跳到第9步;
第7步:根据属性动画值animatedValue重绘图表,也就是重新执行onDraw方法;
第8步:根据ValueAnimator对象的addUpd ate Listener监听事件计算新的属性动画值animatedValue,返回第6步;
第9步:算法结束。
(2)重写onDraw方法
基于Android UI组件的实现原理,开发者完全可以开发出项目定制的组件,当Android系统提供的UI组件不足以满足需求时,可以通过继承View来派生自定义组件。过程为,首先定义一个继承View基类的子类,然后重写View类的一个或多个方法来实现,其中,onDraw方法尤为关键。本文描述的图表组件由散点图、折线图、柱状图、条形图、饼图和雷达图六种基本图表组成,每一种图表的绘制都有一定的差异,在此,仅以折线图为例来对onDraw方法的关键代码做描述。
……
//计算起点坐标
startX=startLeft; startY=startTop+((Float.valueOf(yItemTitle.get(yIt emsCount-1))-(float)value)/(Float.valueOf(yItemTitle.get (yItemsCount-1))-Float.valueOf(yItemTitle.get(0))))*co nt entHeight;
//绘制第一个点标志
drawMark(canvas,seriesMark[i%seriesMark.length], seriesItemColor.get(i%seriesItemColor.size()),startX,startY, dpTopx(markWidth));
//根据属性动画animatedValue变量值计算当前绘制到哪一个刻度区(X轴方向)
int no=getScope(animatedValue);
//对小于no的刻度区进行绘制
for(int j=1;j if(j v1=lineSeries.get(i).getSeriesValues().get(j-1); v2=lineSeries.get(i).getSeriesValues().get(j); //计算折线的起点坐标 startX=startLeft+(j-1)*avgDis; stopX=startLeft+(j)*avgDis; //计算折线的终点坐标 startY=startTop+((Float.valueOf(yItemTitle.get(yItemsCount-1))- (float)v1)/(Float.valueOf(yItemTitle.get(yIte msCount-1))- Float.valueOf(yItemTitle.get(0))))*contentH eight; stopY=startTop+((Float.valueOf(yItemTitle.get(yItemsCount-1))- (float)v2)/(Float.valueOf(yItemTitle.get(yIte msCount-1))- Float.valueOf(yItemTitle.get(0))))*contentH eight; //繪制折线 canvas.drawLine(startX, startY, stopX, stopY, linePaint); //绘制点标志 drawMark(canvas,seriesMark[i% seriesMark. length], seriesItemColor.get(i%seriesItemColor.size()), stopX,stopY,dpTopx(markWidth)); //把上面折线的终点设置为下一条折线的起点 startX=stopX; startY=stopY; } } //若属性动画animatedValue变量值小于等于1,对等于no的刻度区进行重新绘制,以实现动画效果 if(animatedValue<=1){ if(no<lineSeries.get(i).getSeriesValues().size()){ //计算折线终点的X坐标
stopX=startLeft+animatedValue*contentWidth;
//計算折线终点的Y坐标
v1=lineSeries.get(i).getSeriesValues().get(no-1);
v2=lineSeries.get(i).getSeriesValues().get(no);
float x1=startLeft+(no-1)*avgDis;
float x2=startLeft+(no)*avgDis;
float y1=startTop+((Float.valueOf(yItemTitle.get (yItemsCount-1))-
(float)v1)/(Float.valueOf(yItemTitle.get(yItemsCount-1))-
Float.valueOf(yItemTitle.get(0))))*contentHeight;
float y2=startTop+((Float.valueOf(yItemTitle.get (yItemsCount-1))-
(float)v2)/(Float.valueOf(yItemTitle.get(yItems Count-1))-
Float.valueOf(yItemTitle.get(0))))*contentHeight;
float x=startLeft+animatedValue*contentWidth;
stopY=(y2*(x-x1)+y1*(x2-x))/(x2-x1);
//绘制折线
canvas.drawLine(startX, startY, stopX, stopY, linePaint);
}
}
//绘制最后一个点标志
if(animatedValue==1){
drawMark(canvas,seriesMark[i%seriesMark.length],
seriesItemColor.get(i%seriesItemColor.size()),
stopX,stopY,dpTopx(markWidth));
}
本文实现的图表组件的效果如图所示,该图表组件由散点图、折线图、柱状图、条形图、饼图和雷达图六个基本图表构成。该图表组件可以展示多个系列数据,还具有动画效果,文本和图表可以很好的自适应移动端设备。组件在设计的过程中,加入了大量的属性作为类的数据成员,并编写了相应的set方法和get方法,方便Android应用软件开发人员根据自身的需求去设置图表样式,如标题文本、标题文本大小、标题文本颜色、系列标题文本、系列标题文本大小、系列标题文本颜色、坐标轴相关属性、内外边距、动画相关属性、背景相关属性等。相比现有的类似的第三方开源方案,自定义程度高、使用方便、灵活,用户体验好,所以,该组件还是具有很好的实用性和创新性。
本文实现的图表组件可以解决一些数据展示的问题,可以展示多个系列的数据,方便不同系列的数据进行对比,经过测试,组件自定义程度高,使用方便,布局整齐,动画效果良好,大大增强了用户体验,能满足大多数Android应用软件开发的需求。但是,图表包含很多种类型,而本文仅仅实现了散点图、折线图、柱状图、条形图、饼图和雷达图六种,当遇到一些特殊的数据可视化时,该组件就不能满足需求了,在以后的研究工作中,将在图表改进、图表类型扩展方面做深入研究。
参考文献