
这是 Core Animation 的系列文章,介绍了 Core Animation 的用法,以及如何进行性能优化。
上一篇文章介绍了 Core Animation 的各种图层类,这一篇文章将介绍显式动画(explicit animation),显式动画允许为指定属性添加动画,或创建非线性动画(如沿指定曲线运动)。
1. CAAnimation
CAAnimation是 Core Animation 中所有动画的超类,是抽象类。
CAAnimation提供了对CAMediaTiming和CAAction协议的支持。不要创建CAAnimation实例管理 Core Animation 图层动画或 SceneKit 对象,一般使用CABasicAnimation、CAKeyframeAnimation、CASpringAnimation、CAAnimationGroup或CATransition动画。
isRemovedOnCompletion属性决定动画完成后是否自动将动画图层从层级结构中移除,默认为true。如果设置为false,需手动移除,否则会有内存泄漏。
timingFunction定义了动画的时间函数,默认为nil,即线性动画。
delegate属性指定委托对象,默认为nil。CAAnimationDelegate协议提供了动画开始、结束事件。
2. CAPropertyAnimation
继承自CAAnimation的抽象类,用于创建可操纵图层属性值的动画。
不要使用CAPropertyAnimation实例管理 Core Animaiton 的属性动画,应使用CAPropertyAnimation的子类CABasicAnimation或CAKeyframeAnimation,或CABasicAnimation的子类CASpringAnimation。
使用keyPath指定要设置动画的属性,keyPath是用点表示法指向层级关系中任意属性,而非仅仅是属性名称。例如,不仅可以对position添加动画,还可以对position.x添加动画。
3. CABasicAnimation
CABasicAnimation继承自CAPropertyAnimation,为图层单个属性提供动画。
使用继承的init(keyPath:)方法指定属性,以在 render tree 中执行动画。
3.1 常用属性
CABasicAnimation增加了fromValue、toValue和byValue,这三个属性定义了动画要插入的值,所有这些属性都是可选的,但不能同时使用三个。属性值类型应与keyPath类型匹配。因为属性动画可能是颜色渐变、位置移动、变换等,所以,fromValue、toValue和byValue类型都是Any。
有以下几种组合使用方式:
-
fromValue和toValue非空,动画在fromValue和toValue之间插入。 -
fromValue和byValue非空,动画在fromValue和fromValue + byValue之间插入。 -
byValue和toValue非空,动画在toValue - byValue和toValue之间插入。 -
fromValue非空,动画在fromValue和当前 presentation 值之间插入。 -
toValue非空,动画在 presentation 当前值和toValue值之间插入。 -
byValue非空,动画在 presentation 当前值和 presentation + byValue 之间插入。 - 所有值都为空,动画在 presentation layer 之前值和 presentation layer 当前值之间插入。
3.2 设置动画属性
CABasicAnimation可用动画形式改变标量属性,如opacity:
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
layerView.layer.add(animation, forKey: nil)
非标量属性也可以设置动画,如backgroundColor。Core Animation 会自动在fromValue和toValue之间插入中间值。下面代码使用动画将背景色从当前颜色更改为红色:
let animation = CABasicAnimation(keyPath: "backgroundColor")
animation.toValue = UIColor.red.cgColor
layerView.layer.add(animation, forKey: nil)
具有多个值的非标量属性(如bounds、position),为fromValue、byValue和toValue传入数组:
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = [0, 100]
animation.toValue = [100, view.bounds.size.height]
layerView.layer.add(animation, forKey: nil)
keyPath可以访问属性单独组件。下面代码拉伸了图层transform的y:
let animation = CABasicAnimation(keyPath: "transform.scale.y")
animation.duration = 2
animation.byValue = 0.5
animation.toValue = 3
layerView.layer.add(animation, forKey: nil)
可以看到动画插入值是从 y 值为 3-0.5(即2.5)开始,3结束。
3.3 更新 layer model
隐式动画是修改图层属性时自动产生的动画,修改视图的属性不会产生隐式动画。显式动画只是动画,不会修改图层 model,动画结束后默认自动从图层移除。因此,需要更新 layer model。
如上面的缩放 y 可以使用下面任一方法更新 layer model:
// 1. 使用CATransform3D更新 layer model
var transform = CATransform3DIdentity
transform = CATransform3DScale(transform, 1, 3, 1)
layerView.layer.transform = transform
// 2. 使用 CGAffineTransform 更新 layer model
layerView.transform = CGAffineTransform(scaleX: 1, y: 3)
虽然,通过设置isRemovedOnCompletion为false也可以达到同样效果,但应避免这样做。一方面,将动画保留到屏幕中会影响性能;另一方面,presentation layer 与 model layer 一致可以降低复杂度,有利于后期维护。
你可以自行更新其他动画 model,如果有问题可以在文章底部下载源码查看。
4. CASpringAnimation
CASpringAnimation可以将弹簧类似弹性效果添加到图层属性。CASpringAnimation继承自CABasicAnimaiton。
可以把CASpringAnimation设想为摆钟,在理想状态下(即没有阻力),会持续同样振幅的摆动:

