iOS Animation 指北

写在前面

iOS 向来以丝般顺滑的过度动画闻名,好的动画可以让用户更好地理解 app,并且可以让 app 更加有趣。有趣很重要。

iOS 动画(或者所有动画?)的原理简单来讲有两种:

  1. 告诉系统动画对象在某几个时刻的状态(关键帧),由系统自动补全这些时刻之间的中间状态,再把所有这些状态平滑地显示出来。这种动画也叫做关键帧动画。
  2. 每隔一段很短的时间,重新绘制一次动画对象。

整体架构

这里是一张 iOS 动画相关 framework 的架构图:

Core Animation 架构

从图中可以看到,要实现一个 iOS 动画从上层到底层你会接触到 UIKit、Core Animation、Core Graphics/OpenGL。他们使用的难度依次递增,相应地灵活程度以及能实现效果的复杂程度也越来越高。

基础知识

CALayer

CALayer 是 UIView 背后用来管理显示内容的对象,它维护了一个位图以及位图的状态信息。Core Animation 的所有功能都是基于 layer 的。当你通过 Core Animation 修改了 layer 的属性时,Core Animation 会通过 GPU 来重新渲染位图,因为是硬件渲染,因此速度非常快。

利用 CALayer 的属性可以做很多事情,比如改变位置、大小、形状、透视角度、圆角、透明度等,利用 mask 属性还可以做出很多好玩的效果。这些属性的详细介绍在这里

CALayer 有很多有用的子类,常用的有下面几个:

  • CAShapeLayer:用于绘制曲线等图形,有两个重要的属性 strokeStart & strokeEnd,灵活使用有奇效。
  • CATextLayer:专门用来处理文字的 layer。
  • CAGradientLayer:顾名思义,用来处理渐变。

Timing function

Timing function 用来描绘动画完成度随时间增加的曲线。

怎么理解这个含义呢,在前面提到的第二种动画实现方式中,我们设置好了关键帧后,系统需要计算出关键帧中间各个时间点的状态。

假设我们要完成的是一个物体沿直线从 (0,0) 移动到 (0, 100) 的动画,动画时长是 1s。系统可以以匀速将物体从起点移动到终点,也可以加速或者减速,甚至可以先加速再减速移动到终点,timing function 就是用来描绘这里的加减速。

看看 iOS 预定义的几种常见的 timing function:

1
2
3
4
5
let kCAMediaTimingFunctionLinear: String
let kCAMediaTimingFunctionEaseIn: String
let kCAMediaTimingFunctionEaseOut: String
let kCAMediaTimingFunctionEaseInEaseOut: String
let kCAMediaTimingFunctionDefault: String

预设的 timing function

可以看到,这里的横坐标是时间,纵坐标可以看做动画的完成度。

正如命名描绘的那样,kCAMediaTimingFunctionLinear 是线性增加的,而 kCAMediaTimingFunctionEaseOut 是先快后慢的。

你甚至还可以通过 init(controlPoints:_:_:_:) 方法,传入两个贝塞尔曲线的控制点来自定义想要的曲线。

这样做的意义是什么呢?因为人们在日常生活中见到的物体的运动几乎没有匀速运动的,比如汽车启动和刹车,比如杯子从桌子上掉落。合理运用 timing function 可以使动画更符合人们的经验,因而显得更加自然。

UIKit

UIKit 提供了一系列的基于 block 的 API。比如 UIView.animate() 系列方法。对于 view 的 animatable properties, 你只需要在 block 中修改需要对应的参数即可,比如 frame、bounds、alpha 等。

如果想实现关键帧动画,UIKit 还提供了 UIView.animateKeyframes() 方法。

UIKit 适用于简单的动画,如移动、旋转、缩放、改变颜色等。

Core Animation

UIKit 实现动画适用起来非常简单,但是有很大的局限性:如果我想改变 cornerRadius 怎么办?如果我想沿一条曲线移动一个 view 怎么办?

这个时候就需要使用 Core Animation 了(UIKit 其实也是在 Core Animation 的基础上做了一层封装)。

Core Animation 分为以下几个部分:

CABasicAnimation

相比于 UIKit,CABasicAnimation 可以对更多的 CALayer 属性进行动画,比如 cornerRadius。完整的列表见这里

CABasicAnimation 使用起来非常方便:

1
2
3
4
5
6
7
let verticalAnimation = CABasicAnimation(keyPath: "position.y") // 1
verticalAnimation.fromValue = 310 // 2
verticalAnimation.toValue = 10 // 3
verticalAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) // 4
view.layer.add(verticalAnimation, forKey: "Fall") // 5

