如何用Objective-C画颜色渐变的贝塞尔曲线

缘由

在iOS开发中,经常需要用到一些图表来展现数据,为了使图表更加美观直白,需要做一些处理,使得图表在准确展现数据的同时,拥有更好的用户体验。这一点,iOS的系统应用为我们做了很好的示范(如健康app的图表效果)。

健康app官网图

为此,我整理了用OC画颜色渐变的贝塞尔曲线的方法。本文以画光谱曲线为例,用光谱波长值对应颜色值并将多种映射到波长值集中的区域,使图表能展示尽可能多种颜色的渐变效果。

  • 效果图

渐变曲线

话不多说,代码奉上。

实现步骤

第一步,绘制坐标系

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

//主网格曲线视图容器

UIView *mainContainer = [UIView new];

_mainContainer = mainContainer;

[self addSubview:mainContainer];

//封闭阴影

CAShapeLayer * backLayer = [CAShapeLayer new];

_backLayer = backLayer;

[mainContainer.layer addSublayer:backLayer];

//网格横线

CAReplicatorLayer *rowReplicatorLayer = [CAReplicatorLayer new];

_rowReplicatorLayer = rowReplicatorLayer;

rowReplicatorLayer.position = CGPointMake(0, 0);

CALayer *rowBackLine = [CALayer new];

_rowBackLine = rowBackLine;

[rowReplicatorLayer addSublayer:rowBackLine];

[mainContainer.layer addSublayer:rowReplicatorLayer];

//网格列线

CAReplicatorLayer *columnReplicatorLayer = [CAReplicatorLayer new];

_columnReplicatorLayer = columnReplicatorLayer;

columnReplicatorLayer.position = CGPointMake(0, 0);

CALayer *columnBackLine = [CALayer new];

_columnBackLine = columnBackLine;

[columnReplicatorLayer addSublayer:columnBackLine];

[mainContainer.layer addSublayer:columnReplicatorLayer];

//行信息labels容器

UIView *rowLabelsContainer = [UIView new];

_rowLabelsContainer = rowLabelsContainer;

[self addSubview:rowLabelsContainer];

//列信息labels容器

UIView *columnLabelsContainer = [UIView new];

_columnLabelsContainer = columnLabelsContainer;

[self addSubview:columnLabelsContainer];

第二步,画贝塞尔曲线

  • 对传入的数据数组进行for循环,在循环中用UIBezierPath创建path对象,设置其起点坐标和终点坐标,画线。
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

NSMutableArray *temp = [NSMutableArray arrayWithArray:_pointValues];

for (NSInteger i = 0; i < _pointArray.count - 1; i ++) {

UIBezierPath * path = [UIBezierPath bezierPath];

NSValue *startPointValue = _pointArray[i];

NSValue *endPointValue = _pointArray[i + 1];

CGPoint startPoint = [startPointValue CGPointValue];

CGPoint endPoint = [endPointValue CGPointValue];

[path moveToPoint:startPoint];

if (startPointValue == _pointArray[0]) {

continue;

}

[path addLineToPoint:endPoint];

//主曲线

path = [path smoothedPathWithGranularity:10];

CAShapeLayer *curveLineLayer = [CAShapeLayer new];

curveLineLayer.backgroundColor = [UIColor whiteColor].CGColor;

curveLineLayer.fillColor = nil;

curveLineLayer.lineJoin = kCALineJoinRound;

curveLineLayer.lineCap = kCALineCapRound;

curveLineLayer.lineWidth = 1.5;

curveLineLayer.path = path.CGPath;



dispatch_async(dispatch_get_main_queue(), ^{

NSDictionary *dict = temp[i];

int yValue = [[dict objectForKey:XWCurveViewPointValuesColumnValueKey] intValue];

UIColor *strokeColor = [self ordinateValue2RGB:yValue];

curveLineLayer.strokeColor = strokeColor.CGColor;

[self.mainContainer.layer addSublayer:curveLineLayer];

curveLineLayer.strokeEnd = 1;

if (_drawWithAnimation) {

CABasicAnimation *pointAnim = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];

pointAnim.fromValue = @0;

pointAnim.toValue = @1;

pointAnim.duration = _drawAnimationDuration;

[_curveLineLayer addAnimation:pointAnim forKey:@"drawLine"];

}

});

}

其中,ordinateValue2RGB方法就是根据纵坐标的波长值对应RGB的算法。该算法是根据波长颜色对应算法针对可视范围的改写。首先将传入值映射关系进行重新对应。

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

