《iOS 三问》 -- 从动画系统的实现谈 iOS 核心动画

Core Animation 库是 iOS 动画技术的基础,由一系列类与子类组成(他们基本都有个特点就是以各种 CA 开头,如 CALayer,CAAnimation)。我们之前学习到的 View 动画,隐式 Layer 动画等,都是基于 Core Animation 库的一个封装实现。Core Animation 又称显式动画,使用显式动画技术,我们可以更细致地定义我们要的动画的整个实现过程。

通过本章,我希望和大家一起不仅掌握 iOS 的动画编程,更掌握 UI 系统关于动画编程的核心技术与探索核心技术的方法。

学习显式动画,可以揭示 iOS 动画库实现的整个细节,他的设计思想与实现。

1 UI 系统中各种动画的实现与 iOS 的具体实现

据我的总结,一般 UI 系统都支持如下几种动画:

第一种是帧动画 (Frame Animation)。这是最简单的动画,就是把动画拆成一帧帧图片连续播放,从而形成的动画。我们在 Image Animation 已经学习过了;

第二种是属性动画 (Property Animation)。这是最常见的动画方式,我们在 Animation 概述 中介绍过,就是把动画的本质定义为视图某些属性随时间的变化。这个也是我们今天学习的主要动画方式。

第三种是布局动画 (Layout Animation),就是当布局时其子视图展示出来的动画,比如你在桌面移动图标时,你移动一个图标,后面的图标会以动画的形式后移让开一个空。这种动画其实就是基于布局事件与属性动画复合处理而成,并不是新的动画系统。

第四种是转场动画 (Transition Animation),通常用于一个视图到另一个视图的转换。比如 A 视图渐隐 B 视图渐现这样,或 B 视图从边上推进来盖住 A 视图,其实也是属性动画的复合实现。

第五种就是视频动画。其实就是播放视频代替动画,这个就不多说了

当然因为本人对游戏编程,对 unity3d 等技术不熟,还有一些物理引擎动画,通过模拟现实生活中的物理动作实现的动画等,因为我对此不熟,就不介绍了,等我后面学习到再作个补充。

本章主要介绍显示属性动画,这也是 Core Animation 动画的核心。

1.1 让我们来设计一套属性动画系统

如果让我们来设计一套简单的属性动画系统,根据我们在 动画概述 里学习到的,主要要有这几个部分组成:

  1. 计算 - 对属性的计算,比如某时刻下可动画属性的值;
  2. 计时 - 对象属性随时间是个怎样的变化;
  3. UI 刷新 - 属性变化后怎样刷新 UI;
  4. 线程管理 - 当然,总不可以在主线程绘制动画吧,驱动动画的子线程我们还要加以管理。

由此,我们可以作出最简单的动画系统:

最简单的动画系统

我们看下这套系统:

  1. XXLayer 负责 UI 的显示与重绘,我们假设动画针对 layer 的一个属性 propertyX,则它的重绘方法会依赖 propertyX 绘制出当前的效果;
  2. XXAnimationThreadXXAnimation 负责动画线程管理,我们用 XXAnimation 封装一个动画,它包括了一个属性动画需要的基本参数,表示在 duration 时间段时,属性值从 from 值过渡到 to 值。XXAnimationThread 提供线程管理与计时功能,开启动画后,每隔一段时间利用 XXAnimation 的参数与计时方法算出属性当前值,并调用 XXLayerredraw 方法刷新界面;
  3. XXAnimation 中的 timingFunction 负责最核心的计算功能,其

我们用伪代码剖析一下最简单动画系统的实现,主要细节在于 XXAnimationThreadstartAnimation 方法:

计时方法
1

动画线程实时计算与刷新方法
1
2
3
4
5
6
7
8
9
10
11
12
13

void startAnimation()
{
long currentTime = getCurrentTime();

// 在动画的持续时间内隔一段时间计算插值,刷新 UI
while(currentTime <= (animation.beginTime + animation.duration)) {
let propertyX_value = animation.timingFunction(currentTime);
layer.propertyX = propertyX_value;
layer.redraw();
}
}

2 转场动画 (Transition Animation)

在之前介绍 View Animation 时我们介绍过过 转场动画

2.1 图层树的转场动画

CATransition 可以对图层树做动画,这个是非常强大的(个人猜测这个应该是对整个图层树的 cache image 做动画实现的)。这意味着你可以不用考虑图层树的细节,直接在 layer 根部使用 transition animation,方便地启用动画。

比如我们下面的操作想在 tab 切换时展示页面转换的渐隐渐现动画:

tab 切换时展示页面转换的渐隐渐现动画
1
2
3
4
5
6
7
8
9
- (void)tabBarController:(UITabBarController *)tabBarController didSelectVie
{
// 创建转场动画对象
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;

// 应用于 tab 图层根部
[self.tabBarController.view.layer addAnimation:transition forKey:nil];
}

2.2 CAMediaTiming 协议

CAMediaTiming 协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayerCAAnimation 都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。

  • 一个动画迭代

CAMediaTiming 中定义的一些属性 (也可以称之为参数),给要进行的动画的一次迭代指定了时间与计时的参数。比如 durationrepeatCount 这两个参数,duration 表示动画一个迭代的持续时间repeatCount 表示整个完整动画包含多少次迭代。,如果 duration=2repeatCount=3,就意思着整个动画时长为 2*3=6 秒。

durationrepeatCount 的默认值为 0,分别代码 0.25 秒1 次 迭代。把 repeatCount 设为 INFINITY 表示无限迭代;

  • timeOffset 和 beginTime

