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

来源:互联网 发布:中小型企业网络建设 编辑:程序博客网 时间:2024/05/22 00:51

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

前言

CALayer是CoreAnimation框架中的核心类,动画是基于绘图的,连图都绘不了还动个毛的画!而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请参阅这篇翻译的文章

CAShapeLayer

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

渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多。

高效使用内存。一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形(backing image),所以无论有多大,都不会占用太多的内存。

不会被图层边界剪裁掉。一个CAShapeLayer可以在边界之外绘制。你的图层路径不会像在使用Core Graphics的普通CALayer一样被剪裁掉。

不会出现像素化。当你给CAShapeLayer做3D变换时,它不像一个有寄宿图的普通图层一样变得像素化。

矢量图简介

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

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

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

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

关于位图,这里不做更详细的介绍。

矢量图

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

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

如何重新布局是通过把所有点坐标转换成矩阵信息,然后通过矩阵乘法重新计算新的矩阵,再把矩阵转换回点信息。比如要对一个矢量图进行旋转,就先把这个矢量图所有的点转换成一个矩阵(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

构建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];

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

shapeLayer.fillColor = [UIColor clearColor].CGColor;

贝塞尔曲线

贝塞尔曲线简介

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

— 维基百科

线性贝塞尔曲线

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

B(t)=P0+(P1P0)t=(1t)P0+tP1,t[0,1]

且其等同于线性插值

二阶贝塞尔曲线

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

B(t)=(1t)2P0+2t(1t)P1+t2P2,t[0,1]

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

三阶贝塞尔曲线

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

曲线的参数形式为:

B(t)=P0(1t)3+P13t(1t)2+P23t2(1t)+P3t3,t[0,1]

现代的成象系统,如PostScript、Asymptote和Metafont,运用了以贝塞尔样条组成的三次贝塞尔曲线,用来描绘曲线轮廓。

一般化

n阶贝塞尔曲线可如下推断。给定点P0P1Pn,其贝塞尔曲线即

B(t)=i=0n(ni)Pi(1t)niti=(n0)P0(1t)nt0+(n1)P1(1t)n1t1++(nn1)Pn1(1t)1tn1+(nn)Pn(1t)0tn

如上公式可如下递归表达: 用BP0P1Pn表示由点P0P1Pn所决定的贝塞尔曲线,则

B(t)=BP0P1Pn(t)=(1t)BP0P1Pn1(t)+tBP1P2Pn(t)

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

控制点

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

UIBezierPath

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

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

直接构造

UIBezierPath提供了直接构造某种曲线的方法

// 构造一个空的曲线path = [UIBezierPath bezierPath];
// 构造一个矩形path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 40, 40)];
// 构造一个矩形内切圆path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 40, 40)];

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

// 构造一个圆角矩形path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 40, 40) cornerRadius:3];

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

// 构造一个圆角矩形并指定哪几个角是圆角// 比如这里指定左下角和右上角这两个角变圆path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(10, 10, 140, 200) byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerTopRight cornerRadii:CGSizeMake(90, 100)];

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

// 构造一段圆弧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的话就表示圆上最下面那个点。

最终将会从起始点到终止点连一段圆弧出来,最后一个参数决定了这次连接是顺时针的还是逆时针的。具体如图所示

1

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;

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

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

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

// 直接构造一个圆出来UIBezierPath * path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(30, 30, 200, 200)];// 画一条横线[path moveToPoint:CGPointMake(30, 130)];[path addLineToPoint:CGPointMake(230, 130)];shapeLayer.path = path.CGPath;

3

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

// 直接构造一个圆出来UIBezierPath * path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(30, 30, 200, 200)];// 构造一个子路径UIBezierPath * subpath = [UIBezierPath bezierPath];// 画一条横线[subpath moveToPoint:CGPointMake(30, 130)];[subpath addLineToPoint:CGPointMake(230, 130)];// 拼接路径// 把subpath拼接到path上[path appendPath:subpath];shapeLayer.path = path.CGPath;

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

