写在前面
iOS 向来以丝般顺滑的过度动画闻名,好的动画可以让用户更好地理解 app,并且可以让 app 更加有趣。有趣很重要。
iOS 动画(或者所有动画?)的原理简单来讲有两种:
- 告诉系统动画对象在某几个时刻的状态(关键帧),由系统自动补全这些时刻之间的中间状态,再把所有这些状态平滑地显示出来。这种动画也叫做关键帧动画。
- 每隔一段很短的时间,重新绘制一次动画对象。
整体架构
这里是一张 iOS 动画相关 framework 的架构图:
从图中可以看到,要实现一个 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 | let kCAMediaTimingFunctionLinear: String |
可以看到,这里的横坐标是时间,纵坐标可以看做动画的完成度。
正如命名描绘的那样,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 | let verticalAnimation = CABasicAnimation(keyPath: "position.y") // 1 |
简单解释一下:
- 这里我们创建了一个 CABasicAnimation 实例,要对 layer 的
position.y
属性进行动画。 position.y
的初始值是 310。position.y
的最终值是 10。- 指定了动画的 timing function,后面会介绍。
- 将动画添加到 view 的 layer 上。
- 将 layer 的 model layer 属性更改为最终属性。
什么是 model layer?
Core Animation 实际维护了三个 layer:model layer、presentation layer 和 render layer,其中的前两个我们平时会接触到,可以分别使用 CALayer 的两个属性 modelLayer
和 presentationLayer
来获得。Render layer 是系统私有的。
Model layer 的属性是不会变化的,如果你想得到 layer 在动画过程中实时的属性,就需要通过 presentation layer 来获取。
CAKeyframeAnimation
CABasicAnimation 只能让你设置一个初始状态和一个结束状态,如果你的动画需要拆解成几个连贯的动作,CAKeyframeAnimation 可以传入多个不同的值。
此外,CAKeyframeAnimation 还可以设置 CGPath,也就是说你可以让动画对象沿着曲线移动。
1 | let positionAnimation = CAKeyframeAnimation(keyPath: "position") |
CATransition
CATransition 用来进行 view 的转场动画,具体类型有以下四种:
1 | let kCATransitionFade: String |
还有一些私有的类型,不推荐使用。
CATransition 是 CAAnimation 的子类,使用方法跟 CABasicAnimation 一致。
CAAnimationGroup
如果我想让物体在移动的同时由不透明动画到透明,就可以使用 CAAnimationGroup,把多个动画组合起来,同时添加到动画对象上。
1 | let animationA = ... |
此外,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,作用是模拟真实事件的物理定律。苹果声称你可以「声明式」地编写动画,你只需要描述要做什么,而不必说明怎么做,一切都由系统帮你完成。