Core Animation解析

Core Animation解析

Core AnimationiOSmacOS 上用于 图形渲染 和 动画 基础库,Core Animation 位于 AppKitUIKit 之下,并紧密集成到CocoaCocoa 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
/* 时间曲线函数:将动画的速度描述为一个简单的贝塞尔曲线。
* 当前提供的值有:
* .linear:均匀线性
* .easeIn:缓入。先缓慢,后加速
* .easeOut:缓出。先快速,后减速
* .easeInEaseOut:缓入缓出。先缓慢,中间加速,后减速
* .default:隐式动画使用的曲线
*/
let timing = CAMediaTimingFunction(name: .linear)
let anim = CAAnimation()
// 定义动画速度(节奏、步调)的时间函数。默认值为nil,表示线性速度,动画在持续时间内持续均匀发生
anim.timingFunction = timing
1
2
3
4
5
6
7
8

/* 自定义时间曲线的函数
* c1x,c1y代表贝塞尔曲线的第一个点;c2x,c2y代表贝塞尔曲线的第二个点;
* 它们的取值范围都是 0~1
* 具体取值,可以参考 贝塞尔曲线绘制的网站:https://cubic-bezier.com/#.17,.67,.83,.67 或者 http://www.roblaplaca.com/examples/bezierBuilder/
* https://github.com/keefo/CATweaker 这个xcode插件比较老了
*/
//let onetiming = CAMediaTimingFunction(controlPoints: Float, c1y, c2x, c2y)

隐式动画和显式动画

隐式动画:修改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
// 动画持续时间,默认值0.25
anim.duration = 3
// 动画重复的次数,无限循环 MAXFLOAT
anim.repeatCount = 1
// 动画完成后,动画对象是否从CALayer中移除。默认为 true 移除,动画属性会恢复到动画前的状态
anim.isRemovedOnCompletion = false
// 通过代理监听动画的开始和结束
anim.delegate = self
/*把动画添加到图层,使用唯一的key来标识该动画。
添加的该动画对象,是进行了复制,而不是引用。因此不能引用该动画对象,作后续的修改。可以通过其 key 来获取该动画对象,调用CALayer的 .animation(forKey:方法。
*/
redLayer.add(anim, forKey: "key_opacity")

// CAAnimationDelegate,通过代理在动画结束后,更新动画属性的值。
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()
// 根据需要是否移除动画对象
// redLayer.removeAnimation(forKey: "opacity")
}
}
1
2
3
4
5
6
7
8
// 返回[String],添加到图层的动画key(动画方式)
let keys = redLayer.animationKeys()

// 根据 动画的key,获取被添加的动画对象
let anim = redLayer.animation(forKey: keys?.first ?? "")

// 根据 动画的key,删除动画对象
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
// 动画延迟3秒执行
anim.beginTime = CACurrentMediaTime() + 3
anim.duration = 6

/* 填充模式,决定动画属性的值,在动画之前(beginTime不等于0)或动画之后的值。
forwards: 动画开始前,保持未添加动画前的值;动画结束后,layer会一直保持着动画最后的状态。
backwards: 动画开始前,保持值为 fromValue;动画结束后,保持值为未添加动画的值
both: 动画开始前保持 fromValue,动画结束时保持toValue
removed: 动画开始和结束,都不受动画的影响

注意:forwards 和 both 必须在 isRemovedOnCompletion = false 生效。因为显式动画结束后,动画对象移除,动画属性复原。
*/
anim.fillMode = CAMediaTimingFillMode.both
anim.isRemovedOnCompletion = false
redLayer.add(anim, forKey: "position.y")

// 根据需要,在合适的时候把动画属性设置为动画后的值,并移除动画对象。
// redLayer.position.y = 500
// redLayer.removeAnimation(forKey: "position.y")

在使用CALayer动画时,动画结束后如何更新动画属性?上面的例子中给出了两种做法:

  • 方式一:是代理中回调更新动画属性。
    这种方式的弊端是,当有多个动画的代理对象相同时,那么添加动画时要保证动画key不同,并且动画结束后,要做一些判断才能更新动画属性的值,这样做比较麻烦,建议使用方式二。

  • 方式二:通过设置动画对象的属性 .fillMode.isRemovedOnCompletion 来控制。

CAKeyframeAnimation关键帧动画