timeOffsetbeginTime 类似,但是和增加 beginTime 导致的延迟动画不同,增加 timeOffset 只是让动画快进到某一点,例如,对于一个持续 1 秒的动画 来说,设置 timeOffset 为 0.5 意味着动画将从一半的地方开始。

  • 其它参数

speed 是一个时间的倍数,默认 1.0,减少它会减慢图层 / 动画的时间,增加它会加快速度。如果 2.0 的速度,那么对于一个 duration 为 1 的动画,实际上在 0.5 秒的时候就已经完成了。

此外,还有 autoreverse,会自动生成从 tofrom 的反向动画等参数,具体罗列在文章头部的 UML 图中。

最后值得一说的就是 fillMode,它是一个 NSString 类型,表示当动画播放完后保持在一个什么状态。我们都之前说过,一般来说,动画播放完就被 remove 掉了。但是如果你设置了 removeOnCompletionNO,就会保持在动画的一个状态,而 fillMode 就是控制它展示什么状态。其可选属性:

1
2
3
4
kCAFillModeForwards
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved

kCAFillModeRemoved 为默认值,表示动画不再播放时显示图层当前模型中的值。而 forwards,backwards,both 表示展示为动画开始时或结束时的状态。这样可以避免动画被中断,或因各种原因在结束时突然地变到一个状态去,要注意的一点就是,需要把 removeOnCompletion 设置为 NO

3 引用

【1】[Matt Neuburg - 《Programming iOS deep into views,view controllers and frameworks》]
【2】[Nick LockWood - 《iOS Core Animation: Advanced Techniques》]

CAAnimation 定义了动画的基本属性,在上面的类图中我用空行大致分了段,有 + (instancetype) animation 这样的方便创建动画对象的实例方法,设置动画回调对象 delegate;有与动画计时相关的 beginTime,timeOffset,duration 等,还可以指定计时方法 timingFunction,以及动画的额外控制 autoreversesrepeatCount 等。另外 CAAnimation 实现了 KVC,也就是你可以像字典一样对特定的 key 赋值,这点后面会说到。

CAPropertyAnimation 更进一步,对动画所对应的动画属性做了定义,keyPath 对应于要产生动画的 CALayer 动画属性。additive 表示动画结束后的属性值将做为 layer 的当前值 (即把 layer 的当前呈现值赋给模型值,参考 动画基础中的 ‘ 呈现与模型 ‘ 小节理解下)。

CABasicAnimation 则是动画继承结构中较常用到的实现类了,使用它可以定义一个具体的基本的动画行为。比如 fromValuetoValue 表示一个可动画的属性从一个值渐变到另一个值。byValue 表示从动画属性值的变化量,比如 fromeValue=0,byValue=5, 则表示可动画属性从 0 变化到 5. 而如果只设置了 byValue,则表示可动画属性是从属性的当前值变化到 当前值 + byValue。到这里你就知道了,使用 byValue, 其实就是让系统帮你计算 toValue,动画的本质还是属性值从 fromValue 变化到 toValue

3.1 使用 CABasicAnimation

使用 CABasicAnimation 时首先要注意下 呈现与模型问题。因为我们直接修改 layer 的值时改的是模型层的值,直到渲染周期来临时执行动画逻辑此时呈现层属性值才会从 fromValue 变到 toValue。而动画结果后,动画对象会被干掉,此时呈现层会呈现模型层当前的属性值。也就是如果你修改的 layer 的值与动画 toValue 不一致的话,动画结果后 layer 会突然变到你改变的值,这样对用户来说很不友好。所以,一般我们需要保证动画结束后的值与当前模型层的值相等。

一种方法是省去 fromValuetoValue 值,因为系统会自动帮你计算。参考 动画基础中的 ‘ 呈现与模型 ‘ 小节理解下,我们说 Layer 有 呈现层 (presentationLayer)模型层 (moduleLayer),当我们改变 layer 的值时,先是改变其模型层的值,直到下一次渲染周期到来时触发动画才开始变化呈现层的属性值。而这时呈现层的当前值也就是你改变 Layer 属性值之前的值,也就是动画的 fromValue。而要变化到的值也就是 toValue 则是模型层当前的值。如果这样说不好理解可以参考下下面的示例(假设 animLayer 当前 x 值为 100):

省去 fromValue 与 toValue 的 BaseAnimation
1
2
3
4
5
6
7
8
9
10
11
12
13
[CATransaction setDisableActions:YES]; // 去掉隐式动画
CGPoint newPosition = animLayer.position;
newPosition.x = 200;
animLayer.position = newPosition; // 改变 layer 的属性值,将在下个渲染周期呈现
//-- 构造 CABasicAnimation
// 这里不用设置 fromValue 与 toValue (下面的注释部分)
// 系统值自动将 fromValue 设为 position 修改之前的值,toValue 设为修改之后的值 (200)
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"position"];
anim.duration = 1;
// anim.fromValue = [NSValue valueWithCGPoint:CGPointMake(100, 50)];
// anim.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 50)];
//-- 开启动画
[animLayer addAnimation:anim forKey:nil];

上面动画开启后,animLayer 将会从 x=100 处移动 x=200,然后停在那里。

还有另一种方法是使用 addtive,这样就可以省去在添加动画之前修改 CALayer 模型层的属性值。因为如上文所述,动画结束后会将 呈现层 (presentationLayer) 的属性值同步到 模型层 (moduleLayer) 上。比如同样是实现上面的 layer 从 0 移动 200 处的动画

1