IOS绘制动画颜色渐变折线条

时间:2022-08-24 12:04:27

先给大家展示下效果图:

IOS绘制动画颜色渐变折线条

概述

现状

折线图的应用比较广泛,为了增强用户体验,很多应用中都嵌入了折线图。折线图可以更加直观的表示数据的变化。网络上有很多绘制折线图的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

效果如图

IOS绘制动画颜色渐变折线条

绘制坐标轴信息

与坐标轴绘制相关的常量

?
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);
}];
}

效果如图

IOS绘制动画颜色渐变折线条

渐变背景视图

在分类中添加与背景视图相关的常量

?
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];
}

效果如图

IOS绘制动画颜色渐变折线条

折线

在分类中添加与折线绘制相关的成员变量

?
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;
}

效果如图(初始状态不显示折线)

IOS绘制动画颜色渐变折线条

动画的开始与结束

动画开始

?
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绘制动画颜色渐变折线条就给大家介绍这么多,希望对大家有所帮助!