1
class CAKeyframeAnimation : CAPropertyAnimation

CAKeyframeAnimation 和 CABasicAnimation 都是 CAPropertyAnimation 的子类,它依然是对CALayer的单一属性进行动画,其不同之处在于,可以设置一连串随意的值 和 时间 来做动画。

  • 关键帧:指的是动画过程中特定时间点的某一帧,使用 CAKeyframeAnimation 动画时我们提供了显著的帧。关键帧之间值的插值,Core Animation在每帧之间进行插入。在动画期间,Core Animation通过在您提供的值之间插值来生成中间值。

使用 CAKeyframeAnimation 进行动画有2种方式:

  • 大多数类型的属性动画,只需要设置 valueskeyTimes 来确定关键帧的值。然后 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

/* 一个数组,其元素用于动画的关键帧值,动画对象在相应的时间内依次显示关键值。当动画对象 path = nil 时,才使用values。
* 每个值作用的时间由动画计时器决定,动画计时器由:calculationMode、keyTimes、timingFunctions 来决定
* 关键帧之间值的插值,Core Animation在每帧之间进行插入。当 calculationMode = .discrete 时,不提供插值。
*/
anim.values = [UIColor.green.cgColor,
UIColor.yellow.cgColor,
UIColor.blue.cgColor]

/* 用于定义应用给定关键帧段的时间,每个时间点表示关键帧值的时间点。当未设置该值时,表示每个关键帧的时间平分。
* 该数组元素的取值范围为 0.0~1.0,连续的元素必须大于或等于上一个元素。
* 时间点的个数与 values的元素数 或者 path的控制点数 相等。
*/
anim.keyTimes = [0, 0.5, 1]

// 定义每个关键帧段的动画时间曲线,其数组的元素个数 等于 关键帧数 - 1。
anim.timingFunctions = [.init(name: .easeIn), .init(name: .easeOut)]

/* 指定接收器如何计算关键帧之间的插值
* .cubic 平滑线性计算
* .cubicPaced
* .discrete 依次使用每个关键帧值,不计算插值
* .linear 默认值,关键帧值之间线性计算
* .paced
*/
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")
// 如果设置 path 有值,则不使用 values 中的值来动画
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
// 阻尼系数,默认值为10。用来阻止弹簧拉伸。增大其值,摆动会减少,沉降持续时间可能会小于持续时间
anim.damping = 20.0

// 初始速度,默认值为0. 负值表示对象远离弹簧附着点,正值表示对象向弹簧附着点移动。
anim.initialVelocity = 1

// 质量,默认值为1。值越大,弹簧会受到更多次的摆动,拉伸幅度也越大
anim.mass = 10

// 弹性系数,默认值100。其值越大,摆动次数越少,持续时间越短
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
// 过渡开始的进度值,取值0.0~1.0。
anim.startProgress = 0.0
// 过渡终点,其值大于startProgress,但不大于1.0
anim.endProgress = 1.0

/* 变换效果
* CATransitionType类型值有这么几种:.fade(淡入淡出)、.moveIn(滑入)、.push(滑出)、.reveal(揭开)
*
* CATransitionType(rawValue: "")方式,私有API还有这样几种:
* cube(立方体翻滚)、oglFlip(上下左右翻转)、suckEffect(如一块布被抽走)、rippleEffect(水滴)、
* pageCurl(向上翻页)、pageUnCurl(向下翻页)、cameraIrisHollowOpen(镜头打开)、cameraIrisHollowClose(相机镜头关闭)
*/
anim.type = CATransitionType.moveIn
// anim.type = CATransitionType(rawValue: "cube")
// 基于运动的过渡方向方向:.fromRight、.fromLeft、.fromTop、.fromBottom
anim.subtype = CATransitionSubtype.fromRight
// 注意:过渡动画一次只能添加一次,并且其key自动设置为 kCATransition
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
// 1、生成一张屏幕截图
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, true, 0.0)
view.layer.render(in: UIGraphicsGetCurrentContext()!)
let coverImg = UIGraphicsGetImageFromCurrentImageContext()

// 2、把屏幕截图添加到view,作为原始图
let coverView = UIImageView(image: coverImg)
coverView.frame = view.bounds
view.addSubview(coverView)

// 3、修改要过渡到的效果
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)

