CoreAnimation专题二 CAShapeLayer with Bezier Path - Layer世界的神奇画笔

目录
  • 前言
    • 所有的CALayer子类
    • CAShapeLayer
      • 位图简介
      • 矢量图简介
      • 构建CAShapeLayer
    • 贝塞尔曲线
      • 贝塞尔曲线简介
      • 线性贝塞尔曲线
      • 二阶贝塞尔曲线
      • 三阶贝塞尔曲线
      • 一般化
      • 控制点
    • UIBezierPath
      • 直接构造
      • 迭代构造
      • 函数图像构造
      • 任意阶贝塞尔曲线
    • CAShapeLayer的可动画属性
      • strokeStart
      • strokeEnd
      • path
  • 总结
一 前言

CALayerCoreAnimation框架中的核心类,动画是基于绘图的,连图都绘不了还动个毛的画!而CALayer就是来解决绘图问题的。

CoreAnimation框架为我们实现了许多CALayer的子类,它们用来解决特定的问题,比如CATextLayer可以用来显示富文本,CAGradientLayer用来绘制颜色的线性渐变效果。既然它们都是CALayer的子类,它们就拥有CALayer所有的特点:可动画属性、隐式动画、transform变形等。

二 所有的CALayer子类

在CoreAnimation框架中的所有的CALayer的子类如下所示:

CAShapeLayer,用来根据路径绘制矢量图形
CATextLayer,绘制文字信息
CATransformLayer,使用单独的图层创建3D图形
CAGradientLayer,绘制线性渐变色
CAReplicatorLayer,高效地创建多个相似的图层并施加相似的效果或动画
CAScrollLayer,没有交互效果的滚动图层,没有滚动边界,可以任意滚动上面的图层内容
CATiledLayer,将大图裁剪成多个小图以提高内存和性能
CAEmitterLayer,各种炫酷的粒子效果
CAEAGLLayer,用来显示任意的OpenGL图形
AVPlayerLayer,用来播放视频 

而我们在开发中使用频率最高的就是CAShapeLayer,我们将结合贝塞尔曲线详细讲解其使用

其他Specialized Layer请参阅这篇翻译的 iOS核心动画高级技巧

二 CAShapeLayer

CAShapeLayer是一个通过矢量图形而不是bitmap(位图)来绘制的CALayer子类。你指定诸如颜色和线宽等属性,用CGPath来定义想要绘制的图形,最后CAShapeLayer就自动渲染出来了。当然,你也可以用Core Graphics直接向原始的CALyer的内容中绘制一个路径,相比直下,使用CAShapeLayer有以下一些优点:

1.渲染快速,CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多。
2.高效使用内存,一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形(backing image),所以无论有多大,都不会占用太多的内存。
3.不会被图层边界剪裁掉,一个CAShapeLayer可以在边界之外绘制。你的图层路径不会像在使用Core Graphics的普通CALayer一样被剪裁掉。
4.不会出现像素化,当你给CAShapeLayer做3D变换时,它不像一个有寄宿图的普通图层一样变得像素化
2.1 位图简介

在图形世界中有两种图形:位图(bitmap)和矢量图(vector)

位图是通过排列像素点来构造的,像素点的信息包括颜色+透明度(ARGB),颜色通过RGB来表示,所以一个像素一共有4个信息(透明度、R、G、B),每个信息的取值范围是0-255,也就是一共256个数,刚好可以用8位二进制来表示,所以每个像素点的信息通常通过32位(4字节)编码来表示,这种位图叫做32位位图,而一些位图没有Alpha通道,这样的位图每个像素点只有RGB信息,只需要24位就可以表示一个像素点的信息。

位图在进行变形(缩放、3D旋转等)时会重新绘制每个像素点的信息,所以会造成图形的模糊。

值得一提的是,对于GPU而言,它绘制位图的效率是相当高的,所以如果你要提高绘制效率,可以想办法把复杂的绘制内容转换成位图数据,然后丢给GPU进行渲染,比如使用CoreText来绘制文字。

2.2 矢量图