震动曲线如下:

在真实世界中,由于阻力的作用,其摆动效果如下:

震动曲线如下:

4.1 常用属性
CASpringAnimation的以下属性可以控制弹性效果:
-
damping:减震。damping属性定义抑制弹簧运动的摩擦力大小。默认值为10。减小damping值摩擦力变小,弹簧晃动次数增加,settlingDuration可能大于duration。增大damping值阻力变大,弹簧晃动次数减少,settlingDuration比duration小。 -
initialVelocity:初始速度,默认为0,表示静止的对象。负值表示与目标位置相反方向的初始速度,正值表示与目标位置相同方向的初始速度。 -
mass:附着在弹簧末端物体的重量,默认值为1。增加该值会增大弹性效果,即震动次数更多、幅度更大,settlingDuration也会增加。减小mass会减弱弹性效果。 -
settlingDuration:弹性动画完全静止所需预期时间,可能和duration不一致。 -
stiffness:弹簧钢性系数,默认值为100。增大stiffness会减少震动次数,减小settlingDuration时间。减少stiffness会增加震动次数,增加settlingDuration时间。
4.2 设置弹簧动画
下面使用CASpringAnimation创建一个弹簧动画,点击按钮时上下摆动文本框,并使用红色描边。如下所示:
private func testSpringAnimation() {
let jump = CASpringAnimation(keyPath: "position.y")
jump.fromValue = textField.layer.position.y + 1.0
jump.toValue = textField.layer.position.y
jump.duration = 0.25
textField.layer.add(jump, forKey: nil)
}
目前,动画只会将文本框向下移动1point,然后回到初始位置。
为CASpringAnimation添加以下属性:
jump.initialVelocity = 100.0
jump.mass = 10.0
jump.stiffness = 1500.0
jump.damping = 50.0
修改弹簧变量可能并不容易,你可以反复修改这些值,观察其中区别以实现最佳效果。
多次运行demo,会发现动画运行到一定位置后直接跳到了终点。这是由于duration设置为了0.25秒,但弹簧动画在0.25秒内并不能完成。下图显示了弹簧动画如何被切断:

想要修复这一问题,只需设置duration为settlingDuration即可。
jump.duration = jump.settlingDuration
此外,还可以弹性设置描边颜色。如下所示:
textField.layer.borderWidth = 2.0
textField.layer.borderColor = UIColor.clear.cgColor
let flash = CASpringAnimation(keyPath: "borderColor")
flash.damping = 7.0
flash.stiffness = 200.0
flash.fromValue = UIColor(red: 1.0, green: 0.27, blue: 0.0, alpha: 1.0).cgColor
flash.duration = flash.settlingDuration
textField.layer.add(flash, forKey: nil)
textField.layer.cornerRadius = 5
效果如下:

5. CAKeyframeAnimation
CABasicAnimation渐进式修改图层指定属性,在指定时间内从fromValue修改到toValue。例如,将图层从45度旋转到-45度,只需指定开始、结束值,layer 自动渲染中间状态以完成动画。

CAKeyframeAnimation使用数组values取代fromValue、byValue、toValue,values数组元素是动画需经过的点。此外,还需提供经过上述点的keyTimes。CAKeyframeAnimation继承自CAPropertyAnimation,为图层对象提供关键帧(keyframe)动画。
查看以下CAKeyframeAnimation动画:

