用代码来实现你喜欢的动画效果 — 虫儿跑

作者建议读者在PC上阅读,字数太多手机阅读起来不是很好。

文档更新说明

  • 2016-03-25 完成全文
  • 2016-03-24 完成4.3
  • 2016-03-22 完成4.1,4.2
  • 2016-03-21 初稿

前言

接触iOS应用开发不久,对于很多我暂时不清楚怎么实现的功能效果都挺感兴趣的.iOS开发中,动画特效是必不可少的一部分,最近学习了一下动画,顺便花了点时间实现了一个虫子嚅动的动画效果并写成一个指示器(HUD).本文将讲述怎么学习动画设计,最终的效果就是要做到看上一个喜欢的动画,然后能用代码在iOS中实现他,最后能够在实际项目使用.

看上的动画

这是一篇和动画有关的文章,那我就在文章开头放出这次想要实现的动画吧,大家也好思考一下具体怎么去实现.下面是我在豆瓣网上看到的一张动画图,觉得挺有意思的,于是我打算用程序实现它.我给它取了一个名字,虫儿跑,一只勤奋的虫子.


a-diligent-worm.gif

思考

了解iOS中实现动画的方法

iOS中的动画主要有View级别,Layer级别.View级别动画可以利用UIView的类中animation开头的类方法实现,使用简单方便,但是无法操作更多细节,它其实就是Core Animation的封装.Layer级别的动画相对于View级别来说要底层一些,主要是使用Core Animation来实现.相关CA类如下图:

  
Core-Animation-Classes.png

CAAnimation:核心动画的基础类,不能直接使用,负责动画运行时间、速度的控制,本身实现了CAMediaTiming协议。
CAPropertyAnimation:属性动画的基类(通过属性进行动画设置,注意是可动画属性),不能直接使用。
CAAnimationGroup:动画组,动画组是一种组合模式设计,可以通过动画组来进行所有动画行为的统一控制,组中所有动画效果可以并发执行。
CATransition:转场动画,主要通过滤镜进行动画效果设置。
CABasicAnimation:基础动画,通过属性修改进行动画参数控制,只有初始状态和结束状态。
CAKeyframeAnimation:关键帧动画,同样是通过属性进行动画参数控制,但是同基础动画不同的是它可以有多个状态控制。

根据实际场景,本文主要是在CALayer的子类CAShapeLayer(看名字很容易看出这是专门用作图形处理的子类)中利用CABasicAnimation, CAAnimationGroup来实现动画效果.更多iOS动画知识,可以参考文后的阅读推荐
  CABasicAnimation类有几个属性要特别留意,分别是fromValue和toValue,这两个基本就是CABasicAnimation的核心了,其余大部分是继承子父类的.这里要重点指出的是创建CABasicAnimation对象时候要指出keyPath,用来指明动画是平移缩放旋转还是绘制等.其中keyPath可以设置为strokeStart, strokeEnd,而CAShapeLayer也有两个属性分别是strokeStart, strokeEnd.网上很多文章介绍这些属性的作用了.我这里就不重复描述了,感觉这种东西光自己描述真的是很难说清楚,我发现有一个特别便捷直观的方法,那就是你在代码中自己测试观察.所以我特别写了两段测试的代码.大家也可以在我的GitHub上找到可以直接运行的代码.

CAShapeLayer *firstWormShapeLayer = [[CAShapeLayer alloc] init];
    firstWormShapeLayer.path = [self testPath];
    firstWormShapeLayer.lineWidth = 2;
    firstWormShapeLayer.lineCap = kCALineCapRound;
    firstWormShapeLayer.strokeColor = [UIColor redColor].CGColor;
    firstWormShapeLayer.fillColor = [UIColor clearColor].CGColor;
    firstWormShapeLayer.actions = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNull null],@"strokeStart",[NSNull null],@"strokeEnd", nil];

//self 是一个UIView对象
//@property (nonatomic,strong) CAShapeLayer *firstWormShapeLayer;
[self.layer addSublayer:firstWormShapeLayer];
self.firstWormShapeLayer = firstWormShapeLayer;
[self testAnimation];
 
 #pragma mark - 测试用
 // 生成一条用于制作动画的路径
-(CGPathRef)testPath{
    //画一个半圆
    UIBezierPath* wormPath = UIBezierPath.bezierPath;
    [wormPath moveToPoint: CGPointMake(5,20)];
    [wormPath addLineToPoint:CGPointMake(35, 20)];
    CGPathRef path = wormPath.CGPath;
    return path;
}