view.layer.position.y = 10 //6

简单解释一下:

  1. 这里我们创建了一个 CABasicAnimation 实例,要对 layer 的 position.y 属性进行动画。
  2. position.y 的初始值是 310。
  3. position.y 的最终值是 10。
  4. 指定了动画的 timing function,后面会介绍。
  5. 将动画添加到 view 的 layer 上。
  6. 将 layer 的 model layer 属性更改为最终属性。

什么是 model layer?

Core Animation 实际维护了三个 layer:model layer、presentation layer 和 render layer,其中的前两个我们平时会接触到,可以分别使用 CALayer 的两个属性 modelLayerpresentationLayer 来获得。Render layer 是系统私有的。

Model layer 的属性是不会变化的,如果你想得到 layer 在动画过程中实时的属性,就需要通过 presentation layer 来获取。

CAKeyframeAnimation

CABasicAnimation 只能让你设置一个初始状态和一个结束状态,如果你的动画需要拆解成几个连贯的动作,CAKeyframeAnimation 可以传入多个不同的值。

此外,CAKeyframeAnimation 还可以设置 CGPath,也就是说你可以让动画对象沿着曲线移动。

1
2
3
4
5
6
let positionAnimation = CAKeyframeAnimation(keyPath: "position")
positionAnimation.path = path.cgPath
positionAnimation.isRemovedOnCompletion = false
positionAnimation.fillMode = kCAFillModeForwards

view.layer.add(positionAnimation, forKey: "MoveAlongPath")

CATransition

CATransition 用来进行 view 的转场动画,具体类型有以下四种:

1
2
3
4
let kCATransitionFade: String
let kCATransitionMoveIn: String
let kCATransitionPush: String
let kCATransitionReveal: String

还有一些私有的类型,不推荐使用。

CATransition 是 CAAnimation 的子类,使用方法跟 CABasicAnimation 一致。

CAAnimationGroup

如果我想让物体在移动的同时由不透明动画到透明,就可以使用 CAAnimationGroup,把多个动画组合起来,同时添加到动画对象上。

1
2
3
4
5
6
7
8
9
10
11
12
let animationA = ...
let animationB = ...
let animationC = ...

let animationGroup = CAAnimationGroup()
animationGroup.animations = [animationA, animationB, animationC];
animationGroup.duration = 0.7
animationGroup.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
animationGroup.isRemovedOnCompletion = false
animationGroup.fillMode = kCAFillModeForwards

view.layer.add(animationGroup, forKey: "GroupAnimation")

此外,CAAnimationGroup 还可以设置一个 completion block,在所有动画完成时调用。

Core Animation 适用于较为复杂的,有多个中间状态或者包含曲线路径的动画。

Core Graphics

如果动画复杂到不能够用改变位置、透明度、大小等属性的组合来完成,就需要使用 Core Graphics 了。

Core Graphics 是一套用来绘图的框架,你可以绘制曲线、填充形状,做任何想做的事情。这个时候采用的就是前面提到的第二种动画方法:每隔一段很短的时间,重新绘制一次动画对象。

注意,采用 Core Graphics 绘制图形是非常消耗性能的,因为绘制工作由 CPU 完成,而且是在主线程上!如果是简单的图形可以使用 CAShapeLayer,利用 GPU 绘制。

如何来保证「每隔一段很短的时间」呢?iOS 为此提供了 CADisplayLink。它可以被看作是一个特殊的 timer,在系统刷新每一帧的时候,调用开发者设置的回调来重新绘制动画对象。iOS 屏幕的刷新频率是 60帧每秒,也就是每隔约 16.7 毫秒调用一次回调。这也意味着,动画中每一帧的绘制都不应该超过 16.7 毫秒。

那是不是用 NSTimer 也可以达到目的?

并不是的。CADisplayLink 可以保证每次都是在屏幕刷新的时刻附近来调用回调——也就是说,你的每一帧都有约 16.7 毫秒来绘制。NSTimer 不能保证触发时刻都落在屏幕刷新的时刻附近,有可能你的一帧只有 2 毫秒来绘制。

Core Graphics + CADisplayLink 适用于复杂的,不能使用移动、旋转、形变组合完成的动画。

大杀器 UIKit Dynamics

UIKit Dynamics 是随 iOS 7 推出的一套 framework,作用是模拟真实事件的物理定律。苹果声称你可以「声明式」地编写动画,你只需要描述要做什么,而不必说明怎么做,一切都由系统帮你完成。

Ref

给鸡排饭加个蛋