iOS 向来以丝般顺滑的过度动画闻名,好的动画可以让用户更好地理解 app,并且可以让 app 更加有趣。有趣很重要。
iOS 动画(或者所有动画?)的原理简单来讲有两种:
这里是一张 iOS 动画相关 framework 的架构图:
从图中可以看到,要实现一个 iOS 动画从上层到底层你会接触到 UIKit、Core Animation、Core Graphics/OpenGL。他们使用的难度依次递增,相应地灵活程度以及能实现效果的复杂程度也越来越高。
CALayer 是 UIView 背后用来管理显示内容的对象,它维护了一个位图以及位图的状态信息。这个位图可能是开发者用程序绘制的(比如在 drawRect
中),也可能是开发者指定的一张图片。
Core Animation 的所有功能都是基于 layer 的。当你通过 Core Animation 修改了 layer 的属性,Core Animation 会通过 GPU 来重新渲染位图。
利用 CALayer 的属性可以做很多事情,比如改变位置、大小、形状、透视角度、圆角、透明度等,利用 mask 属性还可以做出很多好玩的效果。这些属性的详细介绍在 这里 。
CALayer 有很多有用的子类,常用的有下面几个:
strokeStart
& strokeEnd
,灵活使用有奇效。 Timing function 用来描绘动画完成度随时间增加的曲线。
怎么理解这个含义呢,在前面提到的第二种动画实现方式中,我们设置好了关键帧后,系统需要计算出关键帧中间各个时间点的状态。
假设我们要完成的是一个物体沿直线从 (0,0) 移动到 (0, 100) 的动画,动画时长是 1s。系统可以以匀速将物体从起点移动到终点,也可以加速或者减速,甚至可以先加速再减速移动到终点,timing function 就是用来描绘这里的加减速。
看看 iOS 预定义的几种常见的 timing function:
let kCAMediaTimingFunctionLinear: String let kCAMediaTimingFunctionEaseIn: String let kCAMediaTimingFunctionEaseOut: String let kCAMediaTimingFunctionEaseInEaseOut: String let kCAMediaTimingFunctionDefault: String
可以看到,这里的横坐标是时间,纵坐标可以看做动画的完成度。
正如命名描绘的那样, kCAMediaTimingFunctionLinear
是线性增加的,而 kCAMediaTimingFunctionEaseOut
是先快后慢的。
你甚至还可以通过 init(controlPoints:_:_:_:)
方法,传入两个贝塞尔曲线的控制点来自定义想要的曲线。
这样做的意义是什么呢?因为人们在日常生活中见到的物体的运动几乎没有匀速运动的,比如汽车启动和刹车,比如杯子从桌子上掉落。合理运用 timing function 可以使动画更符合人们的经验,因而显得更加自然。
UIKit 提供了一系列的基于 block 的 API。比如 UIView.animate()
系列方法。对于 view 的 animatable properties , 你只需要在 block 中修改需要对应的参数即可,比如 frame、bounds、alpha 等。
如果想实现关键帧动画,UIKit 还提供了 UIView.animateKeyframes()
方法。
UIKit 实现动画适用起来非常简单,但是有很大的局限性:如果我想改变 cornerRadius 怎么办?如果我想沿一条曲线移动一个 view 怎么办?
这个时候就需要使用 Core Animation 了(UIKit 其实也是在 Core Animation 的基础上做了一层封装)。
Core Animation 分为以下几个部分:
相比于 UIKit,CABasicAnimation 可以对更多的 CALayer 属性进行动画,比如 cornerRadius。完整的列表见 这里 。
CABasicAnimation 使用起来非常方便:
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
简单解释一下:
position.y
属性进行动画。 position.y
的初始值是 310。 position.y
的最终值是 10。 什么是 model layer?
Core Animation 实际维护了三个 layer:model layer、presentation layer 和 render layer,其中的前两个我们平时会接触到,可以分别使用 CALayer 的两个属性 modelLayer
和 presentationLayer
来获得。Render layer 是系统私有的。
Model layer 的属性是不会变化的,如果你想得到 layer 在动画过程中实时的属性,就需要通过 presentation layer 来获取。
CABasicAnimation 只能让你设置一个初始状态和一个结束状态,如果你的动画需要拆解成几个连贯的动作,CAKeyframeAnimation 可以传入多个不同的值。
此外,CAKeyframeAnimation 还可以设置 CGPath,也就是说你可以让动画对象沿着曲线移动。
let positionAnimation = CAKeyframeAnimation(keyPath: "position") positionAnimation.path = path.cgPath positionAnimation.isRemovedOnCompletion = false positionAnimation.fillMode = kCAFillModeForwards view.layer.add(positionAnimation, forKey: "MoveAlongPath")
CATransition 用来进行 view 的转场动画,具体类型有以下四种:
let kCATransitionFade: String let kCATransitionMoveIn: String let kCATransitionPush: String let kCATransitionReveal: String
还有一些私有的类型,不推荐使用。
CATransition 是 CAAnimation 的子类,使用方法跟 CABasicAnimation 一致。
如果我想让物体在移动的同时由不透明动画到透明,就可以使用 CAAnimationGroup,把多个动画组合起来,同时添加到动画对象上。
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 Graphics 了。
Core Graphics 是一套用来绘图的框架,你可以绘制曲线、填充形状,做任何想做的事情。这个时候采用的就是前面提到的第二种动画方法:每隔一段很短的时间,重新绘制一次动画对象。
注意,采用 Core Graphics 绘制图形是非常消耗性能的,因为绘制工作由 CPU 完成,而且是在主线程上!如果是简单的图形可以使用 CAShapeLayer
,利用 GPU 绘制。
如何来保证「每隔一段很短的时间」呢?iOS 为此提供了 CADisplayLink。它可以被看作是一个特殊的 timer,在系统刷新每一帧的时候,调用开发者设置的回调来重新绘制动画对象。iOS 屏幕的刷新频率是 60帧每秒,也就是每隔约 16.7 毫秒调用一次回调。这也意味着,动画中每一帧的绘制都不应该超过 16.7 毫秒。
那是不是用 NSTimer 也可以达到目的?
并不是的。CADisplayLink 可以保证每次都是在 屏幕刷新的时刻附近 来调用回调——也就是说,你的每一帧都有约 16.7 毫秒来绘制。NSTimer 不能保证触发时刻都落在屏幕刷新的时刻附近,有可能你的一帧只有 2 毫秒来绘制。
UIKit Dynamics 是随 iOS 7 推出的一套 framework,作用是模拟真实事件的物理定律。苹果声称你可以「声明式」地编写动画,你只需要描述要做什么,而不必说明怎么做,一切都由系统帮你完成。