PNChart 源码解析

一. 框架介绍

PNChart是国内开发者开发的iOS图表框架,现在已经7900多颗star了。它涵盖了折线图,饼图,散点图等图表。图表的可定制性很高,而且UI设计简洁大方。

该框架分为两层:视图层和数据层。视图层里有两层继承关系,第一层是所有类型图表的父类PNGenericChart,第二层就是所有类型的图表。提供一张图来直观感受一下:

11859001-8be6cd678806d45e

层级图

在这张图里,需要注意以下几点:

  1. 带箭头的线和不带箭头的线的区别。
  2. Data类对应图表的一组数据,因为当前类型的图表支持多组数据(例如:饼状图没有Data类,因为饼状图没有多组数据,而折线图LineChart是支持多组数据的,所以有Data类。
  3. Item类负责将传入图表的某个真实值转化为图表中显示的值,具体做法会在下文详细讲解。
  4. BarChart类里面的每一根柱子都是PNBar的实例(该类型的图表不在本篇讲解的范围之内)。

今天就来介绍一下该框架里的折线图。一旦学会了折线图的绘制,了解了绘图原理,那么其他类型的图表就可以触类旁通。

上文提到过,该框架的折线图是支持多组数据的,也就是在同一张图表上显示多条折线。先带大家看一下效果图:

12859001-7357e47c2b0b062b

折线图

折线图在效果上还是很简洁美观的(并支持动画效果),如果现在的你还不知道如何使用CAShapeLayerUIBezierPath画图并附加动画效果,那么本篇源码解析非常适合你。

阅读本文之后,你可以掌握有关图形绘制的相关知识,也可以掌握自定义各种图形(UIView)的方法,而且你也应该有能力作出这样的图表,甚至更好!

在开始讲解之前,我先粗略介绍一下利用CAShapeLayer画图的过程。这个过程有三个大前提:

  • 因为UIView是对CALayer的封装,所以我们可以通过改变UIView所持有的layer属性来直接改变UIView的显示效果。
  • CAShapeLayerCALayer的子类。
  • CAShapeLayer的使用是依赖于UIBezierPath的。UIBezierPath就是“路径”,可以理解为形状。不难理解,想象一下,如果我们想画一个图形,那么这个图形的形状(包括颜色)是必不可少的,而这个角色,就需要UIBezierPath来充当。

那么了这三个大前提,我们就可以知道如何画图了:

  1. 实例化一个UIBezierPath,并赋给CAShapeLayer实例的path属性。
  2. 将这个CAShapeLayer的实例添加到UIViewlayer上。

简单的代码演示上述过程:

现在大致了解了画图的过程,我们来看一下该框架的作者是如何实现一个折线图的吧!

二. 源码解析

首先看一下整个绘制折线图的步骤:

  1. 图表的初始化。
  2. 获取横轴和纵轴的数据。
  3. 计算折线上所有拐点的x,y值。
  4. 计算每个拐点中间的圆圈的贝塞尔曲线(UIBezierPath)。
  5. 生成每个拐点上面的Label(可有可无)。
  6. 计算每条线段的贝塞尔曲线(UIBezierPath)。
  7. 将上面得到的贝塞尔曲线赋给每条线段和圆圈的layer(CAShapeLayer)。
  8. 绘制所有折线(所有线段+所有圆圈)。
  9. 添加动画(可有可无)。
  10. 绘制x,y坐标轴。

在集合代码具体讲解之前,我们要清楚三点(非常非常重要):

  1. 此折线图框架是可以设置拐点的样式的:可以设置为没有样式,也可以设置有样式:圆圈,方块,三角形。
    • 如果没有样式,则是简单的线段与线段的连接,在拐点处没有任何其他控件。
    • 如果是有样式的,那么这条折线里的每条线段(在本篇文章里统一说成线段)之间是分离的,因为线段中间有一个拐点控件。本篇文章介绍的是圆圈样式(如上图所示,拐点控件是一个圆圈)。
  2. 上文提到过,该折线图框架可以在一张图表里同时显示多条折线,也就是可以设置多组数据(一条折线对应一组数据)。因此,上面的3,4,5,6,7项都是用各自不同的一个数组保存的,数组里的每一个元素对应一条折线的数据。
  3. 既然同一个张图表可以显示多条折线:
    • 那么有些属性就是这些折线共有的,比如横坐标的value,这些属性保存在PNLineChart的实例里面。
    • 有些属性是每条折线私有的,比如每条折线的颜色,纵坐标value等等,这些属性保存在PNLineChartData里面。每一条折线对应一个PNLineChartData实例。这些实例汇总到一个数组里面,这个数组由PNLineChart的实例管理。

在充分了解了这三点之后,我们结合一下代码来看一下具体的实现:

1. 图表的初始化

上面这段代码我刻意省去了其他一些基本的设置,突出了图表布局的设置。

布局的设置是图表绘制的前提,因为在最开始的时候,就应该计算出“画布”,也就是图表内容(不包括坐标轴和坐标label)的具体大小和位置(内边距以内的部分)。

在这里,我们需要获取真正绘制图表的画布的宽高(_chartCavanWidth_chartCavanHeight)。而且,要留意的是_chartMarginLeft在将来是要用作y轴Label的宽度,而_chartMarginBottom在将来是要用作x轴Label的高度的。

用一张图直观看一下:

13859001-958e7d4172c18f55

整个控件的大小和画布的大小

2. 获取横轴和纵轴的数据

现在画布的位置和大小确定了,我们可以来看一下折线图是怎么画的了。
整个图表的绘制都基于三组数据(也可以是两组,为什么是两组,我稍后会给出解释),在讲解该框架是如何利用这些数据之前,我们来看一下这些数据是如何传进图表的:

上面的代码我可以略去了很多多余的设置,目的是突出图表数据的设置。

不难看出,这里有三个数据传给了lineChart:

1.x轴的数据:

这段代码调用之后,实现了:

  1. 根据传入的xLabel数组里元素的数量,内容宽度(_chartCavanWidth)和下边距(_chartMarginBottom),计算每个xlabel的size。
  2. 根据xLabel所需要展示的内容(NSString)和宽度,实例化所有的xLabel(包括内容,位置)并显示出来,最后保存在_xChartLabels里面。

2.y轴的数据:

这段代码调用之后,实现了:

  1. 根据传入的yLabel数组里元素的数量,内容高度(_chartCavanHeight)和左边距(_chartMarginLeft),计算出每个ylabel的size。
  2. 根据xLabel所需要展示的内容(NSString)和宽度,实例化所有的yLabel(包括内容,位置)并显示出来,最后保存在_yChartLabels里面。

3.一条折线上每个点的实际值:

着重讲一下block:为什么不直接把这个数组(dataArray)作为line chart的属性传进去呢?我认为作者是想提供一个接口给用户一个自己转化y值的机会。

像上文所说的,这里1,2是属于lineChart的数据,它适用于这张图表上所有的折线的。而3是属于某一条折线的。

现在回答一下为什么可以只传入两组数据:因为y轴数据可以由每个点的实际值数组得出。可以简单想一下,我们可以获取这些真实值里面的最大值,然后将它n等分,就自然得到了y轴数据了。

我们已经布局了x轴和y轴的所有label,现在开始真正计算图表的数据了。

注意:下面要介绍的3,4,5,6项都是在同一方法中计算出来,为了避免代码过长,我将每个部分分解开来做出解释。因为在同一方法里,所以这些涉及到for循环的语句是一致的。

整个图表的绘制都是依赖于数据的处理,所以3,4,5,6项也是理解该框架的一个关键!

首先,我们需要计算每个数据点(拐点)的准确位置:

3. 计算折线上所有拐点的x,y值。

在这里需要注意两点:

  1. 这里的pathPoints对应的是lineChart_pathPoints属性。它是一个二维数组,保存每条折线上所有点的CGPoint
  2. y值的计算:是需要从y的真实值转化为这个拐点在图表里的y坐标,转化方法的实现(仔细看几遍就懂了):

4. 计算每个拐点中间的圆圈的贝塞尔曲线(UIBezierPath)

在这里,pointsPath对应的是lineChart_pointsPath属性。它是一个一维数组,保存每条折线上的圆圈贝塞尔曲线(UIBezierPath)。

5. 生成每个拐点上面的Label(可有可无)

注意,在这里,这些label的实现是通过一个CATextLayer实现的,并不是生成一个个Label放在数组里保存,具体实现方法如下:

6. 计算每条线段的贝塞尔曲线(UIBezierPath)

7. 将上面得到的贝塞尔曲线赋给每条线段和圆圈的layer(CAShapeLayer)。

7.1 所有线段的layer:

7.2 所有圆圈的layer:

注意,这里并没有将所有圆圈的UIBezierPath赋给对应的layer,而是在下一步,绘图的时候做的。

8.绘制所有折线(所有线段+所有圆圈)&& 9. 添加动画

这里要注意两点:

1.如果想给layer添加动画,只需要实例化一个animation(在这里是CABasicAnimation)并调用layer的addAnimation:方法即可。我们看一下关于CABasicAnimation的实例化代码:

2.在这里调用了setNeedsDisplay方法之后,会调用drawRect:方法,在这个方法里,完成了x,y坐标轴的绘制:

10.绘制x,y坐标轴

到这里,一张完整的图表就可以画出来了。但是当前绘制的图表的折线都是直线,在上面还展示了一张曲线图。那么如果想绘制带有曲线的折线图应该怎么做呢?对,就是在贝塞尔曲线上下功夫。

当我们获取了所有线段的端点数组后,我们可以通过他们绘制弯曲的贝塞尔曲线(注意:该方法是对应上面对第6项的下半部分:生成每一个线段对贝塞尔曲线):

注意一下生成弯曲的贝塞尔曲线的方法:controlPointBetweenPoint1:andPoint2:

OK,这样一来,直线的曲线图还有曲线的曲线图就大概掌握了。不过还差一个东西,就是图表对点击的响应。

我们需要思考一下:既然一张图表里可以显示多条折线,所以,当手指点击图表上的点以后,应该同时返回两个数据:

  1. 点击了哪条折线上的这个点。
  2. 点击了这条折线上的哪个点。

该框架的作者很好地完成了这两个任务,我们来看一下他是如何实现的:

响应点击的代理方法

点击了哪条折线的判断

点击了哪个点的判断

这下就完整了,一个带有响应功能的图表就做好啦!

关于自定义UIView

这里只是将图表的layer加在了UIView的layer上,那如果想完全自定义view的话,只需将图表的layer完全赋给UIView的layer即可,这样一来,想要画出任意形状的UIView都可以。

三. 最后的话

关于图表的绘制,相对贝塞尔曲线与CALayer来说,数据的处理是一个比较麻烦的点。但是一旦学会了折线图的绘制,了解了绘图原理,那么其他类型的图表就可以触类旁通。

 

1 2 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部