解析 iOS 动画原理与实现

这篇文章不会教你们如何实现一个具体的动画效果,我会从动画的本质出发,来讲说 iOS 动画的原理与实现方式。html

什么是动画

动画,顾名思义,就是能“动”的画。
人的眼睛对图像有短暂的记忆效应,因此当眼睛看到多张图片连续快速的切换时,就会被认为是一段连续播放的动画了。ios

好比,中国古代的“走马灯”,就是用的这个原理。
有些人还会在一个本子每页上手绘一些漫画,当快速翻页的时候,也会看到动画的效果,好比:git


图片来自网络

计算机动画的实现方式

动画是由一张张图片组成的,在计算机中,咱们称每一张图片为 一帧画面github

若是咱们想实现这么一个动画:一个水杯放在桌子的左边,移动到右边,那么咱们实际操做的,只是水杯。
因此动画的实现,只是对运动变化了的部分的处理。网络

逐帧 与 关键帧

相似于上面提到的手绘翻页方式,咱们能够将这个水杯在每帧画面中的位置一一找出来,这样实现动画的方式就叫做 逐帧动画,咱们须要处理动画中的每一帧。app

咱们通常在计算机上用 FPS ( Frames Per Second) ,即 每秒的帧数 来表示动画的刷新速度,基于屏幕的刷新率等其余缘由,在计算机上通常采用 60 FPS。
若是运动变化幅度较缓,减半到 30 FPS 时,咱们肉眼也是可接受的。
较低的 FPS 会让咱们有“卡顿”的感受。框架

逐帧动画是最直接的,但要处理的帧数太多,因此实现过程是会麻烦。ide

计算机的工做就是来完成重复单调的工做的,因此,有些工做是能够考虑让计算机来完成的。函数


上面的例子,能够变成一个涉及数学和物理的问题:一个杯子初始位置在左边,n秒后匀速运动到右边,那么在每 1/60 秒的时候,这个杯子的位置显然是能够计算出来的了。
因此,咱们其实只须要指定一些 关键 信息就能让计算机本身计算出每一帧杯子的位置了:性能

  • 起始位置,好比一个坐标 (0,0)
  • 结束位置,再好比一个坐标 (100,0)
  • 动画总时间,好比 0.25 秒
  • 匀速运动

这种方式就称之为 关键帧动画。即咱们只须要给定几个关键帧的画面信息,关键帧与关键帧之间的过渡帧都将由计算机自动生成。

这里说的 关键帧动画,是指的广义上的一种动画制做方式,并不只指 CAKeyframeAnimationCABasicAnimation的实现方式也属于 关键帧动画

iOS 动画

说完广义上的动画,就能够来讲说 iOS 的动画了。
先来讲说动画的本质。

动画的本质

继续用上面的简单例子:一个 UIView 从 (0,0) 匀速移动到 (100,0)的动画,动画总时间是0.25秒。
假设咱们基于 60 FPS 来显示动画,那么在0.25秒内就应该有15帧画面,在每帧画面中,这个 UIViewx坐标,每次应移动 100/15 的距离。
若是咱们每隔 0.25/15 秒刷新一次UIViewx坐标,那么就能实现这个动画效果了。
对于 x坐标而言,每帧的位置就能够经过一个基于时间变化量的函数来求得:x=f(t)

因此,一个动画的本质,就是动画对象(这里是 UIView)的状态,基于时间变化的反应了。
简单说,就是给定任意一个时刻,若是你都能获得这个动画对象的位置和、形状等等属性,你就能实现这个动画了。
属性值的变化,既多是位置、透明度、旋转角度等的变化,也包括形状的改变,好比从一条直线变化成一个圆圈,目标就是要获得变化过程当中特定时刻的中间态。

动画的实现

咱们也可将 iOS 的动画分为两大类:

  • 系统提供的 关键帧动画 实现方式;用户指定 关键 信息,系统实现动画过程,对用户而言操做起来会简单些。
  • 逐帧动画 实现方式;用户本身 出每一帧画面,系统操做方法简单,但用户操做的工做量就会大一些。

逐帧动画实现方式

简单的说,要实现逐帧的方式,就是须要 周期性 的调用 绘制 方法,绘制每帧的动画对象。

这里说的 绘制,不光是指覆写 UIView- drawRect:的方法来手动重绘视图,也包括修改 UIView 它的属性,好比位置、颜色等。

iOS 的动画都是基于 CALayer 的,iOS 的 UIView 背后都有一个对应的 CALayer 。对 UIView 的修改实际上都是对背后 CALayer 的修改。
但若是在逐帧绘制的方法中修改了一个自建的 CALayer,这个 CALayer 不是对应某个 UIView 的,需注意系统的 隐式动画 的影响,后面会提到这点。