// 开始动画效果
-(void)testAnimation{
    CABasicAnimation *strokeStartAm = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    //时间函数,用于描述动画的速度和时间的关系,可以先忽略
    strokeStartAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    strokeStartAm.toValue = [NSNumber numberWithFloat:0.2];
    strokeStartAm.fromValue = [NSNumber numberWithFloat:1];
    //一次动画的执行时间
    strokeStartAm.duration = 2;
    //动画重复次数
    strokeStartAm.repeatCount = 100;
    
    CABasicAnimation *strokeEndAm = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm.toValue = [NSNumber numberWithFloat:1];
    strokeEndAm.fromValue = [NSNumber numberWithFloat:0];
    strokeEndAm.duration = 2;
    strokeEndAm.repeatCount = 100;
    
    [self.firstWormShapeLayer addAnimation:strokeEndAm forKey:nil];
}

哪些工具可以利用?

要实现一个动画,最重要的就是确定动画的速度与时间的关系,确定动画的基本元素.比如动画的基本形状,字形,路径等.这里我找到了几种相当不错的工具.

  1. CAMediaTimingFunction playground
    可视化观察timingFunction的曲线
  2. Sketch
    这是一款Mac上的矢量图绘制工具,可以到处SVG格式的文件
  3. PaintCode
    用于将SVG文件转换成OC的路径代码(提醒一下,PaintCode这个软件制作出来的路径绘制顺序可能不是你想要的)
      本文将要实现的动画效果看上去像是一个半圆,所以应该是用不上路径代码生成工具.

iOS动画的几个重要概念

  1. 关键帧:一个动画可能是单一的变化,等比例放大缩小,也可能是由好几个变化组成,比如先放大,再缩小,再平移.在iOS动画设计中,系统提供一些设置关键帧方法,你只要定义好关键帧和关键帧帧之间的时间分配,系统就会根据关键帧的区别自动补全过度帧,达到一个动画流畅的效果.
  2. 层:4.1小节中提到,在CALayer上才能实现更多动画的细节.View的层概念类似PhotoShop中的图层,一个图像的最终效果可以通过多个层重叠实现的,同样的一个View在屏幕上显示的效果除了View上面的内容之外,还有一个很大的因素就是这个View的层里内容.
  3. 时间函数:动画总会在设定的时间长度内执行完,在这段时间内,动画的变化有多大,变化的速度有多快,这主要取决于时间函数.如果时间函数是线性函数,那表示动画的变化速度在这一段时间内是均匀变化的.如果是曲线的,那又是另一种效果,下面给出系统提供的5种时间函数的函数图像,方便大家理解.


    kCAMediaTimingFunctionLinear.png

    线型函数

kCAMediaTimingFunctionEaseIn.png

缓入

kCAMediaTimingFunctionEaseOut.png

缓出

kCAMediaTimingFunctionEaseInEaseOut.png

慢入慢出

kCAMediaTimingFunctionDefault.png

系统默认的时间函数

系统还提供了一个设置时间函数的方法[CAMediaTimingFunction functionWithControlPoints::::],具体用法参考CAMediaTimingFunction playground

  1. fillMode:填充模式是CAMediaTiming协议里的属性,所有的Core Animation动画类都有,它非常重要又不好直接通过文档理解,所以有必要解释一下.
    kCAFillModeRemoved:默认是这个属性,动画完成后就从layer中移除了.你看到的效果就是动画结束后,你在层中可以看到内容变成了你绘制的完整图形,这经常发生在你有多个动画对象要同时执行动画,但是这些对象的时间长度不同,造成一些对象执行完了就显示完整图形了.
    kCAFillModeForwards:当动画结束后,保留动画结束时的模样.
    kCAFillModeBackwards:动画在开始前就显示出动画要开始时的模样.比如你动画延迟2秒执行,那么动画在没开始执行这段时间的模样就是动画开始执行时的样子.
    kCAFillModeBoth:同时具备kCAFillModeForwards和kCAFillModeBackwards的效果
  2. 动画组: iOS动画中,可以使用CAAnimationGroup来实现动画组功能.动画组对象可以加入多个动画对象,这些动画对象将会在动画组开始执行的时候同时执行动画.CAAnimationGroup也有自己的长度,重复次数等属性,这些和组中的每一个动画属性都不冲突.这个是很重要的,举个例子.动画对象A长度1s,重复2次;动画对象B长度1.5s,重复2次;动画对象C时间长度2s,重复次数3次;动画组G,长度3秒,重复无数次.ABC加入G中,效果就是G在执行一次动画时,A可以重复执行2次,然后空闲1s.B重复2次,没有空闲.C执行完第一次,在重复第二次时执行了1秒就没法继续执行了,因为3秒时间一个周期到了,G又重复了整个这个动画效果.
      iOS动画还有很多概念和属性,由于本文要实现的动画没有涉及到,所以就不讲述了.具体的大家可以在推荐阅读中继续学习,然后尝试实现更多效果

