先给大家展示下效果图:
概述
现状
折线图的应用比较广泛,为了增强用户体验,很多应用中都嵌入了折线图。折线图可以更加直观的表示数据的变化。网络上有很多绘制折线图的demo,有的也使用了动画,但是线条颜色渐变的折线图的demo少之又少,甚至可以说没有。该blog阐述了动画绘制线条颜色渐变的折线图的实现方案,以及折线图线条颜色渐变的实现原理,并附以完整的示例。
成果
本人已将折线图封装到了一个uiview子类中,并提供了相应的接口。该自定义折线图视图,基本上可以适用于大部分需要集成折线图的项目。若你遇到相应的需求可以直接将文件拖到项目中,调用相应的接口即可
项目文件中包含了大量的注释代码,若你的需求与折线图的实现效果有差别,那么你可以对项目文件的进行修改,也可以依照思路定义自己的折线图视图
blog中涉及到的知识点
calayer
图层,可以简单的看做一个不接受用户交互的uiview
每个图层都具有一个calayer类型mask属性,作用与蒙版相似
blog中主要用到的calayer子类有
cagradientlayer,绘制颜色渐变的背景图层
cashapelayer,绘制折线图
caanimation
核心动画的基类(不可实例化对象),实现动画操作
quartz 2d
一个二维的绘图引擎,用来绘制折线(path)和坐标轴信息(text)
实现思路
折线图视图
整个折线图将会被自定义到一个uiview子类中
坐标轴绘制
坐标轴直接绘制到折线图视图上,在自定义折线图视图的 drawrect 方法中绘制坐标轴相关信息(线条和文字)
注意坐标系的转换
线条颜色渐变
失败的方案
开始的时候,为了实现线条颜色渐变,我的思考方向是,如何改变路径(uibezierpath)的渲染颜色(strokecolor)。但是strokecolor只可以设置一种,所以最终无法实现线条颜色的渐变。
成功的方案
在探索过程中找到了calayer的calayer类型的mask()属性,最终找到了解决方案,即:使用uiview对象封装渐变背景视图(frame为折线图视图的减去坐标轴后的frame),创建一个cagradientlayer渐变图层添加到背景视图上。
创建一个cashapelayer对象,用于绘制线条,线条的渲染颜色(strokecolor)为whitecolor,填充颜色(fillcolor)为clearcolor,从而显示出渐变图层的颜色。将cashapelayer对象设置为背景视图的mask属性,即背景视图的蒙版。
折线
使用 uibezierpath 类来绘制折线
折线转折处尖角的处理,使用 kcalinecapround 与 kcalinejoinround 设置折线转折处为圆角
折线起点与终点的圆点的处理,可以直接在 uibezierpath 对象上添加一个圆,设置远的半径为路径宽度的一半,从而保证是一个实心的圆而不是一个圆环
折线转折处的点
折线转折处点使用一个类来描述(不使用cgpoint的原因是:折线转折处的点需要放到一个数组中)
坐标轴信息
x轴、y轴的信息分别放到一个数组中
x轴显示的是最近七天的日期,y轴显示的是最近七天数据变化的幅度
动画
使用cabasicanimation类来完成绘制折线图时的动画
需要注意的是,折线路径在一开始时需要社会线宽为0,开始绘制时才设置为适当的线宽,保证一开折线路径是隐藏的
标签
在动画结束时,向折线图视图上添加一个标签(uibutton对象),显示折线终点的信息
标签的位置,需要根据折线终点的位置计算
具体实现
折线转折处的点
使用一个类来描述折线转折处的点,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 接口 /** 折线图上的点 */ @interface idlinechartpoint : nsobject /** x轴偏移量 */ @property (nonatomic, assign) float x; /** y轴偏移量 */ @property (nonatomic, assign) float y; /** 工厂方法 */ + (instancetype)pointwithx:( float )x andy:( float )y; @end // 实现 @implementation idlinechartpoint + (instancetype)pointwithx:( float )x andy:( float )y { idlinechartpoint *point = [[self alloc] init]; point.x = x; point.y = y; return point; } @end |
自定义折线图视图
折线图视图是一个自定义的uiview子类,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 接口 /** 折线图视图 */ @interface idlinechartview : uiview /** 折线转折点数组 */ @property (nonatomic, strong) nsmutablearray<idlinechartpoint *> *pointarray; /** 开始绘制折线图 */ - ( void )startdrawlinechart; @end // 分类 @interface idlinechartview () @end // 实现 @implementation idlinechartview // 初始化 - (instancetype)initwithframe:(cgrect)frame { if (self = [super initwithframe:frame]) { // 设置折线图的背景色 self.backgroundcolor = [uicolor colorwithred:243/255.0 green:243/255.0 blue:243/255.0 alpha:1.0]; } return self; } @end |
效果如图
绘制坐标轴信息
与坐标轴绘制相关的常量
1
2
3
4
|
/** 坐标轴信息区域宽度 */ static const cgfloat kpadding = 25.0; /** 坐标系中横线的宽度 */ static const cgfloat kcoordinatelinewith = 1.0; |
在分类中添加与坐标轴绘制相关的成员变量
1
2
3
4
5
6
7
8
|
/** x轴的单位长度 */ @property (nonatomic, assign) cgfloat xaxisspacing; /** y轴的单位长度 */ @property (nonatomic, assign) cgfloat yaxisspacing; /** x轴的信息 */ @property (nonatomic, strong) nsmutablearray<nsstring *> *xaxisinformationarray; /** y轴的信息 */ @property (nonatomic, strong) nsmutablearray<nsstring *> *yaxisinformationarray; |
与坐标轴绘制相关的成员变量的get方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
- (cgfloat)xaxisspacing { if (_xaxisspacing == 0) { _xaxisspacing = (self.bounds.size.width - kpadding) / ( float )self.xaxisinformationarray.count; } return _xaxisspacing; } - (cgfloat)yaxisspacing { if (_yaxisspacing == 0) { _yaxisspacing = (self.bounds.size.height - kpadding) / ( float )self.yaxisinformationarray.count; } return _yaxisspacing; } - (nsmutablearray<nsstring *> *)xaxisinformationarray { if (_xaxisinformationarray == nil) { // 创建可变数组 _xaxisinformationarray = [[nsmutablearray alloc] init]; // 当前日期和日历 nsdate *today = [nsdate date]; nscalendar *currentcalendar = [nscalendar currentcalendar]; // 设置日期格式 nsdateformatter *dateformatter = [[nsdateformatter alloc] init]; dateformatter.dateformat = @ "mm-dd" ; // 获取最近一周的日期 nsdatecomponents *components = [[nsdatecomponents alloc] init]; for ( int i = -7; i<0; i++) { components.day = i; nsdate *dayoflatestweek = [currentcalendar datebyaddingcomponents:components todate:today options:0]; nsstring *datestring = [dateformatter stringfromdate:dayoflatestweek]; [_xaxisinformationarray addobject:datestring]; } } return _xaxisinformationarray; } - (nsmutablearray<nsstring *> *)yaxisinformationarray { if (_yaxisinformationarray == nil) { _yaxisinformationarray = [nsmutablearray arraywithobjects:@ "0" , @ "10" , @ "20" , @ "30" , @ "40" , @ "50" , nil]; } return _yaxisinformationarray; } |
绘制坐标轴的相关信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
- ( void )drawrect:(cgrect)rect { // 获取上下文 cgcontextref context = uigraphicsgetcurrentcontext(); // x轴信息 [self.xaxisinformationarray enumerateobjectsusingblock:^(nsstring * _nonnull obj, nsuinteger idx, bool * _nonnull stop) { // 计算文字尺寸 uifont *informationfont = [uifont systemfontofsize:10]; nsmutabledictionary *attributes = [nsmutabledictionary dictionary]; attributes[nsforegroundcolorattributename] = [uicolor colorwithred:158/255.0 green:158/255.0 blue:158/255.0 alpha:1.0]; attributes[nsfontattributename] = informationfont; cgsize informationsize = [obj sizewithattributes:attributes]; // 计算绘制起点 float drawstartpointx = kpadding + idx * self.xaxisspacing + (self.xaxisspacing - informationsize.width) * 0.5; float drawstartpointy = self.bounds.size.height - kpadding + (kpadding - informationsize.height) / 2.0; cgpoint drawstartpoint = cgpointmake(drawstartpointx, drawstartpointy); // 绘制文字信息 [obj drawatpoint:drawstartpoint withattributes:attributes]; }]; // y轴 [self.yaxisinformationarray enumerateobjectsusingblock:^(nsstring * _nonnull obj, nsuinteger idx, bool * _nonnull stop) { // 计算文字尺寸 uifont *informationfont = [uifont systemfontofsize:10]; nsmutabledictionary *attributes = [nsmutabledictionary dictionary]; attributes[nsforegroundcolorattributename] = [uicolor colorwithred:158/255.0 green:158/255.0 blue:158/255.0 alpha:1.0]; attributes[nsfontattributename] = informationfont; cgsize informationsize = [obj sizewithattributes:attributes]; // 计算绘制起点 float drawstartpointx = (kpadding - informationsize.width) / 2.0; float drawstartpointy = self.bounds.size.height - kpadding - idx * self.yaxisspacing - informationsize.height * 0.5; cgpoint drawstartpoint = cgpointmake(drawstartpointx, drawstartpointy); // 绘制文字信息 [obj drawatpoint:drawstartpoint withattributes:attributes]; // 横向标线 cgcontextsetrgbstrokecolor(context, 231 / 255.0, 231 / 255.0, 231 / 255.0, 1.0); cgcontextsetlinewidth(context, kcoordinatelinewith); cgcontextmovetopoint(context, kpadding, self.bounds.size.height - kpadding - idx * self.yaxisspacing); cgcontextaddlinetopoint(context, self.bounds.size.width, self.bounds.size.height - kpadding - idx * self.yaxisspacing); cgcontextstrokepath(context); }]; } |
效果如图
渐变背景视图
在分类中添加与背景视图相关的常量
1
2
3
4
5
6
|
/** 渐变背景视图 */ @property (nonatomic, strong) uiview *gradientbackgroundview; /** 渐变图层 */ @property (nonatomic, strong) cagradientlayer *gradientlayer; /** 颜色数组 */ @property (nonatomic, strong) nsmutablearray *gradientlayercolors; |
在初始化方法中添加调用设置背景视图方法的代码
设置渐变视图方法的具体实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
- ( void )drawgradientbackgroundview { // 渐变背景视图(不包含坐标轴) self.gradientbackgroundview = [[uiview alloc] initwithframe:cgrectmake(kpadding, 0, self.bounds.size.width - kpadding, self.bounds.size.height - kpadding)]; [self addsubview:self.gradientbackgroundview]; /** 创建并设置渐变背景图层 */ //初始化cagradientlayer对象,使它的大小为渐变背景视图的大小 self.gradientlayer = [cagradientlayer layer]; self.gradientlayer.frame = self.gradientbackgroundview.bounds; //设置渐变区域的起始和终止位置(范围为0-1),即渐变路径 self.gradientlayer.startpoint = cgpointmake(0, 0.0); self.gradientlayer.endpoint = cgpointmake(1.0, 0.0); //设置颜色的渐变过程 self.gradientlayercolors = [nsmutablearray arraywitharray:@[(__bridge id)[uicolor colorwithred:253 / 255.0 green:164 / 255.0 blue:8 / 255.0 alpha:1.0].cgcolor, (__bridge id)[uicolor colorwithred:251 / 255.0 green:37 / 255.0 blue:45 / 255.0 alpha:1.0].cgcolor]]; self.gradientlayer.colors = self.gradientlayercolors; //将cagradientlayer对象添加在我们要设置背景色的视图的layer层 [self.gradientbackgroundview.layer addsublayer:self.gradientlayer]; } |
效果如图
折线
在分类中添加与折线绘制相关的成员变量
1
2
3
4
|
/** 折线图层 */ @property (nonatomic, strong) cashapelayer *linechartlayer; /** 折线图终点处的标签 */ @property (nonatomic, strong) uibutton *tapbutton; |
在初始化方法中添加调用设置折线图层方法的代码
1
|
[self setuplinechartlayerappearance]; |
设置折线图层方法的具体实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
- ( void )setuplinechartlayerappearance { /** 折线路径 */ uibezierpath *path = [uibezierpath bezierpath]; [self.pointarray enumerateobjectsusingblock:^(idlinechartpoint * _nonnull obj, nsuinteger idx, bool * _nonnull stop) { // 折线 if (idx == 0) { [path movetopoint:cgpointmake(self.xaxisspacing * 0.5 + (obj.x - 1) * self.xaxisspacing, self.bounds.size.height - kpadding - obj.y * self.yaxisspacing)]; } else { [path addlinetopoint:cgpointmake(self.xaxisspacing * 0.5 + (obj.x - 1) * self.xaxisspacing, self.bounds.size.height - kpadding - obj.y * self.yaxisspacing)]; } // 折线起点和终点位置的圆点 if (idx == 0 || idx == self.pointarray.count - 1) { [path addarcwithcenter:cgpointmake(self.xaxisspacing * 0.5 + (obj.x - 1) * self.xaxisspacing, self.bounds.size.height - kpadding - obj.y * self.yaxisspacing) radius:2.0 startangle:0 endangle:2 * m_pi clockwise:yes]; } }]; /** 将折线添加到折线图层上,并设置相关的属性 */ self.linechartlayer = [cashapelayer layer]; self.linechartlayer.path = path.cgpath; self.linechartlayer.strokecolor = [uicolor whitecolor].cgcolor; self.linechartlayer.fillcolor = [[uicolor clearcolor] cgcolor]; // 默认设置路径宽度为0,使其在起始状态下不显示 self.linechartlayer.linewidth = 0; self.linechartlayer.linecap = kcalinecapround; self.linechartlayer.linejoin = kcalinejoinround; // 设置折线图层为渐变图层的mask self.gradientbackgroundview.layer.mask = self.linechartlayer; } |
效果如图(初始状态不显示折线)
动画的开始与结束
动画开始
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/** 动画开始,绘制折线图 */ - ( void )startdrawlinechart { // 设置路径宽度为4,使其能够显示出来 self.linechartlayer.linewidth = 4; // 移除标签, if ([self.subviews containsobject:self.tapbutton]) { [self.tapbutton removefromsuperview]; } // 设置动画的相关属性 cabasicanimation *pathanimation = [cabasicanimation animationwithkeypath:@ "strokeend" ]; pathanimation.duration = 2.5; pathanimation.repeatcount = 1; pathanimation.removedoncompletion = no; pathanimation.fromvalue = [nsnumber numberwithfloat:0.0f]; pathanimation.tovalue = [nsnumber numberwithfloat:1.0f]; // 设置动画代理,动画结束时添加一个标签,显示折线终点的信息 pathanimation.delegate = self; [self.linechartlayer addanimation:pathanimation forkey:@ "strokeend" ]; } |
动画结束,添加标签
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/** 动画结束时,添加一个标签 */ - ( void )animationdidstop:(caanimation *)anim finished:( bool )flag { if (self.tapbutton == nil) { // 首次添加标签(避免多次创建和计算) cgrect tapbuttonframe = cgrectmake(self.xaxisspacing * 0.5 + ([self.pointarray[self.pointarray.count - 1] x] - 1) * self.xaxisspacing + 8, self.bounds.size.height - kpadding - [self.pointarray[self.pointarray.count - 1] y] * self.yaxisspacing - 34, 30, 30); self.tapbutton = [[uibutton alloc] initwithframe:tapbuttonframe]; self.tapbutton.enabled = no; [self.tapbutton setbackgroundimage:[uiimage imagenamed:@ "bubble" ] forstate:uicontrolstatedisabled]; [self.tapbutton.titlelabel setfont:[uifont systemfontofsize:10]]; [self.tapbutton settitle:@ "20" forstate:uicontrolstatedisabled]; } [self addsubview:self.tapbutton]; } |
集成折线图视图
创建折线图视图
添加成员变量
1
2
|
/** 折线图 */ @property (nonatomic, strong) idlinechartview *linecharview; |
在viewdidload方法中创建折线图并添加到控制器的view上
1
2
|
self.linecharview = [[idlinechartview alloc] initwithframe:cgrectmake(35, 164, 340, 170)]; [self.view addsubview:self.linecharview]; |
添加开始绘制折线图视图的按钮
添加成员变量
1
2
|
/** 开始绘制折线图按钮 */ @property (nonatomic, strong) uibutton *drawlinechartbutton; |
在viewdidload方法中创建开始按钮并添加到控制器的view上
1
2
3
4
5
6
7
8
9
10
|
self.drawlinechartbutton = [uibutton buttonwithtype:uibuttontypesystem]; self.drawlinechartbutton.frame = cgrectmake(180, 375, 50, 44); [self.drawlinechartbutton settitle:@ "开始" forstate:uicontrolstatenormal]; [self.drawlinechartbutton addtarget:self action:@selector(drawlinechart) forcontrolevents:uicontroleventtouchupinside]; [self.view addsubview:self.drawlinechartbutton]; 开始按钮的点击事件 // 开始绘制折线图 - ( void )drawlinechart { [self.linecharview startdrawlinechart]; } |
好了,关于ios绘制动画颜色渐变折线条就给大家介绍这么多,希望对大家有所帮助!