最近在做一个跟数据统计相关的web项目,其中会展示一些饼图,折线图以及柱状图这样的图表组件,可以根据一套数据分别使用不同的图表组件来展示,还是蛮有意思的,但有意思归有意思,职业病关系,我还是想自己上手来尝试一下,这篇文章我就先用Compose来做一个饼图组件吧
这个组件需要支持
- 单个饼图或者多个饼图来展示不同维度的数据
- 环形与实心饼图的样式切换
- 展现饼图时需要有动画效果
- 环形饼图可设置环的粗细大小
- 饼图的区间颜色可由数据动态控制
- 饼图周围需支持展示对应数据的描述
这个组件怎么做
准备数据
首先第一步我们将需要展示饼图用到的数据先罗列出来,一般性饼图的数据是一个数组,数组里面每个元素都会至少包含三个属性,一个是饼图区间的名称,一个是饼图区间的具体数值,还有一个是饼图区间色块的色值,我们将这些属性封装在一个数据类里面
PieData
就表示单个区间块,一个饼图是由若干个PieData
组成,所以这里再创建一个PieData
的数组,作为展示饼图需要的数据源
绘制饼图
现在创建个函数Pie
,我们绘制单个饼图就在Pie
里面进行,让dataList
作为Pie
的一个参数,另外增加个modifier
参数,可由调用方控制饼图的大小
饼图是由若干个扇形拼接组成的,所以这里需要绘制datalist.size()
个拥有同一个topLeft
的扇形,我们先计算出扇形的半径,即画布中心点xy轴的最小值的一半
这个radius
决定了饼图绘制的范围大小,而单个扇形的大小则是由sweepAngle
来控制,但是sweepAngle
对应的是角度,我们只知道每个扇形对应的具体数值大小,这里就需要一个公式来将这个数值转换成角度,公式如下
在这个公式里面我们唯一需要去获取的就是所有数值的总和,那么针对一个List
里面某个属性求和我们可以使用map
和reduce
操作符来实现,代码如下
这样就获得了所有数值的总和total
,接着使用上述公式,我们就能将所有扇形的sweepAngle
都计算出来,并且都放在一个List
当中,代码如下
rateList
里面就放着所有扇形的sweepAngle
,我们可以通过遍历rateList
来将所有扇形绘制出来,至于startAngle
,我们可以这样想,第一个扇形的startAngle
一定是在-90度,第二个那就是在第一个扇形的startAngle
上加上第一个扇形的sweepAngle
,得到的结果就是第二个扇形的startAngle
,以此类推下去,代码如下
这里值得注意的是,我们数据里面的color
是一个十六进制的字符串,因为正式场景下这些数据都来自于后端,后端接口不可能直接返回我们可以使用的Color
类,所以这里使用了hexToInt
函数将字符串转成十六进制的Int
类型才可以色值来使用,最终我们的饼图就绘制出来了
这里绘制出来的是实心的饼图,但有些时候要求饼图是一个环形的,那么这里就需要将style
属性设置为Stroke
,并且传一个环的粗细大小,另外还要将useCenter
属性设置为false
,让扇形两端不要与圆心相连,环形的饼图代码如下
这样就得到了一个环形的饼图
现在我们要让饼图的实心样式与环形样式可配置,要让Pie
函数可以通过一个参数来决定该饼图究竟该使用哪种样式,那么这里我们新创建一个枚举类,里面定义两个值分别代表环形样式与实心样式
然后给Pie
函数新增一个ChartType
的参数,默认值为Full
实心样式,另外圆环的粗细也用一个参数来表示,让它也可以配置,修改后的Pie
函数代码如下所示
动画
绘制部分的代码完成的差不多了,但是目前只能很生硬的将饼图显示出来,这个明显是不够的,任何一个图表组件在展示的时候都会有它自己特有的动画,比如折线图是从第一个点连线到最后一个点,柱状图是每一根柱子逐渐变长,饼图则是从0度开始逐渐展开至360度,那么如何来做呢,要是只有一个扇形要实现一个从0度展开至360的动画,很容易,使用animateFloatAsState
函数,target
值传360度就可以了,但是这里可不仅仅只有一个扇形,是多个扇形,那么就需要创建多个animateFloatAsState
,target
值就是rateList
里面的值,通过遍历来创建多个动画变量,代码如下
这里将创建出来的动画变量放进了diffAngleList
的数组中,并且使用LaunchedEffect
与Flow
制作出了一个定时器,模拟一个接收到数据之后动画展示饼图的过程,然后还要将绘制饼图部分的sweepAngle
的值替换成diffAngleList
里面的值
来看下展示出来的效果如何
可以发现扇形虽然都有了动画效果,但是这些动画并没有连起来,并没有展示出第一个动画结束以后才进行第二个动画的效果,那么如何改呢?我们这里可以使用延迟动画来实现,虽说这些扇形的动画都是同时开始的,但我们可以让除了第一个扇形之外的其他扇形在开始动画前有个延迟开始的时间,这个时间的大小就是前面扇形的动画时间的总和,更改后的代码如下
现在再看下效果就不一样了
有点那个感觉了是不,顺便再试下环形样式的效果
饼图的动画也完成了,接下来就是将饼图的一些数据描述展示出来
展示数据描述
饼图的数据描述基本由色块颜色,名称和数值组成,位置常见的有分布在饼图圆周方向,以及在饼图上下左右某一侧线性排列,所以我们这个组件应该要具备一个可配置描述位置的功能,那么同样给描述位置也添加几个枚举值
同样也给Pie
函数添加一个DescriptionDirection
参数,并且默认值设置为左侧
DescriptionDirection.LEFT
先来画左侧的文案,使用drawText
函数来实现,drawRect
函数来绘制色块
这里首先创建了一个变量mGap
,为什么要创建这个变量呢,这是用来计算drawText
时候topLeft
的位移,因为当饼图是实心的时候,它的半径永远都是radius
,但是当变成环形的时候,饼图的半径就会变成radius+mGap/2
,也就是实心饼图不需要考虑gap
大小,另外还设置了文本大小textSize
,这个大小同时也作为了色块的大小,位置上,每个文本的x坐标都是一样的,都是画布中心x坐标减去radius+mGap/2+200f
,多增加200f
主要是为了不让文本与饼图有重叠部分,色块的x坐标在文本的基础上多增加了两倍的文本大小,为的也是不让色块与文本重叠,而y坐标的逻辑基本都是初始位置在centerY-radius
上,然后逐个递增(radius * 2)/dataList.size
,色块多增加了个textSize的距离,目的也是尽量与文本对齐一些,左侧描述的代码基本都是这样,我们看下效果
上下和右侧的饼图描述基本同左侧一样,只是在topLeft
的计算上有点出入,就不重复讲了,直接上代码跟效果图
DescriptionDirection.RIGHT
DescriptionDirection.TOP
DescriptionDirection.BOTTOM
DescriptionDirection.CIRCLE
最后一个让描述绕着圆周方向,那么每一个描述当然是要在所对应的扇形角度的中心位置,比如每一个描述的位置角度是90f+rateList[it]/2+prefixAngle
,有了角度,计算每一个描述的topLeft
坐标就需要依靠下面两个函数
代入到drawText
函数中,我们绘制文本的代码如下
绘制色块的代码在文本的基础上稍微偏移点位置,代码如下
现在再运行下代码,我们的描述位置就是在圆周方向了
多个维度展示
我们这个组件做的基本差不多了,现在还差最后一步,也就是我们前面讲的都是以一组数据,一个维度来展示饼图,但实际场景下经常会出现多维度的场景,所以我们也需要对这种场景提供支持,这里在创建个函数叫PieChart
,这个函数接收一组多维度数据的PieData
这个函数内部,我们使用一个LazyVerticalGrid
组件来展示我们的饼图,这个组件我们定义为两列,交叉轴上展示两个饼图,代码如下
现在这里的饼图都是一种类型,也就是实心的,描述按照圆周方向排列,我们再定义几组数据传给函数PieChart
得到的效果如下
同样的,我们把样式都切换成环形的再看看效果
总结
这个Compose版本的饼图组件算是完工了,一些视觉方面的东西后期还会再优化下,功能上大致已经实现了饼图组件的基本功能了,但是不能说实现了所有功能,比如鼠标放在某个扇形区域时这个区域会变大,后续会针对这些细节逐个再完善这个组件。