拆分动画,简化思路

不同的拆分思路,写出来的代码也不同,如果一开始拆分得不好,有可能代码要多写很多,而且还不一定能实现.观察一下上面提供的GIF动画,整个动画是由一个半圆弧组成,一共是三种颜色,红黄绿色.这里有几种思路.
  一种是整个动画由一个layer组成,layer上画一个半圆弧,把圆弧分层三段,分别画上不同的颜色.要注意的是动画除了圆弧的伸张缩小外还有一个平移运动.这种思路看似简单,但是有个地方不好实现,就是半圆弧的拉伸和缩小,颜色长度比例要保持一样.半圆弧可以从0画到完整的半圆弧,再从左端向右端消失,这个容易实现拉伸缩小效果,但是颜色跟着拉伸缩小就不好做了.
  另一种思路是拆分动画,用多个layer组成,也是本文所使用的方法.通过拆分动画,可以简化思路和算法.观察GIF,可以把动画看成三个layer,每一个layer各自画着自己的半圆弧,红色保留完整弧长,微调黄色和绿色的可见弧长,其中绿色最短.注意一下,一开我也是把基础图形设定为半圆弧,但是做着做着发现没法实现先头红尾绿,接着先绿尾红这样的效果,所以基础图形的设定是非常重要的,一定要考虑清楚,否则事倍功半.这里我们需要把基础图形设定成有两个半圆弧连着的.如图:


full-worm-red.png

左半圆弧可以实现头红尾绿,右半圆弧可以实现先绿尾红的效果,动画过程中始终只显示最多一半的路径(半圆弧).

开始动手

实现动画的核心部分

根据动画的拆分思路,先实现红色部分的效果,再叠加黄色和绿色的图层.下面是核心代码和注释.
CCWormView.h

//创建UIView的子类CCWormView
@interface CCWormView : UIView
//省略其他代码
@end

CCWormView的实现

#define WORM_ANIMATION_KEY_FIRST @"WORM_ANIMATION_KEY_FIRST"
#define WORM_ANIMATION_KEY_SECOND @"WORM_ANIMATION_KEY_SECOND"
#define WORM_ANIMATION_KEY_THIRD @"WORM_ANIMATION_KEY_THIRD"
@interface CCWormView ()
//用户实现红色虫动画的层
@property (nonatomic,strong) CAShapeLayer *firstWormShapeLayer;
@end
@implementation CCWormView
//省略其他代码
//...
//初始化层
-(instancetype)initWithFrame:(CGRect)frame HUDStyle:(CCWormHUDStyle)style{
    CAShapeLayer *firstWormShapeLayer = [[CAShapeLayer alloc] init];
    //设置动画的路径
    firstWormShapeLayer.path = [self wormRunLongPath];
    //画笔宽度
    firstWormShapeLayer.lineWidth = CCWormHUDLineWidth;
    //设置线段头尾为圆形
    firstWormShapeLayer.lineCap = kCALineCapRound;
    //设置线段拐角点为圆形
    firstWormShapeLayer.lineJoin = kCALineCapRound;
    //设置路径绘制颜色为红色
    firstWormShapeLayer.strokeColor = [UIColor redColor].CGColor;
    //设置填充颜色为透明
    firstWormShapeLayer.fillColor = [UIColor clearColor].CGColor;
    //
    firstWormShapeLayer.actions = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNull null],@"strokeStart",[NSNull null],@"strokeEnd", nil];
    //将其加入到view的layer中.
    [self.layer addSublayer:firstWormShapeLayer];    
    self.firstWormShapeLayer = firstWormShapeLayer;
    //动画部分,为CAShapeLayer添加动画效果
    [self firstWormAnimation];
}
-(CGPathRef)wormRunLongPath{
    //确定路径起点位置
    CGPoint center;
    center = CGPointMake(self.frame.size.width * 9 / 10, self.frame.size.height / 2);
    //两个连着的半圆
    CGFloat radius = (CCWormHUDViewWith / 2.0) / 2.0;
    UIBezierPath *wormPath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:M_PI endAngle:2 * M_PI clockwise:YES];
    [wormPath addArcWithCenter:CGPointMake(center.x  + radius * 2, center.y) radius:radius startAngle:M_PI endAngle:2 * M_PI clockwise:YES];
    //返回CGPathRef,因为CAShapeLayer的path属性类型为CGPathRef
    CGPathRef path = wormPath.CGPath;
    return path;
}