周期性,就须要一个定时器来完成了,即 CADisplayLink
CADisplayLinkNSTimer 比较相似,能够周期性的调用指定的方法。
之因此用 CADisplayLink,是由于它是基于屏幕刷新率的,即屏幕每次刷新时就会触发调用。
iPhone 的屏幕刷新率是 60 FPS。

若是绘制过程过于复杂,不能在屏幕刷新一帧的时间内完成,能够考虑改成每隔一帧绘制,至关因而 30 FPS的刷新率。
否则可能会使动画不连贯,有卡顿感。

用逐帧方法绘制的原理不是很麻烦,麻烦的是绘制过程。
对于一个复杂动画,你可能须要运用各类物理、几何知识去计算视图中间状态的信息。
好比要实现一条直线卷曲变化为一个圆的动画,你就须要计算出中间态的曲线的弯曲程度和位置。

著名的 facebook 的 pop 动画框架,就是使用 CADisplayLink 这种逐帧绘制的方式实现的。

关键帧动画实现方式

采用关键帧的方式来实现动画,要讲的内容相对逐帧的方式就多的多了。

仍是用 UIView 移动的简单例子。
这里面有两个关键帧,起始帧和结束帧,除此以外还有2个关键信息:

  • 起始帧,变化信息:坐标为 (0,0)
  • 结束帧,变化信息:坐标为 (100,0)
  • 动画时间,0.25秒
  • 匀速运动

坐标 信息是 UIView 的一个属性(实际是对应到 CALayer 的属性),在动画实现里,咱们只须要指定起始和结束的两个关键值就够了,中间的过渡值都有系统自动生成。
这里出现了两种值,一个是咱们设定的,一个是系统生成的,因此要先在这里插入一个 模型层展示层 的概念了

CALayer 的同一个属性值,会分别保存在模型层 modelLayer ,和展示层 presentationLayer 中。当咱们修改属性值时,是修改的模型层的数值,动画时系统根据模型层的变化,生成的过渡值,是保存在展示层中的。

CALayer 的对象里能直接访问到这两层的信息。
CALayer 的底层实现实际不止这两层,但咱们如今讨论动画的时候,能够只关心这两层。

在整个动画过程当中,呈现出来的过程是这样的:

  1. 动画前,显示模型层的当前值;
  2. 动画开始,切换显示展示层的值;
  3. 动画过程当中,展示层的值根据时间变化,咱们看到的实际是展示层的值在变化;
  4. 动画结束,切换回显示模型层的值,此时模型层的值应被修改成动画结束时的值。

用一段代码来解释下动画过程。

    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    view.backgroundColor = [UIColor redColor];
    [self.view addSubview:view];

    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
    animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(50, 0)];
    animation.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 0)];
    [view.layer addAnimation:animation forKey:nil];

//    view.frame = CGRectOffset(view.frame, 100, 0);

 

你会发现动画结束后,view 又跳回了原来的位置,这是由于最后一行代码注释了,而这行代码的功能就是实现第4步,将模型层的值修改成动画结束时的值。

动画实现

代码中的 CABasicAnimation 就是真正的动画实现部分,也就是设定关键帧信息的地方。

将动画加入 CALayer 的代码定义为:

- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key


接受的类型是 CAAnimation 类型,有下面这些子类:

  • CABasicAnimation,可设定起始结束两个关键帧的信息。
  • CAKeyframeAnimation,除首尾外,还可添加多个中间关键点。
  • CAAnimationGroup ,可组合多个动画,由于上面两种动画一次只能设置一个属性值。
  • CATransition,图层过渡动画,默认是淡入。好比修改一个 CALayer的背景色时,是从初始色慢慢淡入过渡到结束色。
    可修改成新颜色把旧颜色顶出去等效果。还可以使用 CIFilter 滤镜作过渡效果,一些开源 UIViewController 的过渡动画使用了这种方式。

动画中,除了属性值外,咱们还设置了两个和时间有关的信息:动画时间0.25秒,运动方式是匀速运动。

动画持续时间很简单,是经过 CAAnimation 遵照的 CAMediaTiming 协议设定的。

匀速运动是经过设置 CAAnimationtimingFunction 实现的,这是一个 CAMediaTimingFunction 类的对象。

以前已经说到,动画过程实际是一个时间的函数,横坐标是时间的变化值,纵坐标是动画属性的变化量。那么咱们就能够在一个直角坐标系中,经过做图来画出这个函数。好比匀速运动的图形,就是一条经过原点的直线。

因此这个类的功能就是画出一条曲线,来表示时间和属性变化之间的关系。而画图的方法,是使用的是画贝叶斯曲线的方法。

系统提供了几个经常使用的函数,好比 kCAMediaTimingFunctionLinear 就是匀速运动;kCAMediaTimingFunctionEaseInEaseOut 就是通常系统动画的默认值,渐入渐出,即在动画开始和结束的时候速度稍慢些。


图片来源自网络

隐式动画