// 添加一段圆弧// 构造一个空的路径UIBezierPath * path = [UIBezierPath bezierPath];// 添加一段圆弧// 注意我们没有调用moveToPoint,这样我们的笔就直接从圆弧的起始点画到结束点// 你们可以试试看在下面这行代码之前调用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)];

4

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

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

5

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

UIBezierPath * path = [UIBezierPath bezierPath];[path moveToPoint:CGPointMake(40, 40)];[path addCurveToPoint:CGPointMake(350, 600) controlPoint1:CGPointMake(10, 220) controlPoint2:CGPointMake(380, 380)];

6

这就是系统提供了所有构造UIBezierPath的方法了。

函数图像构造

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

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

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

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

- (void)viewDidLoad {    [super viewDidLoad];    // 使用一个shapeLayer来显示函数图像    CAShapeLayer * shapeLayer = [CAShapeLayer layer];    shapeLayer.strokeColor = [UIColor redColor].CGColor;    shapeLayer.lineWidth = 5;    shapeLayer.fillColor = [UIColor clearColor].CGColor;    // 构造函数图像    CGFloat width = CGRectGetWidth(self.view.bounds);    CGFloat height = CGRectGetHeight(self.view.bounds);    // 先构造一个空的路径    UIBezierPath * path = [UIBezierPath bezierPath];    // 第一个点需要moveToPoint,所以放到for循环之前来    // 当x=0的时候sinx=0    [path moveToPoint:CGPointMake(0, 0)];    for (int i = 1; i < width; i++) {        CGPoint point = CGPointMake(i, sin(i));        [path addLineToPoint:point];    }    shapeLayer.path = path.CGPath;    [self.view.layer addSublayer:shapeLayer];}

看起来似乎是没有问题的,运行看一下效果吧

7

嗨呀,这个波浪线真是最骚的。上面的代码我们犯了两个错误:

UIKit的坐标系y轴正方向向下,而正规的用来画函数图像的直角坐标系y轴正方向是向上的

y = sin(x)的值域是[-1,1],周期是2π,如果我们直接使用这样的值域和周期在画路径,那么这里的[-1,1]就是像素大小,整个图像的高度画出来就俩像素的高度。

第一个问题可以通过用参考系高度减去函数计算出来的y来得到最终要画到屏幕上面的y,这里的参考系是屏幕,所以我们最终画到屏幕上的y’ = height - y。

第二个问题可以通过对函数图像进行变形操作(拉伸和平移),现在我们想把值域变为[0,height],周期变为100,应该怎样操作呢?

值域:先把y值变为原来的height/2倍,这样值域就变成了[-height/2, height/2],然后再加上height/2,值域就变成了[0,height],这样操作的结果就相当于函数图像垂直方向拉伸了height/2倍并且向上平移了height/2的高度

周期:相当于函数图像水平方向拉伸 100/(2π)倍,那么传进函数表达式的x就应该变为原来的2π/100倍,也就是说我们应该使用sin(2πx/100)来代替sin(x)作为函数表达式。

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

UIBezierPath * path = [UIBezierPath bezierPath];    // 第一个点需要moveToPoint,所以放到for循环之前来    // 根据新的函数图像,当x=0的时候f(x)=height/2    [path moveToPoint:CGPointMake(0, height/2)];    for (int i = 1; i < width; i++) {        // 对sinx图像进行变形        CGFloat y = height/2 * sin(2 * M_PI * i / 100) + height/2;        // 解决坐标轴方向相反的问题        CGPoint point = CGPointMake(i, height - y);        [path addLineToPoint:point];    }

8

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

任意阶贝塞尔曲线

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

B(t)=i=0n(ni)Pi(1t)niti=(n0)P0(1t)nt0+(n1)P1(1t)n1t1++(nn1)Pn1(1t)1tn1+(nn)Pn(1t)0tn

其中(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.f ;    CGFloat y = 1.f ;    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;    //  http://en.wikipedia.org/wiki/Bezier_curve#Generalization    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循环来构造一条贝塞尔曲线:

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

封装起来以后就成了这个样子:

// DHBezierCurve.h@interface DHBezierCurve : NSObject- (id)initWithStartPoint:(CGPoint)start endPoint:(CGPoint)end controlPoints:(NSArray <NSValue *>*)points;// return bezier path- (UIBezierPath *)bezierPath;@end
// DHBezierCurve.m#import "DHBezierCurve.h"@interface DHBezierCurve (){    UIBezierPath * _bezierPath;}@property (nonatomic,strong) NSMutableArray * controlPoints;@property (nonatomic,assign) CGPoint startPoint;@property (nonatomic,assign) CGPoint endPoint;@end@implementation DHBezierCurve- (id)initWithStartPoint:(CGPoint)start endPoint:(CGPoint)end controlPoints:(NSArray <NSValue *>*)points{    self = [super init];    self.startPoint = CGPointMake(start.x, start.y);    self.endPoint = CGPointMake(end.x, end.y);    self.controlPoints = [NSMutableArray arrayWithArray:points];    _bezierPath = [UIBezierPath bezierPath];    [self update];    return self;}- (void)update{    [_bezierPath removeAllPoints];    [_bezierPath moveToPoint:self.startPoint];    if (self.controlPoints.count >= 1) {        for (float ti = 0.0; ti <= 1.0; ti += 0.005) {            CGPoint p = [self bezierPointMakeWithT:ti];            [_bezierPath addLineToPoint:CGPointMake(p.x, p.y)];        }    }}- (UIBezierPath *)bezierPath{    return _bezierPath;}#pragma mark - private// used in - update- (CGPoint)bezierPointMakeWithT:(CGFloat)t{    CGPoint bezierPoint = CGPointZero;    NSInteger rank = [self.controlPoints count]+1;    //  http://en.wikipedia.org/wiki/Bezier_curve#Generalization    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;}- (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.f ;    CGFloat y = 1.f ;    for (int i = n; i > n-t; i--) {        x = x * i;    }    for (int i = t; i > 1; i--) {        y = y * i;    }    return x/y;}@end

当然你也可以考虑使用UIBezierPath的Category进行封装

CAShapeLayer的可动画属性

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

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)viewDidLoad {    [super viewDidLoad];    // 构造一个圆弧路径,从圆的底部顺时针画到圆的右部(3/4圆)    CAShapeLayer * shapeLayer = [CAShapeLayer layer];    shapeLayer.strokeColor = [UIColor redColor].CGColor;    shapeLayer.lineWidth = 5;    shapeLayer.fillColor = [UIColor clearColor].CGColor;    [self.view.layer addSublayer:shapeLayer];    UIBezierPath * path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(200, 200) radius:100 startAngle:M_PI_2 endAngle:0 clockwise:YES];    shapeLayer.path = path.CGPath;    // 为strokeStart添加动画    CABasicAnimation * animation = [CABasicAnimation animation];    animation.keyPath = @"strokeStart";    animation.duration = 3;    animation.fromValue = @0;    // 直接修改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];    });}

9

strokeEnd

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

10

path

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

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

11

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

12

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

- (void)viewDidLoad {    [super viewDidLoad];    CAShapeLayer * shapeLayer = [CAShapeLayer layer];    shapeLayer.fillColor = [UIColor orangeColor].CGColor;    [self.view.layer addSublayer:shapeLayer];    // 构造fromPath    UIBezierPath * fromPath = [UIBezierPath bezierPath];    // 从左上角开始画    [fromPath moveToPoint:CGPointZero];    // 因为我的模拟器是6plus,所以屏幕宽度是414    // 向下拉一条直线    [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;    // 构造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)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{    // 直接修改modelLayer的值来代替toValue        shapeLayer.path = toPath.CGPath;        [shapeLayer addAnimation:animation forKey:nil];    });}

运行看一下,怎么样,CABasicAnimation就是这么不讲道理。

总结

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

0 0
原创粉丝点击