矢量图是通过对多个点进行布局然后按照一定规则进行连线后形成的图形。矢量图的信息总共只有两个:点属性和线属性。点属性包括点的坐标、连线顺序等;线属性包括线宽、描线颜色等。

每当矢量图进行变形的时候,只会把所有的点进行重新布局,然后重新按点属性和线属性进行连线。所以每次变形都不会影响线宽,也不会让图变得模糊。

如何重新布局是通过把所有点坐标转换成矩阵信息,然后通过矩阵乘法重新计算新的矩阵,再把矩阵转换回点信息。比如要对一个矢量图进行旋转,就先把这个矢量图所有的点转换成一个矩阵(x,y,0),然后乘以旋转矩阵:

( 
cosa sina 0
-sina cosa 0
0 0 1)

得到新的矩阵(x·cosa-y·sina, x·sina+y·cosa, 0),然后把这个矩阵转换成点坐标(x·cosa-y·sina, x·sina+y·cosa)这就是新的点了。对矢量图所有的点进行这样的操作后,然后重新连线,出现的新的图形就是旋转后的矢量图了。

关于矩阵计算和自定义矢量图的绘制,可以查看原作者的git项目:
DHVectorDiagram

2.3 构建CAShapeLayer

构建一个CAShapeLayer非常简单,对于所有CALayer的子类,它们的初始化都是一个简单的便利构造,像这样:

CAShapeLayer *shapeLayer = [CAShapeLayer layer];

像普通的CALayer一样,接下来你可以设置它的frame、背景颜色、寄宿图等,当然我们的CAShapeLayer肯定不是一个普通的layer,它是用来绘制矢量图的,通过传递给它的对象一个CGPathRef,CAShapeLayer就能以矢量图的形式将这个路径所表示的信息绘制出来。

在让CAShapeLayer渲染之前,我们可以先设置好线属性,比如我们设置线宽和描线颜色:

shapeLayer.lineWidth = 5;
shapeLayer.strokeColor = [UIColor redColor].CGColor;

stroke是描线的意思,我们后面还会接触到strokeStart和strokeEnd等更多的描线属性。

设置好了渲染信息后,我们可以构造一个路径来让CAShapeLayer帮我们绘制出来,这里我们先直接使用UIKit里面的贝塞尔曲线来构造一个简单的矩形路径:

UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 40, 40)];

这里需要注意的是,路径的坐标是相对于shapeLayer的左上角

然后把它的CGPath属性赋值给shapeLayer:

shapeLayer.path = path.CGPath;

最后把shapeLayer加到层级上来显示:

[self.view.layer addSublayer:shapeLayer];
  • 运行结果
image.png

运行一下会发现,我们的红色方框确实是画出来了,但是中间被填充成了黑色。这是因为CAShapeLayerfillColor属性默认为黑色,fillColor表示的是填充颜色,将一个CAShapeLayer的路径的所有封闭区间填充成该颜色,如果你不想要填充的效果,你可以设置其为透明色

shapeLayer.fillColor = [UIColor clearColor].CGColor;
  • 运行结果如下
image.png
三 贝塞尔曲线
3.1 贝塞尔曲线简介

下面是摘自维基百科

贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞尔曲线。

3.2 线性贝塞尔曲线

给定点P0、P1,线性贝塞尔曲线只是一条两点之间的直线。这条线由下公式给出

B(t)=P0+(P1−P0)t=(1−t)P0+tP1,t∈[0,1]
3.3 二阶贝塞尔曲线

二阶贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:

B(t)=(1−t)^2P0+2t(1−t)P1+t^2P2,t∈[0,1]

其中P1又叫做控制点
TrueType字型就运用了以贝塞尔样条组成的二阶贝塞尔曲线。

3.4 三阶贝塞尔曲线

P0、P1、P2、P3P0、P1、P2、P3 四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0P0走向P1P1,并从P2P2的方向来到P3P3。一般不会经过P1P1或P2P2;这两个点只是在那里提供方向资讯。P0P0和P1P1之间的间距,决定了曲线在转而趋进P2P2之前,走向P1P1方向的“长度有多长”。