上面的过程,咱们是 显式 的向一个 CALayer 添加了一个动画,因此这种方式叫作 显式动画
对应的,还有 隐式动画,即系统自动添加上的动画。

    CALayer *layer = [CALayer layer];
    layer.backgroundColor = [UIColor greenColor].CGColor;
    layer.frame = CGRectMake(0, 0, 100, 100);
    [self.view.layer addSublayer:layer];

    layer.frame = CGRectOffset(layer.frame, 100, 0);

 

这段代码里,咱们没有添加 CAAnimation 动画,但 layer 不是直接变化到新的位置,而是有一个动画效果。
这就是 隐式动画 的效果。

当咱们改变 CALayer 的一个可动画的属性值时,就会触发系统的隐式动画。
可动画的属性值,能够在 CALayer 的文档中找到,属性说明中标有 Animatable 的,就是可自动添加动画的属性。

可是,有一个例外,对于 UIView 背后对应的 CALayer,系统关闭了隐式动画,因此当咱们直接修改 UIView 或者是其底层的 CALayer 时,变化是直接生效的,没有动画效果。

因此当咱们在逐帧方式生成动画时,是能够直接修改 UIView 或者是其底层的 CALayer 的信息。
可是若是修改的是一个自建的单独 CALayer 时,帧与帧之间的变化仍是会触发系统的默认隐式动画,这个时候就须要咱们来手动关闭隐式动画。
当快速动画的时候不会察觉到这点,但这明显会带来性能上的浪费。

隐式动画所作的事情和显示动画是同样的,咱们设置的属性值都是模型层的数值,而系统会自动添加属性对应的 CAAnimation 动画到 CALayer 上。

UIView 有一系列的 animateWithDuration 动画方法,在这些方法中 UIView 会恢复隐式动画,因此在动画的 block 中修改属性时,又会触发隐式动画。


那么系统是若是知道对一个属性应该添加哪一种动画呢,这就须要让 CAAction 协议登场了。

当修改一个 CALayer 的属性时,它会经过 - actionForKey: 来查询这个属性对应的 action,而 key 就是对应的属性名称。
CAAnimation 遵照 CAAction 协议,返回的 action 实际上是个 CAAnimation 动画。
也就是说, CALayer 经过 - actionForKey: 来查询某个属性被修改时,须要调用哪一个动画去展示这个变化。
通常默认返回的是 CABasicAnimation ,默认动画时间 0.25秒,时间函数为渐入渐出 kCAMediaTimingFunctionEaseInEaseOut。

- actionForKey: 查询 action 的步骤有4步,在这个方法中有详细的说明。
其中一种方式就是经过 CALayer 的 delegate 返回 action。而对于 UIView 背后对应的 CALayer,其代理就是它对应的 UIViewUIView 就是用这种方式关闭了隐式动画。

动画事务

建立动画事务的目的是为了操做的原子性,保证动画的全部修改能同时生效。
CATransaction 就是动画事务的操做类。

在建立隐式动画的时候,系统也会隐式的建立一个动画事务,以保证全部的动画能同时进行。

除此以外,还能够显式的建立一个事务。
显式事务中能够定义事务中全部动画的运行时间和时间函数,此外,还有这个方法 + (void)setDisableActions:(BOOL)flag 能显式的关闭这个事务中的 action 查询操做。
关闭了查询也就是关闭了动画效果,属性值的变化就会当即生效,而没有动画效果了:

    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    ///...
    layer.frame = CGRectOffset(layer.frame, 100, 0);
    ///...
    [CATransaction commit];

 

注意别把 CATransaction 和 CATransition 搞混了,一个单词是 transaction 事务,另外一个是 transition 转变。

对比 总结

关键帧动画的实现方式,只须要修改某个属性值就能够了,简单方便,但涉及的深层次内容较多,须要更多的理解和练习。

采用逐帧动画的实现方式,实现原理简单,但绘制动画的过程要复杂。若是动画过程处理的事情较多,也会带来较大的开销,就有可能形成动画帧数的降低,出现卡顿的现象,所以须要较多的测试和调试。
动画绘制的过程当中,会要求较多的数学、物理等知识来计算中间态的数据。

但这两种方式也不是绝对分离开的。
关键帧动画实现方式,通常只能对系统实现了可动画的属性作动画处理,但其实也是容许实现自定义属性的动画处理的。
这就须要本身来实现系统中自动计算过渡帧的操做了,也就是逐帧实现动画的方式了。
实现自定义属性的动画能够参考这篇文章: Layer 中自定义属性的动画

对于 iOS 系统提供的动画方法,上面只是从总体的角度做了一个全面的整理,还有不少细节内容没有写出来,好比 CALayer 的三维变换、CAKeyframeAnimation 的延路径动画,CAMediaTiming 的时间控制,等等。感兴趣的话,能够再看看这些内容:

相关文章
相关标签/搜索