上图中,动画从45度旋转至-45度,但分为两个阶段。第一阶段,前三分之二时间从45度旋转到22度,后三分之一时间从22度旋转至-45度。
使用 keyframe 动画时,需提供属性的 key values,同时提供与之匹配数量的 key times,时间为相对比例,范围为0.0至1.0。
5.1 常用属性
CAKeyframeAnimation有以下常用属性:
values:数组类型,指定用于动画的keyframe。keyframe指定了动画必须经过的位置,图层何时经过指定keyframe由动画时间函数决定,即calculationMode、keyTimes、timingFunctions属性等。keyframe之间的值自动插入,除非calculationMode被设置为了discrete。只有当path属性为nil时才会采用values属性的值。path:CGPath类型,动画属性类型为CGPoint时,可以使用path指定点动画的路径。使用了path属性后,动画会忽略values属性。path可以包含move-to、line-to、curve-to等片段。-
keyTimes:数组类型,可选实现。指定动画进行到指定keyframe的时间。数组元素为浮点值,范围为0.0至1.0,即指定动画进行到总持续时间百分比。后一个时间必须大于等于前一个时间。通常,keyTimes元素数量与values、path元素数量应一致。否则,动画时间可能不符合预期。keyTimes数组元素值与calculationMode相关:- 如果
calculationMode设置为linear或cubic,则数组第一个元素需是0.0,最后一个元素需是1.0。中间部分值表示开始时间和结束时间之间的时间点。 - 如果
calculationMode设置为discrete,数组第一个元素必须是0.0,最后一个元素必须是1.0。keyTimes数组元素必须比values数组元素数量多一个。 - 如果
calculationMode设置为cubicPaced、paced,则自动忽略values数组。
如果
keyTimes数组对当前calculationMode无效或不合适,则会被自动忽略。 - 如果
-
timingFunctions:数组类型,可选设置,数组元素为CAMediaTimingFunction类型。通过timingFunctions数组可以设置两个keyframe之间动画为淡入、淡出、自定义。如果values数组有n个元素,则该数组应包含n-1个元素。如果已经为
keyTimes赋值,timingFunctions属性会对时间函数进一步优化。如果未设置keyTimes属性,则使用timingFunctions属性替换 Core Animation 默认时间函数。 calculationMode:指定CAKeyframeAnimation如何计算 keyframe 中间值。默认值为linear,即线性插入。此外,还有cubic、cubicPaced、discrete、linear、paced。rotationMode:对象沿指定path运动过程中,绕切线旋转模式。默认为nil,即无需旋转。
5.2 对非 struct 属性进行动画
以下代码使用CAKeyframeAnimation晃动UILabel:
let wobble = CAKeyframeAnimation(keyPath: "transform.rotation")
wobble.duration = 0.25
wobble.repeatCount = 2
wobble.values = [0.0, -.pi/4.0, 0.0, .pi/4.0, 0.0]
wobble.keyTimes = [0.0, 0.25, 0.5, 0.75, 1.0]
titleLabel.layer.add(wobble, forKey: nil)
创建CAKeyframeAnimation与创建CABasicAnimation方式一致,指定keyPath、duration、repeatCount即可。
旋转角度从0度到-45度,回归到0度,旋转到45度,回到0度。动画开始、结束位置相同,方便重复动画。确保keyTimes开始、结束分别是0.0、1.0。
效果如下:

5.3 对 struct 属性进行动画
结构体(struct)在 swift 中是一等公民,与 class 的使用几乎没有区别。
但 Core Animation 是一个基于 C 的 Objective-C framework。这意味着,结构体的处理会有些不同。Objective-C API 喜欢处理对象,因此,struct 属性动画需要一些特殊处理。
CALayer的很多属性是结构体,如position、transform、bounds等。为了解决这个问题,Cocoa 提供了NSValue类,用于将结构体包装为对象。NSValue提供了以下方法包装结构体:
init(cgPoint: CGPoint)
init(cgSize: CGSize)
init(cgRect rect: CGRect)
init(caTransform3D: CATransform3D)
如果直接为fromValue、toValue赋值结构体,将无法得到预期的动画。
首先,添加CALayer到视图:
let balloon = CALayer()
balloon.frame = CGRect(x: -50, y: 100, width: 50, height: 50)
balloon.contents = UIImage(named: "balloon")!.cgImage
view.layer.addSublayer(balloon)
将balloon图层放到了左上角的可见区域外。使用以下代码添加 keyframe 动画:
let flight = CAKeyframeAnimation(keyPath: "position")
flight.duration = 3.0
flight.values = [
CGPoint(x: -50.0, y: 0.0),
CGPoint(x: view.bounds.width + 50, y: view.bounds.height / 2.0),
CGPoint(x: -50.0, y: view.bounds.height - 100)
].map({ NSValue(cgPoint: $0) })
flight.keyTimes = [0.0, 0.5, 1.0]
balloon.add(flight, forKey: nil)
balloon.position = CGPoint(x: -50.0, y: view.bounds.height - 100)
动画经过了三个指定点。运行后效果如下:

还可使用上述方法对bounds、position、transform等结构体添加动画。
6. CATransition
CATransition对象提供 layer 状态切换的动画。CATransition继承自CAAnimation。
默认 transition 是交叉淡入淡出(cross fade)。通过创建、添加CATransition对象,可以选取不同 transition 效果。
6.1 常用属性
CATransition有以下常用属性:
startProgress:指定动画起点处于整个 transition 的百分比。Float 类型,值范围是0.0至1.0,默认为0。endProgress:指定动画终点处于整个 transition 的百分比。Float 类型,值范围是0.0至1.0,默认为0。endProgress值必须大于等于startProgress,并不大于1.0。如果endProgress小于startProgress,则结果不可预期。默认值为1.0。-
type:预定义的 transition type,为CATransitionType类型。如果设置了filter属性,则会忽略type属性。CATransitionType有以下类型:-
fade:淡入淡出,默认属性。 -
moveIn:图层内容在现有内容之上滑入。moveIn与subType一起使用。 -
push:图层内容推动着现有内容进入。push与subType一起使用。 -
reveal:图层内容根据subType指定方向逐渐显示。
-
-
subType:指定 transition 方向。CATransitionSubtype类型,默认为nil。如果设置了filter属性,则会忽略subType属性。CATransitionSubtype有以下类型:-
fromBottom:transition 从图层底部开始。 -
fromLeft:transition 从图层左侧开始。 -
fromRight:transition 从图层右侧开始。 -
fromTop:transition 从图层顶部开始。
-
filter:可选 Core Image Filter 对象提供 transition。只可用于macOS和 Mac Catalyst 13.0,不可用于 iOS。
6.2 对 CATextLayer 添加 transition
下面代码演示了如何过渡CATextLayer。过渡前,backgroundColor为红色,string为Red。过渡时,创建了一个新的CATransition并添加到图层,图层背景色过渡为蓝色,文本内容过渡为Blue。代码如下:
let transition = CATransition()
transition.duration = 2
transition.type = .push
transitioningLayer.add(transition, forKey: nil)
// Transition to "blue" state
transitioningLayer.backgroundColor = UIColor.blue.cgColor
transitioningLayer.string = "Blue"
效果如下:

6.3 对 NavigationController 添加 transition
这一部分使用CATransition为控制器之间导航添加过渡动画。
首先为UINavigationController添加以下 extension:
extension UINavigationController {
func pushTransition() {
let transition = CATransition()
transition.duration = 1.0
transition.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
transition.type = .push
transition.subtype = .fromTop
view.layer.add(transition, forKey: nil)
}
func popTransition() {
let transition = CATransition()
transition.duration = 1.0
transition.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
transition.type = .push
transition.subtype = .fromBottom
view.layer.add(transition, forKey: nil)
}
}
可以尝试不同
type、subType,查看不同效果。
当执行push、pop时,先调用pushTransition()和popTransition()函数。如下所示:
@objc private func handlePushButtonTapped() {
navigationController?.pushTransition()
let explicitlyVC = ExplicitlyViewController()
navigationController?.pushViewController(explicitlyVC, animated: false)
}
@objc private func handlePopButtonTapped() {
navigationController?.popTransition()
navigationController?.popViewController(animated: false)
}
效果如下:

如果是 present 视图控制器,可以使用
modalTransitionStyle修改呈现方式。
7. CAAnimationGroup
CAAnimationGroup允许多个动画在一个组中同时运行。CAAnimationGroup继承自CAAnimation。
添加到 animation group 的动画,它的时间不会缩放到CAAnimationGroup的duration,而是将超出CAAnimationGroup的duration部分直接裁剪掉。例如,CAAnimationGroup动画duration时间为5秒,添加到该 group 动画duration为10秒,则仅显示前5秒钟动画。
下面代码将改变不透明度、缩放、旋转三个动画添加到CAAnimationGroup:
let fadeOut = CABasicAnimation(keyPath: "opacity")
fadeOut.fromValue = 0
fadeOut.toValue = 1
let expandScale = CABasicAnimation(keyPath: "transform")
expandScale.valueFunction = CAValueFunction(name: .scale)
expandScale.fromValue = [1, 1, 1]
expandScale.toValue = [1.5, 1.5, 1.5]
let rotate = CABasicAnimation(keyPath: "transform")
rotate.valueFunction = CAValueFunction(name: .rotateZ)
rotate.fromValue = Float.pi / 4.0
rotate.toValue = 0.0
let group = CAAnimationGroup()
group.animations = [fadeOut, expandScale, rotate]
group.duration = 0.5
group.beginTime = CACurrentMediaTime() + 0.5
group.fillMode = .backwards
group.delegate = self
layerView.layer.add(group, forKey: nil)
添加到
CAAnimationGroup的动画将忽略delegate和isCompletedOnCompletion属性,CAAnimationGroup会接收这些信息。
效果如下:

8. CAAnimationDelegate

CAAnimation遵守了CAAnimationDelegate协议。CAAnimationDelegate协议包含了下面两个可选实现的方法:
-
optional func animationDidStart(_ anim: CAAnimation)动画开始时调用。 -
optional func animationDidStop(_ anim: CAAnimation, finished flag: Bool)动画结束时调用。动画结束可能是因为到达duration,也可能是因为添加动画的图层被移除。如果是达到指定duration,flag为true;反之,flag为false。
为CAAnimationGroup添加delegate:
group.delegate = self
添加以下 delegate 方法:
extension ExplicitlyViewController: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
print(#function)
}
}
动画结束后会输出以下内容:
animationDidStop(_:finished:)
实际应用时可能有多个动画同时遵守CAAnimationDelegate协议,但如何区分是哪个动画结束调用的CAAnimationDelegate方法呢?
8.1 键值编码 Key-value coding compliance
CAAnimation类和子类都是使用 Objective-C 编写的,都遵守KVC。也就是可以像字典一样在运行时添加属性。
我们将使用KVO机制为动画添加属性,以便在需要时可以对动画进行区分。
let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width/2
flyRight.toValue = view.bounds.size.width/2
flyRight.duration = 2
flyRight.delegate = self
flyRight.setValue("form", forKey: "name")
flyRight.setValue(titleLabel.layer, forKey: "layer")
titleLabel.layer.add(flyRight, forKey: "title")
flyRight.beginTime = CACurrentMediaTime() + 0.3
flyRight.fillMode = .both
flyRight.setValue(textField.layer, forKey: "layer")
textField.layer.add(flyRight, forKey: "field")
titleLabel.layer.position.x = view.bounds.size.width / 2
textField.layer.position.x = view.bounds.size.width / 2
在上述代码中,添加了键form,值为name。这样可以在CAAnimationDelegate回掉方法中根据form区分动画。
Core Animation 动画对象只是数据模型,创建后只需修改所需属性即可。
CABasicAnimation实例描述了动画,可以现在执行、稍后执行,也可以不执行。动画不关联特定 layer,可以复用 animation 到其他 layer,每个 layer 都将获得单独的 animation,
8.2 animationDidStop(_:finished:)
目前已经为动画设置了 key,在animationDidStop(_:finished:)方法中添加以下代码:
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
print(#function)
if !flag {
print("Did not reached the end of the duration")
return
}
guard let name = anim.value(forKey: "name") as? String else { return }
if name == "form" { // form field found
let layer = anim.value(forKey: "layer") as? CALayer
anim.setValue(nil, forKey: "layer")
let pulse = CABasicAnimation(keyPath: "transform.scale")
pulse.fromValue = 1.5
pulse.toValue = 1.0
pulse.duration = 0.25
layer?.add(pulse, forKey: nil)
}
}
如果动画不是因为到达duration结束,则不再添加动画。使用value(forKey:)从动画中获取值,并转为String类型。
value(forKey:)返回值类型是AnyObject?,需转换为所需类型,且转换可能失败。
最终,当动画结束后添加了一个放大动画。
8.3 Animation Keys
add(_:forKey:)函数有以下两个参数:
- anim:第一个参数是要添加到 render tree 的动画。render tree 对该参数进行复制、而非引用。因此,随后对动画的修改不会改变 render tree 中的动画。
- key:标记该动画的 key。每个单独 key 只添加一个动画到图层,对于 transition animation 会自动使用
kCATransition特殊key。该参数可以为nil。使用 key 可以在动画开始后获取、管理动画。
如果动画duration为0或负值,则duration会被设置为当前的kCATransactionAnimationDuration(如果设置了该值),或采用动画默认时长,即0.25秒。
在上面的键值编码部分,为titleLabel设置的key是title,为textField设置的key是field。这里使用以下代码移除运行中的动画:
titleLabel.layer.removeAnimation(forKey: "title")
textField.layer.removeAnimation(forKey: "field")
当titleLabel、textField向右移动过程中,点击chang按钮时调用上述方法,会立即移除动画。效果如下:

Demo名称:CoreAnimation
源码地址:https://github.com/pro648/BasicDemos-iOS/tree/master/CoreAnimation
上一篇:CALayer及其各种子类
参考资料: