从一个实际问题说说CAShapelayer

Core Animation图层不仅仅只有CALayer这种简单的图片和颜色绘制的功能,还有一些专用图层,如:CAShapeLayer、CATextLayer、CAGradientLayer、CAEAGLLayer、AVPlayerLayer、CAScrollLayer等。我在过去的工作中用过其中的大部分,而使用频率最高的应该就是CAShapeLayer。

CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。通过指定颜色和线宽等属性,用CGPath来指定想要绘制的形状,CAShapeLayer就能自动渲染出想要的图形。相比于CALayer直接通过Core Graphics进行内容绘制的方式,CAShapeLayer有以下一些优点:

  • CAShapeLayer使用硬件加速,绘制同一图形会比用Core Graphics快得多
  • CAShapeLayer不需要像CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存
  • CAShapeLayer做2D或者3D变换时,不会出现像素化
  • CAShapeLayer可以非常方便的进行动画操作,实现复杂的形状变换

说了很多CAShapeLayer的优势,我们来看一个具体的问题,某一天的某一个产品迭代中接到这样一个需求:


需求
  • 笛卡尔2D坐标系
  • X轴Y轴根据数据实时缩放比例尺
  • 给定数据节点,通过光滑曲线串连起来
  • 曲线下面是带有颜色渐变的堆积图
  • Y轴数据最小值为0,最大值不确定
  • X轴数据为日期,最多展示15天数据

并不是很复杂的系统,有很多种实现方案,这里我们要通过CAShapeLayer把它实现出来。

数据处理

由于我们拿到的Y轴数据的变化区间是不确定的,因此在绘图之前要做一点必要的比例尺缩放。我们限定Y轴坐标最多进行5等份展示,因此如果我们拿到的Y轴数据<5,那就按[0,1,2,3,4,5]进行划分;如果>5,就按照n=ceil(max/5)进行等分。比如max=15,则等分区间为[0,3,6,9,12,15]

X轴数据有最大限定15,因此处理起来要简单的多,设X轴日期天数为n,X轴宽度为W,则X轴的等分间距xw=W/n

代码实现如下:

- (void)initData {
    _yMaxAmount = 5;
    _maxValue = 0;
    _xAmount = self.data.count;
    _xInterval = (self.bounds.size.width - _insets.left - _insets.right)/(_xAmount - 1);
    
    [self.data enumerateObjectsUsingBlock:^(ShowDataModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (obj.pageView > _maxValue) {
            _maxValue = obj.pageView;
        }
    }];
    
    // 异常数据处理
    if (_maxValue <= 0) {
        _maxValue = 5;
    }
    
    CGFloat height = (self.bounds.size.height - _insets.top - _insets.bottom);
    if (_maxValue < _yMaxAmount) {
        _yAmount = _maxValue;
        _yInterval = height/_yAmount;
    }else {
        NSInteger tYAmount = ceilf((0.0 + _maxValue)/_yMaxAmount);
        _yAmount = _yMaxAmount;
        _yInterval = height/_yMaxAmount;
        _maxValue = tYAmount * _yMaxAmount;
    }
}

绘制坐标轴

坐标轴不需要频繁的变换,也不需要进行动画,因此我们简单的通过Core Graphics来完成

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // Y轴坐标
    CGContextMoveToPoint(context, _insets.left, _insets.top - 10);
    CGContextAddLineToPoint(context, _insets.left, self.bounds.size.height - _insets.bottom);
    CGContextSetLineWidth(context, SCREEN_SCALE);
    CGContextSetRGBStrokeColor(context, 0.9, 0.9, 0.9, 1.0);
    CGContextStrokePath(context);
    
    // X轴坐标
    CGContextMoveToPoint(context, _insets.left, self.bounds.size.height - _insets.bottom);
    CGContextAddLineToPoint(context, self.bounds.size.width - _insets.right, self.bounds.size.height - _insets.bottom);
    
    // X轴坐标等间隔短线
    for (NSInteger i = 0; i < _xAmount; i++) {
        CGContextMoveToPoint(context, _insets.left + _xInterval * i, self.bounds.size.height - _insets.bottom);
        CGContextAddLineToPoint(context, _insets.left + _xInterval * i, self.bounds.size.height - _insets.bottom - 2.5);
    }
    CGContextSetLineWidth(context, SCREEN_SCALE);
    CGContextSetRGBStrokeColor(context, 0.9, 0.9, 0.9, 1.0);
    CGContextStrokePath(context);
    
    // Y轴坐标等间隔长横线
    for (NSInteger i = 1; i < _yAmount + 1; i++) {
        CGContextMoveToPoint(context, _insets.left, self.bounds.size.height - _insets.bottom - i * _yInterval);
        CGContextAddLineToPoint(context, self.bounds.size.width - _insets.right, self.bounds.size.height - _insets.bottom - i * _yInterval);
    }
    CGContextSetLineWidth(context, SCREEN_SCALE);
    CGContextSetRGBStrokeColor(context, 0.968, 0.968, 0.968, 1.0);
    CGContextStrokePath(context);
}