下面是firstWormAnimation部分
第一步 实现红色虫运动的前半部分

    ///虫子拉伸
    CABasicAnimation *strokeEndAm = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm.toValue = [NSNumber numberWithFloat:0.5];
    //fromValue 开始画时已经存在的部分的量
    strokeEndAm.fromValue = [NSNumber numberWithFloat:0];
    strokeEndAm.duration = 0.75;
    strokeEndAm.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    //动画结束后,保留结束时的模样
    strokeEndAm.fillMode = kCAFillModeForwards;
    //将动画加入firstWormShapeLayer的代码省略了.

将CABasicAnimation对象的动画关键路径为strokeEnd,fromValue=0,toValue=0.5.效果就是动画从空开始画,到一半路径时结束,效果如下:

worm-strokeEndAm1.gif

第二步 实现红色虫收缩的动画

    //虫子缩小
    CABasicAnimation *strokeStartAm = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm.toValue = [NSNumber numberWithFloat:0.5];
    strokeStartAm.fromValue = [NSNumber numberWithFloat:0];
    strokeStartAm.duration = 0.45;
    //如果不被Group加入的话,CACurrentMediaTime() + 0.75 才表示延迟0.75秒.
    strokeStartAm.beginTime = 0.75;//延迟一秒执行
    strokeStartAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    strokeStartAm.fillMode = kCAFillModeForwards;

将CABasicAnimation对象的动画关键路径为strokeStart,fromValue=0,toValue=0.5.效果就是动画开始执行时就从起点也就是左端慢慢消失直到一半路径的位置停止.设置延迟0.75秒的作用就是等待虫子拉伸动画结束,再执行缩小.效果如下:
(提醒一下,下图是单独执行缩小动画,不包括上面的拉伸动画的结果,由于动画延迟0.75秒,所以这0.75秒会显示完整路径,结合拉伸动画效果就正常了)

worm-strokeStartAm1.gif

将第一二步动画放入一个组里同时执行,看看效果

    CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
    animationGroup.animations = [NSArray arrayWithObjects: strokeEndAm, strokeStartAm, nil];
    animationGroup.repeatCount = HUGE_VALF;
    //动画总时间应该以组中动画时间最长为标准
    animationGroup.duration = WormRunTime * 2;
    [self.firstWormShapeLayer addAnimation:animationGroup forKey:WORM_ANIMATION_KEY_FIRST];
worm-strokeEndAm1-strokeStartAm1.gif

第三步 实现虫子第二阶段的拉伸

    //虫子拉伸2
    CABasicAnimation *strokeEndAm2 = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm2.toValue = [NSNumber numberWithFloat:0.5 + 0.5];
    //fromValue 开始画时已经存在的部分的量
    strokeEndAm2.fromValue = [NSNumber numberWithFloat:0.5 + 0];
    strokeEndAm2.duration = 0.75;
    strokeEndAm2.beginTime = 1.2;
    strokeEndAm2.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm2.fillMode = kCAFillModeForwards;

虫子第二阶段的拉伸和第一阶段基本一样,需要改变的是toValue,fromValue的值,还有动画的开始时间必须是第一阶段结束后立即执行,一共是延迟1.2s,看看效果.

worm-strokeEndAm2.gif

第四步 实现虫子第二阶段的缩小

    //虫子缩小2
    CABasicAnimation *strokeStartAm2 = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm2.toValue = [NSNumber numberWithFloat:0.5 + 0.5];
    strokeStartAm2.fromValue = [NSNumber numberWithFloat:0.5 + 0];
    strokeStartAm2.duration = 0.45;
    strokeStartAm2.beginTime = 0.75 + 1.2;//延迟一秒执行
    strokeStartAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

和第一阶段类似,看看效果.

worm-strokeStartAm2.gif

现在把前四步都放入组中同时进行,看看效果.

    animationGroup.animations = [NSArray arrayWithObjects: strokeStartAm, strokeEndAm, strokeEndAm2, strokeStartAm2, nil];
worm-strokeEndAm1&2-strokeStartAm1&2.gif

第五步 实现虫子持续移动的效果

    //平移动画
    CABasicAnimation *xTranslationAm = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm.toValue = [NSNumber numberWithFloat: (40 / -1.0)];
    xTranslationAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm.duration = 1.18;
    xTranslationAm.fillMode = kCAFillModeForwards;
    
    CABasicAnimation *xTranslationAm2 = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm2.toValue = [NSNumber numberWithFloat: (40 / -1.0) * 2];
    xTranslationAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm2.duration = 1.18;
    xTranslationAm2.beginTime = 1.20;
    xTranslationAm2.fillMode = kCAFillModeForwards;

