Core Animation解析
Core Animation 是 iOS 和 macOS 上用于 图形渲染 和 动画 基础库,Core Animation 位于 AppKit 和 UIKit 之下,并紧密集成到Cocoa 和 Cocoa Touch 的 视图 工作流程中。
Core Animation 会将大部分实际绘图工作交给图形硬件(Graphics hardware)来加速渲染。这种自动化的图形加速的结果是让动画拥有更高的帧率并且显示效果更加平滑,而不会加重CPU负担而影响App的运行速度。
一般开发中不需要与用户交互的用 Core Animation,如转场动画。其它用 UIView 的动画。
1、Core Animation管理APP显示的内容
Core Animation 并不是一个绘图系统。它是用于在硬件中合成和处理应用程序内容的基础库,这个基础库的核心是 CALayer,可以使用 CALayer 实例来管理和操作内容。CALayer实例将内容捕获到位图中,图形硬件(Graphics hardware)可以很容易的操作位图。
2、修改图层会触发动画
使用 Core Animation 创建的大多数动画都涉及对 CALayer 属性的修改。改变 CALayer 的可动画属性会导致创建 隐式动画 ,从而使图层从旧值动画到新值。如果想更好地控制生成的动画行为,还可以 显式设置这些属性的动画。
3、图层树
可以按层次排列 CALayer 以创建父子关系,层的排列以类似于视图的方式影响它们管理的视觉内容,以将应用程序的视觉内容扩展到视图之外。
4、更改 CALayer 的默认行为
隐式动画是使用 action 对象实现的,action 对象是实现预定义接口的通用对象。Core Animation 使用 action 对象来实现通常与层关联的默认动画。您可以创建自己的动作对象来实现自定义动画,也可以使用它们来实现其他类型的行为。然后将动作对象分配给该层的一个属性。当那个属性改变时,Core Animation 会检索你的动作对象并告诉它执行它的动作。
CAAnimation
CAAnimation 是所有动画子类的抽象类。CAAnimation 采用 CAMediaTiming 协议为动画提供简单持续时间、速度和重复计数。CAAnimation 还采用了 CAAction 协议,为图层触发一个动画动作提供 了标准化响应。CAAnimation 定义了一个使用贝塞尔曲线来描述动画改变的时间函数。
1 2 3 4 5 6 7 8 9 10 11 12
|
let timing = CAMediaTimingFunction(name: .linear) let anim = CAAnimation()
anim.timingFunction = timing
|
隐式动画和显式动画
隐式动画:修改CALayer的动画属性时,它是从先前的值平滑过渡到新的值,会触发隐式动画,Core Animation 会使用默认的时间和动画属性来执行动画。
显式动画:创建动画对象,并配置动画参数,把该动画对象添加到图层上执行。
需要注意的是:显式动画仅生成动画,不会修改图层树中的数据。在动画结束时,Core Animation会从图层中删除动画对象,并使用其当前数据值重画该图层。如果需要保持动画后的值,需要我们手动设置图层的属性。
CABasicAnimation基本动画
1
| class CABasicAnimation : CAPropertyAnimation
|
CABasicAnimation 用于对 CALayer 的Animatable Properties从开始值更改为结束值,提供基本动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| let anim = CABasicAnimation(keyPath: "opacity")
anim.fromValue = 1 anim.toValue = 0
anim.duration = 3
anim.repeatCount = 1
anim.isRemovedOnCompletion = false
anim.delegate = self
redLayer.add(anim, forKey: "key_opacity")
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { if let opacityAnim = redLayer.animation(forKey: "key_opacity"), opacityAnim === anim { CATransaction.begin() CATransaction.setDisableActions(true) redLayer.opacity = (anim as! CABasicAnimation).toValue as! Float CATransaction.commit()
} }
|
1 2 3 4 5 6 7 8
| let keys = redLayer.animationKeys()
let anim = redLayer.animation(forKey: keys?.first ?? "")
redLayer.removeAnimation(forKey: keys?.first ?? "")
|
fillMode(填充模式)
- 对于设置了 beginTime 延迟时间的动画,当动画对象被添加到layer时,动画处于开始等待,这时,动画开始之前其动画属性的值是什么?
- 对于设置了 anim.isRemovedOnCompletion = false,动画对象在动画结束后不被移除,这时,动画结束后其动画属性的值是什么?
如下例子,position.y 的值,在动画开始之前可能为:未添加动画前的值 or fromValue;动画结束后可能为:toValue
这就需要:fillMode填充模式来控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| let anim = CABasicAnimation(keyPath: "position.y") anim.fromValue = 300 anim.toValue = 500
anim.beginTime = CACurrentMediaTime() + 3 anim.duration = 6
anim.fillMode = CAMediaTimingFillMode.both anim.isRemovedOnCompletion = false redLayer.add(anim, forKey: "position.y")
|
在使用CALayer动画时,动画结束后如何更新动画属性?上面的例子中给出了两种做法:
CAKeyframeAnimation关键帧动画
1
| class CAKeyframeAnimation : CAPropertyAnimation
|
CAKeyframeAnimation 和 CABasicAnimation 都是 CAPropertyAnimation 的子类,它依然是对CALayer的单一属性进行动画,其不同之处在于,可以设置一连串随意的值 和 时间 来做动画。
- 关键帧:指的是动画过程中特定时间点的某一帧,使用 CAKeyframeAnimation 动画时我们提供了显著的帧。关键帧之间值的插值,
Core Animation在每帧之间进行插入。在动画期间,Core Animation通过在您提供的值之间插值来生成中间值。
使用 CAKeyframeAnimation 进行动画有2种方式:
- 大多数类型的属性动画,只需要设置
values 和 keyTimes 来确定关键帧的值。然后 Core Animation 根据关键帧之间来生成插值来进行过渡动画。
- 当设置坐标动画时,需要设置
path 属性的路径,而不是单个的值。
1、对 CALayer 的背景色进行关键帧动画
可以看到 CAKeyframeAnimation 不会把初始值作为第一帧,而是突然恢复到原始的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| let anim = CAKeyframeAnimation(keyPath: "backgroundColor") anim.duration = 9
anim.values = [UIColor.green.cgColor, UIColor.yellow.cgColor, UIColor.blue.cgColor]
anim.keyTimes = [0, 0.5, 1]
anim.timingFunctions = [.init(name: .easeIn), .init(name: .easeOut)]
anim.calculationMode = .cubicPaced redLayer.add(anim, forKey: "key_backgroundColor")
|
2、使用UIBezierPath绘制动画运动的路径
1 2 3 4 5 6 7 8 9 10
| let path = UIBezierPath(ovalIn: CGRect(x: redLayer.position.x, y: redLayer.position.y, width: 200, height: 200)) let anim = CAKeyframeAnimation(keyPath: "position")
anim.path = path.cgPath anim.duration = 10 anim.timingFunctions = [.init(name: .linear), .init(name: .easeIn), .init(name: .easeOut), .init(name: .easeInEaseOut)] anim.fillMode = .both anim.isRemovedOnCompletion = false redLayer.add(anim, forKey: "key_position")
|
CASpringAnimation弹簧动画
1 2
| @available(iOS 9.0, *) class CASpringAnimation : CABasicAnimation
|
CASpringAnimation 是 CABasicAnimation 的子类,通常使用它来设置 position 的动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| anim.damping = 20.0
anim.initialVelocity = 1
anim.mass = 10
anim.stiffness = 10 redLayer.add(anim, forKey: "key_position.y")
print(anim.settlingDuration)
|
1 2 3 4 5 6 7 8 9 10 11
| let anim = CASpringAnimation(keyPath: "position.y") anim.beginTime = CACurrentMediaTime() + 1 anim.duration = 3 anim.fromValue = redLayer.position.y - 50 anim.toValue = redLayer.position.y + 300 anim.initialVelocity = 20 anim.damping = 40 anim.stiffness = 100 redLayer.add(anim, forKey: "key_position.y")
|
CAAnimationGroup动画组
1
| class CAAnimationGroup : CAAnimation
|
CABasicAnimation、CASpringAnimation、CAKeyframeAnimation都是单一的对CALayer的动画属性进行动画,CAAnimationGroup用于把多个动画组合起来同事运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| let anim = CABasicAnimation(keyPath: "opacity") anim.fromValue = 0 anim.toValue = 1 anim.duration = 3 let scaleAnim = CABasicAnimation(keyPath: "transform.scale") scaleAnim.fromValue = [1, 1, 0] scaleAnim.toValue = [1.5, 1.5, 0] scaleAnim.duration = 4 let path = UIBezierPath(ovalIn: CGRect(x: redLayer.position.x, y: redLayer.position.y, width: 200, height: 200)) let keyAnim = CAKeyframeAnimation(keyPath: "position") keyAnim.path = path.cgPath keyAnim.duration = 8 keyAnim.timingFunctions = [.init(name: .linear), .init(name: .easeIn), .init(name: .easeOut), .init(name: .easeInEaseOut)] keyAnim.fillMode = .both let group = CAAnimationGroup() group.animations = [anim, scaleAnim, keyAnim] group.duration = 8 group.isRemovedOnCompletion = false group.fillMode = .both redLayer.add(group, forKey: "key_group")
|
CATransition过渡动画
1 2
| @available(iOS 2.0, *) class CATransition : CAAnimation
|
CAPropertyAnimation 只对 CALayer 的可动画属性起作用,如果要改变一个不能动画的属性(例如:图片切换,更改图层的层级关系等),这就需要用到 CATransition 。
CATransition并不能对动画属性的两个值之间做动画,它为图层提供变化时的过渡效果,从原本外观交换过渡到一个新的外观,能影响图层的整个内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| let anim = CATransition() anim.duration = 6
anim.startProgress = 0.0
anim.endProgress = 1.0
anim.type = CATransitionType.moveIn
anim.subtype = CATransitionSubtype.fromRight
redLayer.add(anim, forKey: nil) redLayer.backgroundColor = UIColor.blue.cgColor
|
CATransition 的提供的标准动画类型太少了,那么如果我们需要做一些自定义的效果该如何做?做过渡动画基础的原则就是对原始的图层外观截图,然后添加一段动画,平滑过渡到图层改变之后那个截图的效果。如果我们知道如何对图层截图,我们就可以使用属性动画来代替CATransition或者是UIKit的过渡方法来实现动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, true, 0.0) view.layer.render(in: UIGraphicsGetCurrentContext()!) let coverImg = UIGraphicsGetImageFromCurrentImageContext()
let coverView = UIImageView(image: coverImg) coverView.frame = view.bounds view.addSubview(coverView)
let red = CGFloat(arc4random()) / CGFloat(INT_MAX) let green = CGFloat(arc4random()) / CGFloat(INT_MAX) let blue = CGFloat(arc4random()) / CGFloat(INT_MAX) view.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0)
UIView.animate(withDuration: 3, animations: { var transform = CGAffineTransform(scaleX: 0.01, y: 0.01) transform = transform.rotated(by: CGFloat.pi/2) coverView.transform = transform coverView.alpha = 0.0 }) { (res) in coverView.removeFromSuperview() }
|
动画简单的定义就是指一个值随时间的变化。Core Animation 通过 CAMediaTiming为 CAAnimation 和 CALayer 提供了基本的计时功能,和强大的时间轴功能。CAMediaTiming 协议中定义了一段动画内用于控制逝去时间的属性集合,如:动画的时间间隔、持续时间、速度、重复计数等属性。所以时间可以被任意基于一个图层或者一段动画的类控制。
对于 CAMediaTiming 协议 Apple文档的定义为:CAMediaTiming 为分层计时系统建模,允许对象在其父时间和本地时间之间映射时间。从父时间到本地时间的转换有两个阶段:
转换为“active local time”。这包括对象在父对象的时间轴上出现的时间点,以及它相对于父对象播放的速度。
从“active local time”到“basic local time”的转换。计时模型允许对象多次重复它们的基本持续时间,并且可以选择在重复之前回放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public protocol CAMediaTiming { var beginTime: CFTimeInterval { get set }
var duration: CFTimeInterval { get set }
var speed: Float { get set }
var timeOffset: CFTimeInterval { get set } var repeatCount: Float { get set } var repeatDuration: CFTimeInterval { get set } var autoreverses: Bool { get set } var fillMode: CAMediaTimingFillMode { get set } }
|
层级关系时间
每个动画和图层在时间上都有它自己的层级概念,相对于它的父亲来测量。对图层调整时间将会影响到它本身和子图层的动画,但不会影响到父图层。
CoreAnimation有一个全局时间的概念,通过如下方法获取,其值是根据CPU的时钟周期数来描述的。我们无需知道该函数返回的值,它的值作用在于给动画的时间测量提供一个相对值。
每个 CALayer 和 CAAnimaion 实例都有自己本地时间的概念,它是根据父图层/动画层级关系中的 beginTime、timeOffset 和 speed 属性计算来的。CALayer提供了如下方法来转换不同图层之间的本地时间:
1 2 3 4 5 6 7 8 9 10
|
返回
func convertTime(_ t: CFTimeInterval, from l: CALayer?) -> CFTimeInterval
func convertTime(_ t: CFTimeInterval, to l: CALayer?) -> CFTimeInterval
|
来看看下面例子:
1 2 3 4 5 6 7 8 9 10 11
| let layer = CALayer() let offLayer = CALayer() offLayer.timeOffset = CFTimeInterval(1) offLayer.speed = 0.5
print(offLayer.convertTime(CFTimeInterval(0.5), from: layer))
print(layer.convertTime(CFTimeInterval(0.5), to: offLayer))
|
例子:动画的暂停、恢复、取消
这个例子是根据 timeOffset 属性提供的公式来进的,具体讲解可以学习 iOS动画暂停与恢复的理解
1 2 3 4
| t = (tp - begin) * speed + offset tp是父layer的时间点,为了方便理解,可以认为是绝对时间,随时间流逝而增加。
begin、speed、offset就是动画的属性beginTime、speed、timeOffset。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @objc func startAnim() { let anim = CABasicAnimation(keyPath: "position") anim.duration = 6.0 anim.fromValue = redLayer.position anim.toValue = CGPoint(x: 100, y: 400) redLayer.add(anim, forKey: "key_position") }
@objc func pauseAnim() { let pausedTime = redLayer.convertTime(CACurrentMediaTime(), from: nil) redLayer.speed = 0.0 redLayer.timeOffset = pausedTime }
@objc func continueAnim() { let pausedTime = redLayer.timeOffset redLayer.speed = 1.0 redLayer.timeOffset = 0.0 redLayer.beginTime = 0.0 let timeSincePause = redLayer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime redLayer.beginTime = timeSincePause }
@objc func removeAnim() { redLayer.removeAnimation(forKey: "key_position") }
|
例子:控制动画时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| let slider = UISlider(frame: CGRect(x: 0, y: 0, width: 250, height: 20)) slider.center = view.center slider.minimumValue = 0.0 slider.maximumValue = 1.0 slider.addTarget(self, action: #selector(changeColor(_ :)), for: .valueChanged) view.addSubview(slider) let anim = CABasicAnimation(keyPath: "backgroundColor") anim.fromValue = UIColor.red.cgColor anim.toValue = UIColor.blue.cgColor anim.duration = 1.0 redLayer.add(anim, forKey: "backgroundColor")
redLayer.speed = 0.0 @objc func changeColor(_ slider: UISlider) { redLayer.timeOffset = CFTimeInterval(slider.value) }
|
CATransaction(事务)
1 2
| @available(iOS 2.0, *) class CATransaction : NSObject
|
CATransaction(发音CA [trænˈzækʃn] ) 是 Core Animation 中负责成批的把多个图层树的修改作为一个原子更新到渲染树的机制。图层的动画属性的每一个修改必然是事务的一个部分。
在大多数情况下,我们不需要手动创建 CATransaction,每当我们把 显式动画 或者 隐式动画 添加到图层时,Core Animation 都会自动创建一个隐式事务。我们也可以创建显式事务来更精确地管理动画。
- 隐式事务:当图层树被没有获得事务的线程修改的时候将会自动创建,当线程的运行循环(runloop)执行下次迭代的时候将会自动提交事务。如下改图层的 transform,position、backgroundColor动画属性,依赖隐式事务来确保动画同时一起发生。
1 2 3 4 5 6
| redLayer.transform = CATransform3DScale(redLayer.transform, 1.2, 1.2, 1.0) redLayer.position = CGPoint(x: redLayer.position.x + 10, y: redLayer.position.y + 30) let red = CGFloat(arc4random()) / CGFloat(INT_MAX) let green = CGFloat(arc4random()) / CGFloat(INT_MAX) let blue = CGFloat(arc4random()) / CGFloat(INT_MAX) redLayer.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0).cgColor
|
- 显式事务:当修改图层树之前,可以通过给 CATransaction 类发送一个 begin 消息来创建一个显式事务,修改完成之后发送 comit 消息。我们经常通过显示事务作如下操作:
暂时禁用图层的行为
通过 显示事务 暂时禁用图层的行为,使得在事务范围所作的任何更改也不会因此而发生的动画。
1 2 3 4
| CATransaction.begin() CATransaction.setDisableActions(false) redLayer.transform = CATransform3DScale(redLayer.transform, 1.2, 1.2, 1.0) CATransaction.commit()
|
重载隐式动画的时间
由于隐式动画的默认时间为0.25s,我们可以设置显式事务的key-value来作为动画持续时间。
1 2 3 4
| CATransaction.begin() CATransaction.setAnimationDuration(3) redLayer.position = CGPoint(x: redLayer.position.x + 10, y: redLayer.position.y + 30) CATransaction.commit()
|
嵌套显式事务
显示事务可以被嵌套,通过事务嵌套可以 禁用部分动画的行为 或者 在属性被修改的时候产生的动画使用不同的时间。仅当最外层的事务被提交的时候,动画才会发生。
1 2 3 4 5 6 7 8 9 10 11 12 13
| CATransaction.begin() CATransaction.setAnimationDuration(1.0) redLayer.position = CGPoint(x: 200, y: 400) CATransaction.begin() CATransaction.setAnimationDuration(3.0) redLayer.transform = CATransform3DMakeScale(1.2, 1.2, 1.0) redLayer.opacity = 0.1 CATransaction.commit() CATransaction.commit()
|
同时设置UIView和CALayer更改的动画
在iOS中,UIView都关联一个CALayer,并且UIView本身就直接从CALayer中派生出其大部分的数据。
在iOS中可以根据需要自由地混合基于UIView和基于CALayer的动画代码。可以在基于UIView的动画块(block-based animation)的内部或外部应用 基于 CALayer 的动画。
如果改变的是UIView关联的CALayer的动画属性时,CALayer会采用基于UIView的动画块(block-based animation)的动画参数;而当改变的是自己创建的CALayer时,会忽略基于视图的动画块参数。
UIView 类默认禁用基于层的动画,但是可以在基于视图的动画块内部内部启用它们。
1 2 3 4 5 6 7 8 9 10
| UIView.animate(withDuration: 1.0) { self.purpleView.layer.opacity = 0.0 let anim = CABasicAnimation(keyPath: "position") anim.fromValue = self.purpleView.layer.position anim.toValue = CGPoint(x: 200, y: 600) anim.duration = 3.0 self.purpleView.layer.add(anim, forKey: "key_positon") }
|
学习博客
Core Animation Programming Guide
Introduction to Animation Types and Timing Programming Guide
Advanced Animation Tricks
Graphics&Animation
iOS Core Animation: Advanced Techniques中文译本
Controlling Animation Timing
Animation Class Roadmap
iOS动画原理–CAMediaTiming
玩转iOS开发Core Animation