// 4、对原始图作动画,并显示过渡后的效果
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()
}

CAMediaTiming

动画简单的定义就是指一个值随时间的变化。Core Animation 通过 CAMediaTimingCAAnimationCALayer 提供了基本的计时功能,和强大的时间轴功能。CAMediaTiming 协议中定义了一段动画内用于控制逝去时间的属性集合,如:动画的时间间隔、持续时间、速度、重复计数等属性。所以时间可以被任意基于一个图层或者一段动画的类控制。

对于 CAMediaTiming 协议 Apple文档的定义为:CAMediaTiming 为分层计时系统建模,允许对象在其父时间和本地时间之间映射时间。从父时间到本地时间的转换有两个阶段:

  1. 转换为“active local time”。这包括对象在父对象的时间轴上出现的时间点,以及它相对于父对象播放的速度。

  2. 从“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 {

// 指定 接收方 相对于其父对象的开始时间。默认值0
// 用于设置动画开始之前的的延迟时间
var beginTime: CFTimeInterval { get set }

// 指定动画的基本持续时间,以秒为单位,默认值为0
var duration: CFTimeInterval { get set }

/* 指如何将时间从父时间空间映射到接收者的时间空间,默认值 1.0
* 例如,如果速度为2.0,本地时间的进展速度是父时间的两倍。
*/
var speed: Float { get set }

/* 指active local time中的额外偏移量。默认值为 0.0
* 从 parent time(tp) 转换为 active local time(t): t = (tp - begin) * speed + offset
* 它的用途:设置 speed == 0,timeOffset == suitable value 来暂停一个动画
* tp是父layer的时间点,为了方便理解,可以认为是绝对时间,随时间流逝而增加。
* begin、speed、offset就是动画的属性beginTime、speed、timeOffset
*/
var timeOffset: CFTimeInterval { get set }

// 动画重复的迭代次数
var repeatCount: Float { get set }

// 动画重复的秒数,如果和repeatCount同时设置,则无效
var repeatDuration: CFTimeInterval { get set }

// 动画结束后,是否反向播放。默认值为false
var autoreverses: Bool { get set }

// 填充模式,定义动画在其有效持续时间之外如何显示
var fillMode: CAMediaTimingFillMode { get set }
}

层级关系时间

每个动画和图层在时间上都有它自己的层级概念,相对于它的父亲来测量。对图层调整时间将会影响到它本身和子图层的动画,但不会影响到父图层。

CoreAnimation有一个全局时间的概念,通过如下方法获取,其值是根据CPU的时钟周期数来描述的。我们无需知道该函数返回的值,它的值作用在于给动画的时间测量提供一个相对值。

1
2
3
// 返回当前 CoreAnimation 的绝对时间,它是通过调用mach_absolute_time()并将单位转换为秒的结果。
// 其实,开机后设备一共运行了(设备休眠不统计在内)多少秒
CACurrentMediaTime()

每个 CALayer 和 CAAnimaion 实例都有自己本地时间的概念,它是根据父图层/动画层级关系中的 beginTime、timeOffset 和 speed 属性计算来的。CALayer提供了如下方法来转换不同图层之间的本地时间:

1
2
3
4
5
6
7
8
9
10
/// 将指定层的时间间隔转换为接收方的时间间隔
/// - Parameters:
/// - t: 在l坐标系中指定位置的点
/// - l: 时间空间中有t的那一个l。接收者和l和必须共享一个公共的父层。
返回
/// - Returns: 转换为接收器的时间间隔
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

// 将 layer时间空间的 0.5秒 的时间间隔 转换为 offLayer 时间空间的 1.25
print(offLayer.convertTime(CFTimeInterval(0.5), from: layer)) // prints 1.25

// 将 layer 的时间空间里的 0.5秒时间间隔 转换为 offLayer 时间空间的时间间隔 1.25
print(layer.convertTime(CFTimeInterval(0.5), to: offLayer)) // prints 1.25

例子:动画的暂停、恢复、取消

这个例子是根据 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() {
// 获取redLayer的当前本地时间
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) {
// UIView的layer隐式动画默认被禁用,需要UIView的块动画启用
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

文章作者: Czm
文章链接: http://yoursite.com/2020/09/09/Core-Animation%E8%A7%A3%E6%9E%90/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Czm