持续移动可以沿x轴平移来实现,平移要分两部分,第一阶段移动结束后稍微停留一下,再继续第二阶段的移动.每一个阶段的移动停留时间和虫子拉伸缩小总时长一样.将平移和上面四步动画结合,看看效果

worm-strokeEndAm1&2-strokeStartAm1&2-translation.x.gif

这样,红色虫的动画效果基本就实现了.

整合动画实现最终效果

现在还差黄色和绿色部分的动画.那么怎么实现黄色部分?注意观察,如果虫子在拉伸还没结束的时候就开始缩小,是不是效果就是黄色那样!如果黄色拉伸时间比红色慢,其他时间比例都不变,是不是可以实现红色先走随后拉动黄色呢?答案是肯定的.
  黄色和绿色的动画逻辑和红色是一模一样的,不同的就是动画的参数设置不同,下面给出黄色和绿色的参数.
先看看黄色虫子的参数设置.

    //虫子拉伸
    CABasicAnimation *strokeEndAm = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm.toValue = [NSNumber numberWithFloat:0.5];
    //fromValue 开始画时已经存在的部分的量
    strokeEndAm.fromValue = [NSNumber numberWithFloat:0.010];
    strokeEndAm.duration = 0.75;
    strokeEndAm.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm.fillMode = kCAFillModeForwards;
    
    //虫子缩小
    CABasicAnimation *strokeStartAm = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm.toValue = [NSNumber numberWithFloat:0.490];
    strokeStartAm.fromValue = [NSNumber numberWithFloat:0];
    strokeStartAm.duration = 0.70;
    strokeStartAm.beginTime = 0.50;
    strokeStartAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    strokeStartAm.fillMode = kCAFillModeForwards;
    
    
    //虫子拉伸2 拉伸的第二阶段,必须让上一层的第二阶段先动
    CABasicAnimation *strokeEndAm2 = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm2.toValue = [NSNumber numberWithFloat:0.5 + 0.5];
    //fromValue 开始画时已经存在的部分的量
    strokeEndAm2.fromValue = [NSNumber numberWithFloat:0.5];
    strokeEndAm2.duration = 0.75;
    strokeEndAm2.beginTime = 1.2 + 0.15;
    strokeEndAm2.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm2.fillMode = kCAFillModeForwards;
    
    //虫子缩小2
    CABasicAnimation *strokeStartAm2 = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm2.toValue = [NSNumber numberWithFloat:0.5 + 0.5];
    strokeStartAm2.fromValue = [NSNumber numberWithFloat:0.5 + 0];
    strokeStartAm2.duration = 0.30;
    strokeStartAm2.beginTime =0.15 + 0.75 + 1.2;
    strokeStartAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    
    
    //平移动画
    CABasicAnimation *xTranslationAm = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm.toValue = [NSNumber numberWithFloat: (40 / -1.0)];
    xTranslationAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm.duration = 1.18;
    xTranslationAm.fillMode = kCAFillModeForwards;
    
    CABasicAnimation *xTranslationAm2 = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm2.toValue = [NSNumber numberWithFloat: (40 / -1.0) * 2];
    xTranslationAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm2.duration = 1.18;
    xTranslationAm2.beginTime = 1.20;
    xTranslationAm2.fillMode = kCAFillModeForwards;
    
    
    
    CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
    animationGroup.animations = [NSArray arrayWithObjects: strokeStartAm, strokeEndAm, xTranslationAm, strokeEndAm2, strokeStartAm2, xTranslationAm2, nil];
    animationGroup.repeatCount = HUGE_VALF;
    //动画总时间应该以组中动画时间最长为标准
    animationGroup.duration = 1.2 * 2;
    [self.secondWormShapeLayer addAnimation:animationGroup forKey:nil];

将黄色和红色两个层合并在一起同时执行动画,看看效果

worm-first&secondLayer.gif