- (UIColor *)ordinateValue2RGB:(int)value {

int newValue = 0;

int offset = 1;

if ((value >= 0) && (value < 512)) {

newValue = (int)((double) value / (double)8.53);

newValue = newValue+340;

}else if ((value >= 512) && (value < 2048)) {

offset = value - 512;

newValue = (int)((double) offset / (double)7.68);

newValue = newValue + 400;

}else if (value >= 2048) {

offset = value - 2048;

newValue = (int)((double) offset / (double)8.53);

newValue = newValue + 600;

}

return [self wavelength2RGB:newValue];

}
  • 再根据获取的对应波长计算RGB值。
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

- (UIColor *)wavelength2RGB:(int)wavelength {

double Gamma = 0.50;

int IntensityMax = 255;

double red, green, blue;

if((wavelength >= 340) && (wavelength < 440)){

red = ((double) -(wavelength - 440)) / ((double) (440 - 340));

green = 0.0;

blue = 1.0;

}else if((wavelength >= 440) && (wavelength < 490)){

red = 0.0;

green = ((double)(wavelength - 440)) /((double) (490 - 440));

blue = 1.0;

}else if((wavelength >= 490) && (wavelength < 510)){

red = 0.0;

green = 1.0;

blue = ((double)-(wavelength - 510) )/ ((double)(510 - 490));

}else if((wavelength >= 510) && (wavelength < 580)){

red = ((double)(wavelength - 510)) / ((double) (580 - 510));

green = 1.0;

blue = 0.0;

}else if((wavelength >= 580) && (wavelength < 645)){

red = 1.0;

green = ((double) - (wavelength - 645) ) / ((double) (645 - 580));

blue = 0.0;

}else if((wavelength >= 645) && (wavelength < 781)){

red = 1.0;

green = 0.0;

blue = 0.0;

}else{

red = 0.0;

green = 0.0;

blue = 0.0;

}

double factor;

// Let the intensity fall off near the vision limits

if((wavelength >= 380) && (wavelength < 420)){

factor = 0.3 + 0.7 * (wavelength - 380) / (420 - 380);

}else if((wavelength >= 420) && (wavelength < 701)){

factor = 1.0;

}else if((wavelength >= 701) && (wavelength < 781)){

factor = 0.3 + 0.7 * (780 - wavelength) / (780 - 700);

}else{

factor = 0.0;

}

if (red != 0){

red = round(IntensityMax * pow(red * factor, Gamma));

}

if (green != 0){

green = round(IntensityMax * pow(green * factor, Gamma));

}

if (blue != 0){

blue = round(IntensityMax * pow(blue * factor, Gamma));

}



return [UIColor colorWithRed:(CGFloat)red/255.0 green:(CGFloat)green/255.0 blue:(CGFloat)blue/255.0 alpha:1.0];

}

最后,平滑处理

  • 使用smoothedPathWithGranularity方法对曲线进行平滑处理。
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

- (UIBezierPath*)smoothedPathWithGranularity:(NSInteger)granularity;

{

NSMutableArray *points = [pointsFromBezierPath(self) mutableCopy];



if (points.count < 4) return [self copy];



// Add control points to make the math make sense

[points insertObject:[points objectAtIndex:0] atIndex:0];

[points addObject:[points lastObject]];



UIBezierPath *smoothedPath = [self copy];

[smoothedPath removeAllPoints];



[smoothedPath moveToPoint:POINT(0)];



for (NSUInteger index = 1; index < points.count - 2; index++)

{

CGPoint p0 = POINT(index - 1);

CGPoint p1 = POINT(index);

CGPoint p2 = POINT(index + 1);

CGPoint p3 = POINT(index + 2);



// now add n points starting at p1 + dx/dy up until p2 using Catmull-Rom splines

for (int i = 1; i < granularity; i++)

{

float t = (float) i * (1.0f / (float) granularity);

float tt = t * t;

float ttt = tt * t;



// 中间点

CGPoint pi;

pi.x = 0.5 * (2 * p1.x+ (p2.x- p0.x) * t + (2*p0.x-5*p1.x+4*p2.x-p3.x)*tt + (3*p1.x-p0.x-3*p2.x+p3.x)*ttt);

pi.y = 0.5 * (2*p1.y+(p2.y-p0.y)*t + (2*p0.y-5*p1.y+4*p2.y-p3.y)*tt + (3*p1.y-p0.y-3*p2.y+p3.y)*ttt);

[smoothedPath addLineToPoint:pi];

}



// Now add p2

[smoothedPath addLineToPoint:p2];

}



// finish by adding the last point

[smoothedPath addLineToPoint:POINT(points.count - 1)];



return smoothedPath;

}

再对颜色和背景进行适当美化,就能够画出效果不错的颜色渐变曲线了。