曲线的参数形式为:

B(t)=P0(1−t)^3+P13t(1−t)^2+P23t^2(1−t)+P3t^3,t∈[0,1]
3.5 一般化

n阶贝塞尔曲线可如下推断。给定点P0、P1、…、Pn,其贝塞尔曲线即

image.png

如上公式可如下递归表达: 用BP0P1…PnBP0P1…Pn表示由点P0、P1、…、PnP0、P1、…、Pn所决定的贝塞尔曲线,则

B(t)=BP0P1…Pn(t)=(1−t)BP0P1…Pn−1(t)+tBP1P2…Pn(t)B(t)=BP0P1…Pn(t)=(1−t)BP0P1…Pn−1(t)+tBP1P2…Pn(t)

用平常话来说,n阶的贝塞尔曲线,即双n-1阶贝塞尔曲线之间的插值。

3.6 控制点

所有的Pi叫做贝塞尔曲线的控制点,起始点和结束点(P0、Pn)是特殊的控制点,在有些情况可以把它们和控制点分开来理解(也就是当我们说控制点的时候,不包括起始点和结束点)。

四 UIBezierPath

在UIKit框架中苹果用面向对象为我们封装了一个用来表示抽象贝塞尔曲线的类:UIBezierPath。我们可以使用它来很方便的表示一条曲线。

UIBezierPath实际上是广义上的曲线,它可以用来构造各种各样的曲线,比如我们之前使用过的表示一个矩形的线,接下来我们来看看它能构造哪些曲线出来。

4.1 直接构造

1.构造一个空的曲线

UIBezierPath *path = [UIBezierPath bezierPath];

2.构造一个矩形

UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 40, 40)];

3.构造一个矩形内切圆

UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 40, 40)];

所以如果要快速构造一个圆形出来的话,直接用正方形的内切圆就行了。如果传入的是一个长方形,那么构造出来的将是一个椭圆。

4.构造一个圆角矩形

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 40, 40) cornerRadius:3];

你也可以使用这种方式构造一个圆形,只需要设置圆角半径为正方形边长的一半即可。

5.构造一个圆角矩形并指定哪几个角是圆角 - 这里指定左下角和右上角这两个角变圆

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 140, 200) byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerTopRight cornerRadii:CGSizeMake(90, 100)];

这个方法的第三个参数传入的是一个CGSize,它的width成员就是你要设置的圆角半径,height有什么用我目前还没弄明白。值得注意的是,如果你设置的半径大于其宽或高的一半,那么系统会自动帮我们修正到一个不错的效果,你们可以试一试

6.构造一段圆弧

UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(200, 200) radius:100 startAngle:M_PI_2 endAngle:M_PI clockwise:YES];

第一个参数center表示的是圆弧的圆心
第二个参数radius表示圆弧的半径
第三个参数startAngle表示的是圆弧的起始点
第四个参数endAngle表示的是圆弧的终止点
第五个参数clockwise表示是否以顺时针的方向连接起始点和终止点
注意startAngle和endAngle所代表的只是两个点,0则表示圆的最右边那个点,所以如果是π2π2的话就表示圆上最下面那个点。
最终将会从起始点到终止点连一段圆弧出来,最后一个参数决定了这次连接是顺时针的还是逆时针的。具体如图所示

  • 运行结果如下
image.png
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(200, 200) radius:100 startAngle:M_PI_2 endAngle:M_PI clockwise:NO];
image.png
4.2 迭代构造

所有的UIBezierPath对象都能够通过对其添加子曲线来变得更为复杂。UIBezierPath通过控制一支虚拟的画笔来勾勒出各种你想要的形状。

想象你手里拿着一支用来绘制贝塞尔路径的笔,现在你想画出一条折线,应该怎么画呢?没错,先把笔放到一个地方,然后画一条线,然后笔不离开继续画一条线。

把笔放到一个地方可以通过调用moveToPoint方法,画一条线则调用addLineToPoint方法,比如像这样来画一个直角:

UIBezierPath * path = [UIBezierPath bezierPath];
// 把笔放在10,10的位置
[path moveToPoint:CGPointMake(10, 10)];
// 将笔移动到100,10的位置,路过的地方将会留下一条路径
[path addLineToPoint:CGPointMake(100, 10)];
// 笔现在已经在100,10的位置了,然后再画一条线到100,100
[path addLineToPoint:CGPointMake(100, 100)];

shapeLayer.path = path.CGPath;
  • 运行结果
image.png

这样画的效果是“一横一竖”,像个“7”。注意我们在画的过程中并没有再次调用moveToPoint,一旦调用了moveToPoint就相当于当前绘制点移动到了这个方法的参数指定的点。

任何贝塞尔曲线都可以随时添加各种子路径

比如你用直接构造法画了一个圆,然后想在里面再画一条横线,你可以这样做:

// 直接构造一个圆出来
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(100, 100, 200, 200)];
// 画一条横线
[path moveToPoint:CGPointMake(100, 200)];
[path addLineToPoint:CGPointMake(300, 200)];

shapeLayer.path = path.CGPath;
  • 运行结果如下
image.png

除了使用move和add方法来添加新的路径外,还可以使用appendPath方法来拼接子路径。上面的效果还可以这样来实现:

// 直接构造一个圆出来
UIBezierPath * path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(100, 100, 200, 200)];

// 构造一个子路径
UIBezierPath * subpath = [UIBezierPath bezierPath];

// 画一条横线
[subpath moveToPoint:CGPointMake(100, 200)];
[subpath addLineToPoint:CGPointMake(300, 200)];

// 拼接路径
// 把subpath拼接到path上
[path appendPath:subpath];

shapeLayer.path = path.CGPath;
  • 运行结果如下
image.png

除了可以使用addLineToPoint来在当前路径上添加直线外,还可以添加曲线。

UIBezierPath *path = [UIBezierPath bezierPath];
// 添加一段圆弧
// 注意我们没有调用moveToPoint,这样我们的笔就直接从圆弧的起始点画到结束点
[path addArcWithCenter:CGPointMake(200, 200) radius:100 startAngle:0 endAngle:M_PI clockwise:YES];
// 现在我们的笔处在endAngle所代表的点(简单计算一下,圆心200,200,半径100,endAngle是π,那么结束点就是100,200),
// 如果我们继续添加直线的话,就会直接从结束点开始画
[path addLineToPoint:CGPointMake(120, 20)];
  • 运行结果如下
image.png

我们还可以添加正统的贝塞尔曲线

UIBezierPath * path = [UIBezierPath bezierPath];
// 将笔置于40,40
[path moveToPoint:CGPointMake(40, 40)];
// 从40,40到300,200画一条贝塞尔曲线,其控制点为120,360,也就是说P0是40,40,P1是120,360,P3是300,200
[path addQuadCurveToPoint:CGPointMake(300, 200) controlPoint:CGPointMake(120, 360)];
  • 运行结果如下
image.png

一个控制点的贝塞尔曲线是二阶贝塞尔曲线,系统还提供了三阶贝塞尔曲线的实现:

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(40, 40)];
[path addCurveToPoint:CGPointMake(350, 600) controlPoint1:CGPointMake(10, 220) controlPoint2:CGPointMake(380, 380)];
  • 运行结果如下
image.png
4.3 函数图像构造

现在我们想要画一条sin曲线(正弦曲线),应该怎么画呢?这里就要发挥我们自己的聪明才智了。

在数学上我们的函数图像都是一系列满足函数表达式的连续的点,而计算机是没法处理“连续”的(比如数字音频没法处理模拟信号,只能用采样的方式以数字信号的形式进行离散处理),所以我们可以使用上一章我们逐帧绘制动画的方法,通过“足够近的离散的点”来模拟一条连续的曲线。

我们考虑任何一个函数 y = f(x),要怎样画出它的图像呢?我们按照离散的思想,肯定是每隔一个足够短的距离取一个点,然后把这些点全部拼接到一起就行了。

好现在我们至少有实现的思路了,就拿y = f(x) = sinx开刀吧。

