自己做的笔记,过段时间却又忘记了,为了尽量避免这种情况,同时治服拖延种种,打算开始多写写技术向的博客。
UIKit Animation or Core Animation?
在 iOS 中,动画可以分为两个类别:UIKit,和底层的 Core Animation。
通过最基本的 UIView.animate(withDuration:)
方法建立如下的 UIKit animation,能胜任复杂程度不高的动画:
UIView.animate(withDuration: 0.8) {
self.orangeBlock.frame = CGRect(x: 38, y: 70, width: 300, height: 60)
}![image.png](https://upload-images.jianshu.io/upload_images/8048781-4b3a1db18a8c7915.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
但是当要处理不单一的属性时,往往会出现一些不合预期的效果,这是因为,用 UIKit 接口创建的动画是基于视图(view)的,它并不能直接对图层(layer)属性作出更改。
为了制作出能符合预期的动画效果,我们就需要直接对图层进行控制,这就是 Core Animation 了。
基于 Core Animation,我们可以创建两步的 CABasicAnimation
,或者用 CAKeyframAnimation
绘制路径等。
Layer v.s. View
图层不是视图的替代,而是后者的底层支持。在能满足需求的情况下,对视图而不是图层进行操作——即使用 UIKit 动画——是被建议的做法。
在 iOS 开发中,通常是每一个视图有一个对应的图层(称为 layer-backed view),图层对象会保证两者保持同步。然而在一些特殊情况下,这种一对一的关系不是必须的(比如多个图层支持一个单独 UIImageView 来节省因单张图片重复出现的内存占用)。
基本过渡动画 - CABasicAnimation
想要对哪一组值进行设置,知道对应的 keyPath 就好了。
let radiusAnimation = CABasicAnimation(keyPath: "cornerRadius")
radiusAnimation.fromValue = 50
radiusAnimation.toValue = 30
radiusAnimation.duration = 3
purpleBlock.layer.add(radiusAnimation, forKey: "cornerRadius")
// 为模型图层设置新的半径
purpleBlock.layer.cornerRadius = 30
需要注意的一点是,add(_: CAAnimation, forKey:)
设置的动画针对的是模型图层(model layer)而不是显示图层(presentation layer),当动画显示完,它就被移除了,显示图层属性将会恢复成初始的模型图层属性。所以,为了保持对象视图变化后的状态,要记得手动更新。
Core Animation 控制的图层中包含有两套平行的继承树:模型图层树(model layer tree)和显示图层树(presentation layer tree)。前者反映的是图层状态,后者是图层在动画中动态的值。如果要实时跟踪图层的属性变化,要检测的是显示图层的值。
关键帧动画 - CAKeyframeAnimation
现在,试想我们要实现的动画超过了两步(而这是非常常见的情况),那么以 fromValue
和 toValue
简单两组值就显然过于局限了。这时候我们用 CAKeyframeAnimation
就能任意地定义和设置帧。
let shakeAnimation = CAKeyframeAnimation(keyPath: "position.x")
shakeAnimation.values = [0, 10, -10, 10, 0]
shakeAnimation.keyTimes = [0, 0.2, 0.6, 0.8, 1]
shakeAnimation.duration = 0.4
shakeAnimation.isAdditive = true
blueBlock.layer.add(shakeAnimation, forKey: "position.x")
values
数组代表的是对象的位置,keyTimes
数组代表划分的时间段。
除了使用状态数组的方式,设定路径(CGPath
)也是可以的:
let boundingRect = CGRect(x: -150, y: -150, width: 300, height: 300)
let orbitAnimation = CAKeyframeAnimation(keyPath: "position")
orbitAnimation.path = CGPath(ellipseIn: boundingRect, transform: nil)
orbitAnimation.duration = 4
orbitAnimation.isAdditive = true
orbitAnimation.repeatCount = Float.infinity
orbitAnimation.calculationMode = kCAAnimationPaced
orbitAnimation.rotationMode = kCAAnimationRotateAuto
greyBlock.layer.add(orbitAnimation, forKey: "position")
CAAnimation 的可重用性
一个动画实例由 keyPath 指定其所定义的图层属性类型,因此对于该属性(如 cornerRadius)不必再重复创建新实例,也即是能被多个对象所共用的。
为了提高其可重用性,我们还可以用 byValue
来替代 CABasicAnimation
中的 toValue
,前者设定的是变化量。
let radiusAnimation = CABasicAnimation(keyPath: "cornerRadius")
radiusAnimation.fromValue = 50
// radiusAnimation.toValue = 30
// equals:
radiusAnimation.byValue = -20
如果同时创建了多个动画,可以使用 CAAnimationGroup
打包成一个动画组:
let aniGroup = CAAnimationGroup()
aniGroup.animations = [shakeAnimation, radiusAnimation]
aniGroup.duration = 3
magentaBlock.layer.add(aniGroup, forKey: "whatsoever")
Timing Functions 使动画流畅
为了让动画看起来更自然,添加一个 CAMediaTimingFunction
对象实例。可以直接对动画实例设置:
// 现有的「淡入淡出」效果
let timingFunc1 = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
ANI_INSTANCE.timingFunction = timingFunc
也可以为 CATransaction
设置:
// 自定义函数,通过 controlPoints 设置贝塞尔曲线
let timingFunc2 = CAMediaTimingFunction(controlPoints: 0.65, -0.55, 0.27, 1.55)
CATransaction.setAnimationTimingFunction(timingFunc)
CATransaction 事务机制
当我们要进行多组图层操作时,一个很好的习惯是把它们显式(explicitly)合并到一个事务中。
CATransaction Definition: A mechanism for grouping multiple layer-tree operations into atomic updates to the render tree.
事实上,即使我们不主动创建 CATransaction 事务,系统也会默认给我们创建一个隐式(implicit)事务。显式声明还有个好处是,可以为事务内部的代码统一设置一些默认值(如 duration
)。嵌套的 CATransaction 是允许的。
CATransaction.begin()
CATransaction.setAnimationDuration(3)
let positionAnimation = CABasicAnimation(keyPath: "position")
positionAnimation.fromValue = [38, 484]
positionAnimation.toValue = [112, 484]
greenBlockLayer.add(positionAnimation, forKey: "position")
greenBlockLayer.position = CGPoint(x: 112, y: 484)
CATransaction.begin()
CATransaction.setAnimationDuration(1)
UIView.animate(withDuration: b_Interval) {
self.greenBlock.alpha = 0.5
}
CATransaction.commit()
CATransaction.commit()
ref:
- Design Patterns on iOS using Swift - Part 1/2 - raywenderlich
- Design Patterns on iOS using Swift – Part 2/2 - raywenderlich
- 设计模式 - 菜鸟教程
- KVO - Swifter
- Key-Value Observing Programming Guide - Apple
- Is key-value observation (KVO) available in Swift? - Stack Overflow
- Exploring KVO alternatives with Swift - Scott Logic