没有多少需要解释的,简单的直线绘制

绘制曲线

接下来就用到CAShapeLayer了,我们需要绘制一条具有一定宽度的连续的曲线,基本设置如下:

self.lineLayer = [CAShapeLayer layer];
self.lineLayer.lineWidth = 2;
self.lineLayer.fillColor = [UIColor clearColor].CGColor;
self.lineLayer.strokeColor = COLOR_NAV_BAR.CGColor;
[self.layer addSublayer:self.lineLayer];
self.lineLayer.strokeStart = 0;
self.lineLayer.strokeEnd = 1.0;

我们还需要一个path来指定曲线的路径,我们通过UIBezierPath来设定。要使用Bezier曲线,就需要解决控制点的问题。这里的曲线绘制不要求非常高的精度,因此控制点的选取可以简单的采取如下方案:设上一个数据点为fp,当前数据点为p,则控制点c0 = ((fp.x + px)/2, fp.y)),c1 = ((fp.x + px)/2, p.y))。当然这样计算控制点是无法保证整条曲线的连续性的,要保证整条曲线的连续性可参考之前写的一篇文章:如何画出一条优雅的曲线

UIBezierPath *linePath = [UIBezierPath bezierPath];
    
for (NSInteger i = 0; i < _maxPoint; i++) {
    CGPoint p = [self pointAtIndex:i scale:1];
    if (i == 0) {
        [linePath moveToPoint:p];
        [areaPath moveToPoint:p];
    }else if(i < self.data.count){
        CGPoint fp = [self pointAtIndex:i - 1 scale:1];
        CGPoint controlPoint1 = CGPointMake((fp.x + p.x)/ 2 , fp.y);
        CGPoint controlPoint2 = CGPointMake((fp.x + p.x)/ 2, p.y);
        [linePath addCurveToPoint:p controlPoint1:controlPoint1 controlPoint2:controlPoint2];
    }else {
        [linePath addLineToPoint:p];
    }
}

self.lineLayer.path = linePath.CGPath;

给CAShapeLayer设置path之后曲线就绘制完成了,简单快速

绘制堆积图

曲线已经绘制完成了,接下来绘制曲线下面的堆积图。如果堆积图只是一个颜色那么绘制起来要简单的多,只需要把上一步绘制的曲线闭合之后填充颜色就能得到我们需要的堆积图。带有过度颜色的堆积图绘制起来需要一点技巧。我们先用一个CAGradientLayer作为过渡颜色的画板,然后通过CAShapeLayer完成堆积图形状的绘制,最后把CAShapeLayer赋值给CAGradientLayer的mask。这也是CAShapeLayer的另一个用法,你可以通过CAShapeLayer绘制出想要的形状,然后把它赋值给任何layer的mask,就可以得到各种新奇精彩的图形。

self.areaMaskLayer = [CAShapeLayer layer];
self.areaMaskLayer.lineWidth = 0;
self.areaMaskLayer.fillColor = [UIColor redColor].CGColor;
self.areaMaskLayer.strokeColor = [UIColor clearColor].CGColor;
self.areaMaskLayer.strokeStart = 0;
self.areaMaskLayer.strokeEnd = 0;
        
self.areaLayer = [CAGradientLayer layer];
[self.areaLayer setColors :[NSArray arrayWithObjects:(id)[[UIColor colorWithHexStr:@"#00cdd1" Alpha:0.3] CGColor ],(id)[[UIColor colorWithHexStr:@"#00cdd1" Alpha:0.0] CGColor],nil]];
[self.areaLayer setEndPoint:CGPointMake (0.5,1)];
[self.areaLayer setStartPoint:CGPointMake(0.5,0)];
[self.layer addSublayer:self.areaLayer];
[self.areaLayer setMask:self.areaMaskLayer];

如上初始化颜色渐变图层CAGradientLayer,和CAGradientLayer图层的mask图层CAShapeLayer

UIBezierPath *areaPath = [UIBezierPath bezierPath];
    
for (NSInteger i = 0; i < _maxPoint; i++) {
    CGPoint p = [self pointAtIndex:i scale:1];
    if (i == 0) {
        [areaPath moveToPoint:p];
    }else if(i < self.data.count){
        CGPoint fp = [self pointAtIndex:i - 1 scale:1];
        CGPoint controlPoint1 = CGPointMake((fp.x + p.x)/ 2 , fp.y);
        CGPoint controlPoint2 = CGPointMake((fp.x + p.x)/ 2, p.y);
        [areaPath addCurveToPoint:p controlPoint1:controlPoint1 controlPoint2:controlPoint2];
    }else {
        [areaPath addLineToPoint:p];
    }
}
    
CGPoint p0 = CGPointMake(self.bounds.size.width - _insets.right, self.bounds.size.height - _insets.bottom);
[areaPath addLineToPoint:p0];
    
CGPoint p1 = CGPointMake(_insets.left, self.bounds.size.height - _insets.bottom);
[areaPath addLineToPoint:p1];
    
[areaPath closePath];

self.areaMaskLayer.path = areaPath.CGPath;

与linePath唯一不同的地方就是areaPath需要把曲线封闭起来。这样曲线下面的堆积图也绘制完成了,跟曲线绘制一样简单方便。

曲线动画

上面已经介绍了关于CAGradientLayer绘图的相关知识,当然CAGradientLayer的强大功能还不止如此,接下来我们看下CAGradientLayer的动画功能

曲线动画

如上我们希望在数据切换的时候能够以更加流畅的方式体现出来,曲线能从一种形态平滑的过渡到另一种形态。这个时候我们就需要用到CAGradientLayer的path的动画。我们需要提供CAGradientLayer变换之前的path和变换之后的path,然后构建path的动画。

- (void)lineChangeAnimation:(CGPathRef)pathRef {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"];
    animation.fromValue = (__bridge id)self.lineLayer.path;
    animation.toValue = (__bridge id)pathRef;
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    animation.duration = .5;
    [self.lineLayer addAnimation:animation forKey:@"path"];
    self.lineLayer.path = pathRef;
}

是不是很简单,只要设置好fromValue和toValue就可以了。

曲线未曾使用的动画

除了上面讲的CAGradientLayer的path的动画,CAGradientLayer还提供基于strokeEnd的动画。CAGradientLayer的strokeStart和strokeEnd代表了曲线的起始位置和终止位置。可以通过修改strokeEnd调整CAGradientLayer的绘制进度。

CABasicAnimation *pathAnima = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
pathAnima.duration = 3.0f;
pathAnima.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
pathAnima.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnima.toValue = [NSNumber numberWithFloat:1.0f];
pathAnima.fillMode = kCAFillModeForwards;
[self.lineLayer addAnimation:pathAnima forKey:@"strokeEndAnimation"];
self.lineLayer.strokeEnd = 1.0f;

以上你可以看到整个曲线的绘制过程

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容

  • 目录: 主要绘图框架介绍 CALayer 绘图 贝塞尔曲线-UIBezierPath CALayer子类 补充:i...
    Ryan___阅读 1,666评论 1 9
  • 转载:http://www.cnblogs.com/jingdizhiwa/p/5601240.html 1.ge...
    F麦子阅读 1,541评论 0 1
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,478评论 6 30
  • 前言:关于贝塞尔曲线与CAShapeLayer的学习 学习Demo演示: 贝塞尔曲线简单了解 使用UIBezier...
    麦穗0615阅读 17,864评论 18 149
  • 教育既要看过程更要看结果,当温和的过程没有效果时,我会改变策略,就像今晚,我们又回到了以前那个熟悉的不能再熟悉场面...
    九五自尊阅读 179评论 0 0