下面是绿色虫子的参数.

    //虫子拉伸
    CABasicAnimation *strokeEndAm = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm.toValue = [NSNumber numberWithFloat:0.5];
    //fromValue 开始画时已经存在的部分的量
    strokeEndAm.fromValue = [NSNumber numberWithFloat:0.010];
    strokeEndAm.duration = 0.75;
    strokeEndAm.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm.fillMode = kCAFillModeForwards;
    
    //虫子缩小
    CABasicAnimation *strokeStartAm = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm.toValue = [NSNumber numberWithFloat:0.490 ];
    strokeStartAm.fromValue = [NSNumber numberWithFloat:0];
    strokeStartAm.duration = 0.90;
    strokeStartAm.beginTime = 0.25;
    strokeStartAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    strokeStartAm.fillMode = kCAFillModeForwards;
    
    
    //虫子拉伸2 拉伸的第二阶段,必须让上一层的第二阶段先动
    CABasicAnimation *strokeEndAm2 = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm2.toValue = [NSNumber numberWithFloat:0.5 + 0.5];
    //fromValue 开始画时已经存在的部分的量
    strokeEndAm2.fromValue = [NSNumber numberWithFloat:0.5];
    strokeEndAm2.duration = 0.75;
    strokeEndAm2.beginTime = 1.2 + 0.15 + 0.20;
    strokeEndAm2.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm2.fillMode = kCAFillModeForwards;
    
    //虫子缩小2
    CABasicAnimation *strokeStartAm2 = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm2.toValue = [NSNumber numberWithFloat:0.5 + 0.5];
    strokeStartAm2.fromValue = [NSNumber numberWithFloat:0.5 + 0];
    strokeStartAm2.duration = 0.30;
    strokeStartAm2.beginTime =0.15 + 0.75 + 1.2;
    strokeStartAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    
    
    //平移动画
    CABasicAnimation *xTranslationAm = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm.toValue = [NSNumber numberWithFloat: (HUDWith / -1.0 + 10)];
    xTranslationAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm.duration = 1.18;
    xTranslationAm.fillMode = kCAFillModeForwards;
    
    CABasicAnimation *xTranslationAm2 = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm2.toValue = [NSNumber numberWithFloat: (HUDWith / -1.0 + 10) * 2];
    xTranslationAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm2.duration = 1.18;
    xTranslationAm2.beginTime = 1.20;
    xTranslationAm2.fillMode = kCAFillModeForwards;
    
    
    
    CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
    animationGroup.animations = [NSArray arrayWithObjects: strokeStartAm, strokeEndAm, xTranslationAm, strokeEndAm2, strokeStartAm2, xTranslationAm2, nil];
    animationGroup.repeatCount = HUGE_VALF;
    //动画总时间应该以组中动画时间最长为标准
    animationGroup.duration = 1.2 * 2;
    [self.thirdWormShapeLayer addAnimation:animationGroup forKey:nil];

最终将红黄绿三个虫子合并,看看合成的效果

worm-first&second&thirdLayer.gif

  .

优化代码,实现HUD的功能

三个虫子的动画逻辑都是一下的,所以这里可以优化一下代码结构,最后加上UIView动画,实现一个指示器(HUD),这样就可以提供给别人使用了~
  先把虫子动画中变化的参数提取出来,封装在一个方法里,然后三个虫分别给出自己的参数调用一下方法就可以得到动画对象了.封装后的代码如下.(其中有一些是全局静态变量和宏定义,不用太在意,具体可参考Git上的完整源码)


/**
 *  第三条虫子嚅动
 */
-(void)thirdWormAnimation{
    
    CAAnimationGroup *animationGroup = [self baseWormAnimationWithEnd1From:0.010 end1To:0.5 end1Duration:0.75 start1From:0 start1To:0.490 start1Duration:0.9 start1Begin:0.25 end2From:0.5 end2To:0.5 + 0.5 end2Duration:0.75 end2Begin:WormRunTime + 0.15 + 0.20 start2From:0.5 start2To:0.5 + 0.5 start2Duration:0.30 start2Begin:0.15 + 0.75 + WormRunTime];
    
    [self.thirdWormShapeLayer addAnimation:animationGroup forKey:WORM_ANIMATION_KEY_THIRD];
}

/**
 *  第二条虫子嚅动
 */
-(void)secondWormAnimation{
    
    CAAnimationGroup *animationGroup = [self baseWormAnimationWithEnd1From:0.010 end1To:0.5 end1Duration:0.75 start1From:0 start1To:0.490 start1Duration:0.70 start1Begin:0.50 end2From:0.5 end2To:0.5 + 0.5 end2Duration:0.75 end2Begin:WormRunTime + 0.15 start2From:0.5 start2To:0.5 + 0.5 start2Duration:0.30 start2Begin:0.15 + 0.75 + WormRunTime];
    
    [self.secondWormShapeLayer addAnimation:animationGroup forKey:WORM_ANIMATION_KEY_SECOND];
}

/**
 *  第一条虫子嚅动 (最底部的那条)
 */