- (void)drawSinBezierPath {
    // 构造函数图像
    CGFloat width = CGRectGetWidth(self.view.bounds);
    CGFloat height = CGRectGetHeight(self.view.bounds);
    
    // path
    UIBezierPath *path = [UIBezierPath bezierPath];
    // 第一个点需要moveToPoint,所以放到for循环之前来
    [path moveToPoint:CGPointMake(0, 100)];
    
    // 循环画点
    for (int i = 1; i < width; i++) {
        CGPoint point = CGPointMake(i, 100 + sin(i));
        [path addLineToPoint:point];
    }
    
    // layer
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.lineWidth = 5;
    shapeLayer.strokeColor = [UIColor redColor].CGColor;
    shapeLayer.fillColor = [UIColor clearColor].CGColor;
    
    shapeLayer.path = path.CGPath;
    
    // 最后把shapeLayer加到层级上来显示:
    [self.view.layer addSublayer:shapeLayer];
}
  • 运行结果


    image.png

上面的代码我们犯了两个错误:

UIKit的坐标系y轴正方向向下,而正规的用来画函数图像的直角坐标系y轴正方向是向上的。
y = sin(x)的值域是[-1,1],周期是2π,如果我们直接使用这样的值域和周期在画路径,那么这里的[-1,1]就是像素大小,整个图像的高度画出来就俩像素的高度。

所以我们将上面构造路径的代码修改为:

- (void)drawSinBezierPath2 {
    // 构造函数图像
    CGFloat width = CGRectGetWidth(self.view.bounds);
    CGFloat height = CGRectGetHeight(self.view.bounds);
    
    // path
    UIBezierPath *path = [UIBezierPath bezierPath];
    // 第一个点需要moveToPoint,所以放到for循环之前来
    [path moveToPoint:CGPointMake(0, height * 0.5)];
    
    // 循环画点
    for (int i = 1; i < width; i++) {
        // 对sinx图像进行变形
        CGFloat y = height * 0.5 * sin(2 * M_PI * i / 100) + height * 0.5;
        CGPoint point = CGPointMake(i, height - y);
        [path addLineToPoint:point];
    }
    
    // layer
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.lineWidth = 5;
    shapeLayer.strokeColor = [UIColor redColor].CGColor;
    shapeLayer.fillColor = [UIColor clearColor].CGColor;
    
    shapeLayer.path = path.CGPath;
    
    // 最后把shapeLayer加到层级上来显示:
    [self.view.layer addSublayer:shapeLayer];
}
  • 运行结果如下
image.png

总结一下,要绘制一般函数图像,就是在一般函数表达式注意上面的两个问题:坐标系转换和图像变形。坐标系转换通过参考系高度减去函数表达式算出来的值来得到绘图的y值,注意要把这个操作放在图像变形计算之后;图像变形是中学数学的内容,对于函数y = f(x),若要对其图像垂直方向拉伸n倍,向上平移a,水平方向拉伸m倍,向右平移b,则新的表达式为 y = nf((x-b)/m)+a,其中m和n若小于1则图像会被压缩,a和b若小于0则向负方向平移。

4.4 任意阶贝塞尔曲线

在我们实现UIBezierPath的时候大家可能已经注意到了,系统提供的贝塞尔曲线最多只有三阶贝塞尔曲线(两个控制点),如果要实现任意阶贝塞尔曲线怎么办呢?答案显而易见:用贝塞尔曲线的构造函数表达式一个点一个点的自己构造:

image.png

其中(ni)表示从n当中选出i个,也就是排列组合中的组合。

我们可以这样来实现这个函数:

- (CGFloat)choose:(CGFloat)t in:(CGFloat)n {
    if (t == 0) {
        return 1;
    }
    if (t == 1) {
        return n;
    }
    if (n == t) {
        return 1;
    }
    
    CGFloat x = 1.0f;
    CGFloat y = 1.0f;
    
    for (int i = n; i > n-t; i--) {
        x = x * i;
    }
    
    for (int i = t; i > 1; i--) {
        y = y * i;
    }
    
    return x / y;
}

贝塞尔曲线是一个关于t的函数B(t),根据公式我们可以在代码中实现这个函数关于t的表达式:

- (CGPoint)bezierPointMakeWithT:(CGFloat)t {
    CGPoint bezierPoint = CGPointZero;
    
    NSInteger rank = self.controlPoints.count + 1;
    
    bezierPoint.x = [self choose:0 in:rank] * (self.startPoint.x * pow(1-t, rank) * pow(t, 0));
    bezierPoint.y = [self choose:0 in:rank] * (self.startPoint.y * pow(1-t, rank) * pow(t, 0));
    
    for (int i = 1; i < rank; i++) {
        CGPoint p = [[self.controlPoints objectAtIndex:i - 1] CGPointValue];
        
        bezierPoint.x = bezierPoint.x + [self choose:i in:rank] * (p.x * pow(1-t, rank - i) * pow(t, i));
        bezierPoint.y = bezierPoint.y + [self choose:i in:rank] * (p.y * pow(1-t, rank - i) * pow(t, i));
    }
    
    bezierPoint.x = bezierPoint.x + [self choose:rank in:rank] * (self.endPoint.x * pow((1-t), 0)*pow(t, rank));
    bezierPoint.y = bezierPoint.y + [self choose:rank in:rank] * (self.endPoint.y * pow((1-t), 0)*pow(t, rank));
    
    return bezierPoint;
}

每一个t的值代表贝塞尔曲线上一个点的坐标,而t的取值范围是[0,1],所以我们可以使用一个for循环来构造一条贝塞尔曲线:

// update
- (void)update {
    [_bezierPath removeAllPoints];
    [_bezierPath moveToPoint:self.startPoint];
    
    if (self.controlPoints.count >= 1) {
        for (float ti = 0; ti <= 1.0 ; ti += 0.005) {
            CGPoint p = [self bezierPointMakeWithT:ti];
            [_bezierPath addLineToPoint:CGPointMake(p.x, p.y)];
        }
    }
}

具体封装详情看 CSBezierCurve

五 CAShapeLayer的可动画属性

作为CALayer大家族中的一员,CAShapeLayer拥有许多它自己的可动画属性,我们来几个比较关键的属性,剩下的属性大家可以点进CAShapeLayer的类声明里面进行查看。

5.1 strokeStart

strokeStart是一个被标记为Animatable的属性,它表示描线开始的地方占总路径的百分比,默认值是0,取值范围[0,1]。

比如你从(0,0)点画了一条直线到(100,0),(moveToPoint:(0,0);addLineToPoint:(100,0)),那么当strokeStart = 0.5的话,画出来的线就相当于从(50,0)画到(100,0)。

注意,如果你是从(100,0)画到了(0,0),那么绘制开始的点是(100,0),当strokeStart = 0.5的时候,画出来的线就相当于从(50,0)画到(0,0)。

我们来画一段圆弧并为strokeStart添加动画来试一试

- (void)strokeStart {
    // 构造一个圆弧路径,从圆的底部顺时针画到圆的右部(3/4圆)
    UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(200, 200) radius:100 startAngle:M_PI_2 endAngle:0 clockwise:YES];
    
    // 为strokeStart添加动画
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"strokeStart";
    animation.duration = 3.0;
    animation.fromValue = @0;
    
    // layer
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.lineWidth = 5;
    shapeLayer.strokeColor = [UIColor redColor].CGColor;
    shapeLayer.fillColor = [UIColor clearColor].CGColor;
    
    shapeLayer.path = path.CGPath;
    
    // 最后把shapeLayer加到层级上来显示:
    [self.view.layer addSublayer:shapeLayer];
    
    // 直接修改modelLayer的属性来代替toValue,见原理篇第四篇
    // 这样shapeLayer的strokeStart属性就会在3秒内从0变到1,可以观察动画的过程和你自己想象的是否一致
    
    // 添加一个延迟这样看得更明白些
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        shapeLayer.strokeStart = 1;
        [shapeLayer addAnimation:animation forKey:nil];
    });
}
  • 运行结果如下
Aug-17-2019 20-05-06.gif
5.2 strokeEnd

