隐式动画
一般我们在做动画的时,会使用到 CAAnimation 的相关类,通过 CALayer 的 addAnimation:forKey: 方法,添加动画效果,这种动画称为显式动画。还有一种动画被称为隐式的动画,在没有主动添加动画代码时,会自动的产生动画效果。
我们知道 UIView 的背后,有 CALayer 作为内容的显示,CALayer 类似于 UIView 也具有树型结构,可以单独的创建。单独的创建 CALayer 实例,并修改它的某些属性,会神奇的出现动画效果。
override func viewDidLoad() {
super.viewDidLoad()
animationLayer = CALayer()
animationLayer?.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
animationLayer?.backgroundColor = UIColor.red.cgColor
view.layer.addSublayer(animationLayer!)
}
@IBAction func move() {
let centerX: CGFloat = CGFloat(Int(arc4random()) % 375)
let centerY: CGFloat = CGFloat(Int(arc4random()) % 667)
let center = CGPoint(x: centerX, y: centerY)
animationLayer?.position = center
}
上述代码使用 CALayer 创建一个红色的正方形,并随机的修改它的位置。效果是:
Moving.gif修改 CALayer 的 position,它没有立即出现在目标位置,而会有过渡的动画效果,该过程为 0.25 秒,这就是所谓的隐式动画。
隐式动画的背后,存在事务的概念,任何一个 animatable 属性的修改,都会默认创建一个 CATransaction 的事务,来配置动画的参数。重写 CATransaction 可以覆盖默认的动画效果。
将动画时间修改为五秒:
CATransaction.begin()
CATransaction.setAnimationDuration(5)
animationLayer?.position = center
CATransaction.commit()
替换成上述的代码段,移动的效果变慢了。另外,还可以用 CATransaction.setDisableActions(true)
方法直接禁用隐式动画。需要注意的是,修改隐式动画效果的代码,都必须要在 begin()
和 commit()
之间,并且成对出现。
CATransaction 没有实例方法,它像个神秘的配置工具类。当嵌套的使用 CATransaction 时,它以栈式结构管理。执行 begin
的时,会将后面的配置信息入栈;当的 commit
时,出栈配置信息,处于它们之间的属性修改是原子的,等下一次 RunLoop 到来时,开始执行动画。
举一个嵌套使用的例子:
CATransaction.begin()
CATransaction.setAnimationDuration(1)
let centerX: CGFloat = CGFloat(Int(arc4random()) % 375)
let centerY: CGFloat = CGFloat(Int(arc4random()) % 667)
let center = CGPoint(x: centerX, y: centerY)
animationLayer?.position = center
CATransaction.begin()
CATransaction.setAnimationDuration(5)
let random: CGFloat = CGFloat(Int(arc4random()) % 4)
let transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_4) * random)
animationLayer?.setAffineTransform(transform)
CATransaction.commit()
CATransaction.commit()
外层是移动的效果,内层是旋转效果,内层的效果会先被提交执行。
仔细观察动画效果,肉眼发现旋转和移动几乎是同时开始的,它们分明是按顺序提交的两段代码,为什么会同时执行?
这时候就需要了解下 CALayer 动画的形成原理,在 CALayer 的头文件里面有两个方法,modelLayer()
和 presentationLayer()
它们分别叫模型树和呈现树,文档已有详细说明。
当我们开始动画的时候,我们必须要先确定动画最终效果是什么,在给某个动画属性赋新值的时候,它是瞬间被修改的,我们从模型树中读取,它的值已经是新的值,而在动画过程中的值由呈现树来保存。但实际上 CA 内部还有私有的渲染树 CARender,渲染当前的动画效果,并且是异步的,所以上面两次提交的动画,是分别在不同的线程中执行的,并且不会阻塞主线程。
UIView 是如何禁用隐式动画的
UIView 的内容是由 CALayer 呈现的,但在修改 UIView 属性的时候,却没有动画效果,说明 UIView 在包装 CALayer 的时,对它做了手脚。
CALayer 属性的修改的动画被叫做 action,只要实现 CAAction 协议的类,都可以被作用于 CALayer 的动画。当 CALayer 的属性被修改时,首先会调用 -actionForKey:
方法,传递修改的属性,接着会通过4种方式来获取动画的 action:
- CALayer 是否存在代理,并实现
-actionForlayer:forKey
方法。 - 如果代理不存在或者未实现上述代理方法,会检查 actions 字典,是否包含所修改属性的动作。
- 如果
actions
字典,不包含当前修改的属性,会在继承体系中查找 style 字典属性。 - 最后如果在
style
属性里也没法发现,就会使用默认的-defaultActionForKey:
,也就是形成隐式动画的动作。
然而,UIView 禁用隐式动画的方式非常简单,它遵循了 CALayerDelegate 并给 方法返回 nil
,就不会有动画效果了。
等等,那如果直接返回 nil,那 UIView 不就做不了动画了吗?animateWithDuration 这类 block 动画如何实现呢?比如修改颜色的动画效果:
UIView.animate(withDuration: 1, delay: 10, options: .curveEaseIn, animations: {
self.view.backgroundColor = UIColor.black
}, completion: nil)
虽然传递 delay 10 秒,但 block 是瞬间执行的,只不过动画的效果会在 10 秒之后才发生。上述 block 等价于
UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDelay(10)
UIView.setAnimationDuration(1)
self.view.backgroundColor = UIColor.black
UIView.commitAnimations()
处于 beginAnimations
和 commitAnimations
之间的属性修改会被做标记 ,在 -actionForlayer:forKey
方法中,会为有标记的操作返回特定的 CAAction 而不是 nil
,UIView 就可以自如的控制是否需要动画效果了。
本文绝大部分内容 CALayer 头文件就有说明,但是如果仅仅看头文件的注释,会不知所云,稍微了解一些用法,会清楚不少。