-(void)firstWormAnimation{
    CAAnimationGroup *animationGroup = [self baseWormAnimationWithEnd1From:0 end1To:0.5 end1Duration:0.75 start1From:0 start1To:0.5 start1Duration:0.45 start1Begin:0.75 end2From:0.5 end2To:0.5 + 0.5 end2Duration:0.75 end2Begin:1.2 start2From:0.5 start2To:0.5 + 0.5 start2Duration:0.45 start2Begin:0.75 + WormRunTime];
    
    [self.firstWormShapeLayer addAnimation:animationGroup forKey:WORM_ANIMATION_KEY_FIRST];
}

-(CAAnimationGroup *)baseWormAnimationWithEnd1From:(CGFloat)end1FromValue end1To:(CGFloat)end1ToValue end1Duration:(CGFloat)end1Duration start1From:(CGFloat)start1FromValue start1To:(CGFloat)start1ToValue start1Duration:(CGFloat)start1Duration start1Begin:(CGFloat)start1BeginTime end2From:(CGFloat)end2FromValue end2To:(CGFloat)end2ToValue end2Duration:(CGFloat)end2Duration end2Begin:(CGFloat)end2BeginTime start2From:(CGFloat)start2FromValue start2To:(CGFloat)start2ToValue start2Duration:(CGFloat)start2Duration start2Begin:(CGFloat)start2BeginTime{
    
    
    //虫子拉伸1
    CABasicAnimation *strokeEndAm = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm.toValue = [NSNumber numberWithFloat:end1ToValue];
    //fromValue 开始画时已经存在的部分的量
    strokeEndAm.fromValue = [NSNumber numberWithFloat:end1FromValue];
    strokeEndAm.duration = end1Duration;
    strokeEndAm.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm.fillMode = kCAFillModeForwards;
    
    //虫子缩小1
    CABasicAnimation *strokeStartAm = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm.toValue = [NSNumber numberWithFloat:start1ToValue];
    strokeStartAm.fromValue = [NSNumber numberWithFloat:start1FromValue];
    strokeStartAm.duration = start1Duration;
    //如果不被Group加入的话,CACurrentMediaTime() + 1 才表示延迟1秒.
    strokeStartAm.beginTime = start1BeginTime;//延迟执行
    strokeStartAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    strokeStartAm.fillMode = kCAFillModeForwards;
    
    
    //虫子拉伸2 拉伸的第二阶段,必须让上一层的第二阶段先动
    CABasicAnimation *strokeEndAm2 = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm2.toValue = [NSNumber numberWithFloat:end2ToValue];
    //fromValue 开始画时已经存在的部分的量
    strokeEndAm2.fromValue = [NSNumber numberWithFloat:end2FromValue];
    strokeEndAm2.duration = end2Duration;
    strokeEndAm2.beginTime = end2BeginTime;
    strokeEndAm2.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm2.fillMode = kCAFillModeForwards;
    
    //虫子缩小2
    CABasicAnimation *strokeStartAm2 = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm2.toValue = [NSNumber numberWithFloat:start2ToValue];
    strokeStartAm2.fromValue = [NSNumber numberWithFloat:start2FromValue];
    strokeStartAm2.duration = start2Duration;
    //如果不被Group加入的话,CACurrentMediaTime() + 1 才表示延迟1秒.
    strokeStartAm2.beginTime = start2BeginTime;//延迟一秒执行
    strokeStartAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    
    
    //平移动画
    CABasicAnimation *xTranslationAm = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm.toValue = [NSNumber numberWithFloat: ( (CCWormHUDViewWith / 2.0) / -1.0)];
    xTranslationAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm.duration = 1.18;
    xTranslationAm.fillMode = kCAFillModeForwards;
    
    CABasicAnimation *xTranslationAm2 = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm2.toValue = [NSNumber numberWithFloat: ( (CCWormHUDViewWith / 2.0) / -1.0) * 2];
    xTranslationAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm2.duration = 1.18;
    xTranslationAm2.beginTime = 1.20;
    xTranslationAm2.fillMode = kCAFillModeForwards;
    
    
    
    CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
    animationGroup.animations = [NSArray arrayWithObjects: strokeStartAm, strokeEndAm, xTranslationAm, strokeEndAm2, strokeStartAm2, xTranslationAm2, nil];
    animationGroup.repeatCount = HUGE_VALF;
    //动画总时间应该以组中动画时间最长为标准
    animationGroup.duration = WormRunTime * 2;
    
    return animationGroup;
}

接下来为这个动画所在的视图添加UIView级别动画,做成一个HUD,并且提供两个公开的实例方法用于显示和隐藏HUD
  讲了半天Core Animation动画,不知道你是不是累了.往下看,接下来你一定会觉得使用UIView级别的动画是多么轻松,让我们开始轻松一下.下面看一下怎么在显示和隐藏视图时使用动画.

-(void)startLodingWormHUD{
    self.isShowing = YES;
    [self.presentingView addSubview:self];
    //动起来.显示指示器的时候才开始设置动画效果
    [self firstWormAnimation];
    [self secondWormAnimation];
    [self thirdWormAnimation];
    
    self.transform = CGAffineTransformMakeScale(0.1, 0.1);
    [UIView animateKeyframesWithDuration:0.6 delay:0.0 options:0 animations:^{
        [UIView addKeyframeWithRelativeStartTime:0 relativeDuration:0.5 animations:^{
            self.transform = CGAffineTransformMakeScale(1.3, 1.3);
        }];
        [UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{
            self.transform = CGAffineTransformIdentity;
        }];
    } completion: nil];
}

-(void)endLodingWormHUD{
    //隐藏指示器,同时移除动画
    [UIView animateKeyframesWithDuration:0.6 delay:0 options:0 animations:^{
        [UIView addKeyframeWithRelativeStartTime:0 relativeDuration:0.5 animations:^{
            self.transform = CGAffineTransformMakeScale(1.2, 1.2);
        }];
        [UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{
            self.transform = CGAffineTransformMakeScale(0.1, 0.1);
        }];
        
    } completion:^(BOOL finished){
        self.isShowing = NO;
        [self.firstWormShapeLayer removeAnimationForKey:WORM_ANIMATION_KEY_FIRST];
        [self.secondWormShapeLayer removeAnimationForKey:WORM_ANIMATION_KEY_SECOND];
        [self.thirdWormShapeLayer removeAnimationForKey:WORM_ANIMATION_KEY_THIRD];
        [self removeFromSuperview];
    }];
}

如果在此之前你还不了解UIView动画的基本变化,可参考理解iOS中CGAffineTransform与矩阵的关系

最后看一下结合指示器后勤奋虫子的最终效果

a-diligent-worm-in-iOS.gif

后语

从一开始接触动画到实现一个完整的动画中间,肯定会经历很多困惑与不解,然后就要不停搜索查文档了,这个过程虽然很累,但是一旦你把效果实现了,原理想明白了,那是多么享受的一件事情.这里我给一个建议,由于iOS动画中有太多类和属性,方法,你不可能一下子就把他们都明白透彻.一开始肯定是要先找准几个基本的概念先理解了,然后尝试动手写demo,实现一些基础效果.再慢慢将他们结合起来朝着你的目标效果前进.如果中途发现走不动了,效果实现很久也不能完成,那很可能是少用了某些对象,属性,方法,或者你的基础图形都是错的.比如本文,一开始没有使用动画组,发现很多效果做不出,然后我查了一下文档资料发现可以使用动画组实现,效果很容易就出来了.基础图形也是,从一个半圆弧到两个半圆弧,实现起来事半功倍.这个过程需要时间和经验,慢慢来吧,一起进步!
  至此,勤奋虫子的完整效果都已经实现了.还有没多具体细节上面并没有提到.本文所实现的指示器源码和使用Demo可以从本文源码:GitHub中找到,可以的话帮忙点一下Star,好让更多人看到.
  希望大家能从本文中得到自己需要的东西,如果你有更好的想法或发现本文的不足,欢迎指出来.更多文章我还来不及发到简书上,可在我的博客查看

参考阅读

本文源码:GitHub
iOS开发系列--让你的应用“动”起来
按钮打勾动画特效
理解iOS中CGAffineTransform与矩阵的关系
下面是工具
CAMediaTimingFunction playground
Sketch下载 密码:5d2b
PaintCode下载 密码:ftpt

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,461评论 6 30
  • 在中国自由知识分子阶层里面,吴晓波无疑是优秀代表。我以前陆续看过他的几本书,比如《大败局》,《激荡三十年》。。。那...
    爱吃香蕉的猴阅读 187评论 1 2
  • 第七篇 时间不早了,但还得忙着交作业,有点小小的为难,但依然拿起手机随意写。 今天的主题是占星,我不太熟悉,但知道...
    正念此心阅读 306评论 0 1
  • 看了加勒比海盗5,好好看。买了屈臣氏漱口水,试试效果。吃了今年夏天第一个冰激凌和雪碧,爽爆了哈哈哈。明天大扫除,扔...
    明天你好郭郭阅读 259评论 0 0