介绍core animation中的显式动画和隐式动画,参考官方文档iOS Core Animation Advanced Techniques
隐式动画
事务
事务是Core Animation中用来执行属性动画的机制。在事务期间,我们修改CALayer的可以动画的属性时,该属性不会立即更改,而是在当前的事务提交之后,统一进行更改。默认情况下,Core Animation会在每个run loop的周期中开始一次新的事务,并在结束时提交事务。这意味着,我们对于CALayer的修改都会通过事务进行一次0.25秒的动画。
我们用事务提交的动画并不需要对动画方式进行设定,动画会根据属性的初始值和终止值以及动画时间,自动完成平滑的效果过渡,故我们称这种动画为隐式动画。系统维护一个我们无法访问的事务栈,我们可以通过+begin和+commit来压入或弹出事务。我们也可以更改动画的运行时间以及完成后的操作。这些设定会对栈顶的事务生效。
我们看下面的例子,我们假设使用到的layer已经初始化,已经添加到某个view的layer中:
1 | //添加一个新的事务 |
代码运行的效果为,layer先慢慢变为蓝色,然后立刻旋转90度。
视图图层
当我们试图在UIView关联的layer上进行隐式动画时,会发现动画全部失效了。其原因是UIView禁止了隐式动画(应该是出于显示动画的需求)。CALayer执行隐式动画时分为如下几步:
- 图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。如果有,直接调用并返回结果。
- 如果没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
- 如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
- 如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。
UIView通过第一个步骤,将所有的action设置为nil,从而禁止了layer的动画。事实上,在UIView的+beginAnimations和+commitAnimations中,UIView会将返回CABasicAnimation的动画。此外,我们也可以通过-setDisableActions来禁止隐式动画。
我们也可以通过第二步来实现对于动画行为的定制,如下面的代码实现了背景颜色的左侧滑入切换动画:
1 | CATransition *transition = [CATransition animation]; |
隐式动画原理
隐式动画中,改变一个图层属性后,该属性值其实立刻生效了,但在屏幕上并没有体现,而是根据动画时间渐变更新。CALayer的动画遵循了典型的MVC模式,C层为Core Animation的CATransaction,V层为CALayer的presentationLayer,M层为CALayer的modelLayer。
动画的过程中,modelLayer的属性立刻发生了变化,但presentationLayer是我们在屏幕上实际看到的layer,它的属性会发生渐变。我们获取到的layer属性实际是其modelLayer,故如果我们需要在动画的过程中捕捉如对layer的点击事件(例如使用hitTest),则应该使用presentationLayer。
显式动画
显式动画中,我们可以针对属性的变换做出更为复杂的自定义动画,甚至是非线性变化的动画效果。CAAnimation是所有动画的抽象基类,它提供了一系列诸如计时,动画状态委托,CAAction协议等功能。
属性动画
属性动画指CAPropertyAnimation及其子类动画,其基本特征顾名思义就是作用于图层的特定属性,可以分为CABasicAnimation和CAKeyframeAnimation
CABasicAnimation
CABasicAnimation的实质通过指定某一属性的初始值和终止值来实现一个动画效果。(属性不一定是是直属于图层的,可以是有嵌套层次的)。
CABasicAnimation有三个重要属性:
- id fromValue (动画开始之前属性的值)
- id toValue (动画结束之后的值)
- id byValue (动画执行过程中改变的值)
不能同时指定三个属性值,因为任何一个都可以根据另外两个计算出来。fromValue是可以默认的,默认是该layer开始动画时的属性。注意,属性值都是id,这意味着我们需要利用NSValue、NSNumber以及bridge对原有的值进行转换。
我们先看一个简单的示例:
1 | //假设该view已经初始化,并加入到视图层级中 |
我们可以看到该view从蓝色用2秒时间变到红色,然后瞬间变回蓝色。对比在隐式动画中提到的UIView的+beginAnimations开启动画的方法,我们可以发现,我们仅仅执行了向图层添加动画的操作,却并没有真正修改图层的属性值。换言之,我们仅定义了layer的presentationLayer的行为,却根本没有修改modelLayer,这导致了在动画结束后,view又变回了原来的样子。注意,这是显示动画,故view不会屏蔽掉其关联layer的动画。
如何解决这个问题呢?网上给出的一种很常见的做法如下:
1 | animation.removedOnCompletion = NO; |
通过设置removedOnCompletion为NO来使得动画不消失,通过设置fillMode使得动画停留在最后一帧上。但如果使用这种方法,我们必须要留意手动将动画移除,否则动画将持续占用内存直到layer销毁。在动画存在的时候,我们对于该属性的修改都不会产生任何效果。移除动画后,视图会恢复到原状态。所以该方法其实并不适合解决该问题。
另一种方法是在动画开始之前修改layer的属性:
1 | //假设该view已经初始化,并加入到视图层级中 |
我们对之前的例子做出了两个修改。
其一是设置animation的fromValue。这是由于在后面我们需要修改layer的属性,如果不设定fromValue的话,在动画执行阶段会自动选择新的属性作为fromValue。这里可以直接使用view的layer,因为大多数时候layer的属性与presentationLayer的属性是一致的。但这里为了谨慎起见,我们使用presentationLayer的属性,因为这才是在屏幕上显示的状态。
其二是直接设置layer的属性为新值。该操作是修改layer的modelLayer的属性,确保当动画结束后,layer的属性已经正确处于新的状态。按照官方文档的建议,我们需要在这里关闭隐式动画。但实际过程中,显式动画会覆盖隐式动画,所以我注释掉了这部分操作。
animation拥有CAAnimationDelegate,可以监视动画结束的信息。我们似乎可以利用-animationDidStop来在动画结束后更新layer的属性。然而在真机实测时,我们会发现在回调产生之前,layer会瞬间回到原来的状态。所以该方法是无法解决上面的问题的。
CAKeyframeAnimation
如果只有CABasicAnimation,那显示动画相较于隐式动画并没有多大优势。CAKeyframeAnimation的动画要求我们提供关键帧的属性,由core animation来为关键帧之间提供插值。
1 | CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; |
需要注意关键帧的第一帧应当和layer当前的状态一致,这样才不会发生跳帧现象。最后一帧之后会恢复到原来的状态,解决办法跟CABasicAnimation提到的相同。
当我们需要对layer的position进行动画时,CAKeyframeAnimation提供了一种更为直观的方式path来进行更好的定制:
1 | //路径设置 |
CAAnimationGroup
CAAnimationGroup是为了将多个动画组合在一起播放而设计的类。我们通过设置该类的animations数组来将animation组合在一起。group中的animation使用的都是CAAnimationGroup的duration:
1 | CAAnimationGroup *groupAnimation = [CAAnimationGroup animation]; |
CATransition
当我们需要改变非动画属性(图片)或从层级关系中添加和删除图层时,之前的属性动画将不会生效。所以我们使用CATransition来实现图层外观的过渡变化。
CATransition通过type和subtype来标记变换效果。type属性是一个NSString类型,可以被设置成如下类型:
- kCATransitionFade
- kCATransitionMoveIn
- kCATransitionPush
- kCATransitionReveal
kCATransitionFade创建一个平滑的淡入淡出效果。kCATransitionMoveIn中新图层从外部滑入,覆盖旧图层。kCATransitionPush中新图层从外部滑入,旧图层滑出。kCATransitionReveal中旧图层划出,显露出新图层。
subtype提供移动方向,默认kCATransitionFromLeft
- kCATransitionFromRight
- kCATransitionFromLeft
- kCATransitionFromTop
- kCATransitionFromBottom
1 | //假设存在imageview和一个与其内容不一样的image |
对于CATransition动画,指定图层一次只能使用一个CATransition,所以-addAnimation:forKey:中,无论key设置什么,它的键都是“transition”,也就是常量kCATransition。
在隐式动画中,我们曾经用过渡来自定义动画的行为。事实上,我们设置layer的content属性时,默认的隐式动画就是过渡。
图层树的过渡动画
过渡动画中,并不关心图层到底哪个属性发生了变化。这意味着即使一个图层的图层树发生了变化,我们仍然可以利用过渡来进行动画效果。
1 | //初始化三个view |
1 | //添加动画 |
我们可以看到黄色的view从左面出现,挤出了红色的view。
自定义动画
CATransition本身的动画很少,但UIView提供了+transitionFromView:toView:duration:options:completion:和+transitionWithView:duration:options:animations:来完成过渡的效果。动画相比CATransition丰富很多:
- UIViewAnimationOptionTransitionFlipFromLeft
- UIViewAnimationOptionTransitionFlipFromRight
- UIViewAnimationOptionTransitionCurlUp
- UIViewAnimationOptionTransitionCurlDown
- UIViewAnimationOptionTransitionCrossDissolve
- UIViewAnimationOptionTransitionFlipFromTop
- UIViewAnimationOptionTransitionFlipFromBottom
transitionFromView的方法相当于为两个view的父view的layer添加了一个过渡动画,而transitionWithView则是为自身添加了一个过渡动画。
动画的取消
我们可以通过:1
- (CAAnimation *)animationForKey:(NSString *)key;
来获取动画,但不能对其进行修改,只能获得其属性。
移除动画可以用1
- (void)removeAnimationForKey:(NSString *)key;
或者移除所有动画:1
- (void)removeAllAnimations;
移除动画后,图层的外观立刻恢复到modelLayer的设定。一般情况下,动画在结束后自动移除,除非设置removedOnCompletion为NO。