类似于strokeStart,只不过它代表了绘制结束的地方站总路径的百分比,默认值是1,取值范围是[0,1]。如果小于等于strokeStart,则绘制不出任何内容。你们可以把它和strokeStart联系起来对比认识。
我们把上面的动画代码中的keyPath改为@”strokeEnd”然后删掉shapeLayer.strokeStart = 1;这一行。再运行看看

  • 运行结果如下
Aug-17-2019 20-07-58.gif
5.3 path

有意思的是,path这个属性也被标记为了Animatable。可动画的路径,可能会比较难以想象是怎样的效果,我们用一个例子来进行说明。

如果我们要实现这样的一个动画:

Aug-17-2019 20-19-45.gif

实际上就是我们用一个填充颜色为橙色的shapeLayer将它的路径按如下做变化:

image.png

所以我们只需要一个CABasicAnimation,from左边的路径to右边的路径,CABasicAnimation就自动帮我们插值计算出中间的每帧的路径并动画显示出来了。

- (void)drawPath {
    // CAShapeLayer
    CAShapeLayer * shapeLayer = [CAShapeLayer layer];
    shapeLayer.fillColor = [UIColor orangeColor].CGColor;
    [self.view.layer addSublayer:shapeLayer];
    
    // 1.构造fromPath,并且从左上角开始画
    UIBezierPath *fromPath = [UIBezierPath bezierPath];
    [fromPath moveToPoint:CGPointZero];
    
    // 向下拉一条直线
    [fromPath addLineToPoint:CGPointMake(0, 400)];
    // 向右拉一条线,因为是向下弯的并且是从中间开始弯的,所以控制点的x是宽度的一半,y比起始点和结束点的y要大
    [fromPath addQuadCurveToPoint:CGPointMake(414, 400) controlPoint:CGPointMake(207, 600)];
    
    // 向上拉一条线
    [fromPath addLineToPoint:CGPointMake(414, 0)];
    // 封闭路径,会从当前点向整个路径的起始点连一条线
    [fromPath closePath];
    
    shapeLayer.path = fromPath.CGPath;
    
    // 2.构造toPath
    UIBezierPath * toPath = [UIBezierPath bezierPath];
    
    // 同样从左上角开始画
    [toPath moveToPoint:CGPointZero];
    // 向下拉一条线,要拉到屏幕外
    [toPath addLineToPoint:CGPointMake(0, 836)];
    // 向右拉一条曲线,同样因为弯的地方在正中间并且是向上弯,所以控制点的x是宽的一半,y比起始点和结束点的y要小
    [toPath addQuadCurveToPoint:CGPointMake(414, 836) controlPoint:CGPointMake(207, 736)];
    // 再向上拉一条线
    [toPath addLineToPoint:CGPointMake(414, 0)];
    // 封闭路径
    [toPath closePath];
    
    // 构造动画
    CABasicAnimation * animation = [CABasicAnimation animation];
    animation.keyPath = @"path";
    animation.duration = 5;
    
    // fromValue应该是一个CGPathRef(因为path属性就是一个CGPathRef),它是一个结构体指针,使用桥接把结构体指针转换成OC的对象类型
    animation.fromValue = (__bridge id)fromPath.CGPath;
    
    // 同样添加一个延迟来方便我们查看效果
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 直接修改modelLayer的值来代替toValue
        shapeLayer.path = toPath.CGPath;
        [shapeLayer addAnimation:animation forKey:nil];
    });
}
  • 运行结果如上图所示
六 总结

我们这一章中的内容比较多,首先我们介绍了CALayer的各种子类,然后讲解了如何简单的构造一个CAShapeLayer,接下来我们花了大量的时间来介绍贝塞尔曲线,包括数学推导,这样我们就能自己实现任意阶的贝塞尔曲线了。最后我们看了一下CAShapeLayer的可动画属性,使用这些可动画属性能够实现很多很多的效果。


本文摘自
iOS CoreAnimation专题——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇画笔

非常感谢该作者


项目链接地址 - CAShapeLayer_ CoreAnimation

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

推荐阅读更多精彩内容