隐式动画和显式动画

介绍core animation中的显式动画和隐式动画,参考官方文档iOS Core Animation Advanced Techniques

隐式动画

事务

事务是Core Animation中用来执行属性动画的机制。在事务期间,我们修改CALayer的可以动画的属性时,该属性不会立即更改,而是在当前的事务提交之后,统一进行更改。默认情况下,Core Animation会在每个run loop的周期中开始一次新的事务,并在结束时提交事务。这意味着,我们对于CALayer的修改都会通过事务进行一次0.25秒的动画。

我们用事务提交的动画并不需要对动画方式进行设定,动画会根据属性的初始值和终止值以及动画时间,自动完成平滑的效果过渡,故我们称这种动画为隐式动画。系统维护一个我们无法访问的事务栈,我们可以通过+begin和+commit来压入或弹出事务。我们也可以更改动画的运行时间以及完成后的操作。这些设定会对栈顶的事务生效。

我们看下面的例子,我们假设使用到的layer已经初始化,已经添加到某个view的layer中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//添加一个新的事务
[CATransaction begin];
//设置动画持续时间为1秒
[CATransaction setAnimationDuration:1.0];
//添加动画结束的操作
[CATransaction setCompletionBlock:^{
//旋转90度
CGAffineTransform transform = layer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
layer.affineTransform = transform;
}];
//设置layer的颜色为蓝色
layer.backgroundColor = [UIColor blueColor].CGColor;
//提交事务
[CATransaction commit];
};

代码运行的效果为,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
2
3
4
CATransition *transition = [CATransition animation];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
layer.actions = @{@"backgroundColor": transition};

隐式动画原理

隐式动画中,改变一个图层属性后,该属性值其实立刻生效了,但在屏幕上并没有体现,而是根据动画时间渐变更新。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
2
3
4
5
6
7
//假设该view已经初始化,并加入到视图层级中
view.backgroundColor = [UIColor blueColor];
CABasicAnimation *animation = [CABasicAnimation animation];
animation.duration = 2.0;
animation.keyPath = @"backgroundColor";
animation.toValue = (__bridge id)[UIColor redColor].CGColor;
[view.layer addAnimation:animation forKey:nil];

我们可以看到该view从蓝色用2秒时间变到红色,然后瞬间变回蓝色。对比在隐式动画中提到的UIView的+beginAnimations开启动画的方法,我们可以发现,我们仅仅执行了向图层添加动画的操作,却并没有真正修改图层的属性值。换言之,我们仅定义了layer的presentationLayer的行为,却根本没有修改modelLayer,这导致了在动画结束后,view又变回了原来的样子。注意,这是显示动画,故view不会屏蔽掉其关联layer的动画。

如何解决这个问题呢?网上给出的一种很常见的做法如下:

1
2
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;

通过设置removedOnCompletion为NO来使得动画不消失,通过设置fillMode使得动画停留在最后一帧上。但如果使用这种方法,我们必须要留意手动将动画移除,否则动画将持续占用内存直到layer销毁。在动画存在的时候,我们对于该属性的修改都不会产生任何效果。移除动画后,视图会恢复到原状态。所以该方法其实并不适合解决该问题。

另一种方法是在动画开始之前修改layer的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//假设该view已经初始化,并加入到视图层级中
view.backgroundColor = [UIColor blueColor];
CABasicAnimation *animation = [CABasicAnimation animation];
animation.duration = 2.0;
animation.keyPath = @"backgroundColor";
animation.toValue = (__bridge id)[UIColor redColor].CGColor;
//需要设置animation的初始值
animation.fromValue = [view.layer.presentationLayer ?:
view.layer valueForKeyPath : animation.keyPath];
[view.layer addAnimation:animation forKey:nil];
//关闭隐式动画(可选)
//[CATransaction begin];
//[CATransaction setDisableActions:YES];
//直接设置layer的属性为新值
[view.layer setValue:animation.toValue forKeyPath:animation.keyPath];
//[CATransaction commit];

我们对之前的例子做出了两个修改。

其一是设置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
2
3
4
5
6
7
8
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"backgroundColor";
animation.duration = 2.0;
animation.values = @[
(__bridge id)[UIColor blueColor].CGColor,
(__bridge id)[UIColor redColor].CGColor,
(__bridge id)[UIColor greenColor].CGColor,
(__bridge id)[UIColor blueColor].CGColor ];

需要注意关键帧的第一帧应当和layer当前的状态一致,这样才不会发生跳帧现象。最后一帧之后会恢复到原来的状态,解决办法跟CABasicAnimation提到的相同。

当我们需要对layer的position进行动画时,CAKeyframeAnimation提供了一种更为直观的方式path来进行更好的定制:

1
2
3
4
5
6
7
8
9
10
11
12
//路径设置
UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
[bezierPath moveToPoint:CGPointMake(0, 150)];
[bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];

CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.duration = 4.0;
//设置动画路径
animation.path = bezierPath.CGPath;
//使图层能够根据路径切线方向旋转
animation.rotationMode = kCAAnimationRotateAuto;

CAAnimationGroup

CAAnimationGroup是为了将多个动画组合在一起播放而设计的类。我们通过设置该类的animations数组来将animation组合在一起。group中的animation使用的都是CAAnimationGroup的duration:

1
2
3
CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
groupAnimation.animations = @[animation1, animation2];
groupAnimation.duration = 4.0;

CATransition

当我们需要改变非动画属性(图片)或从层级关系中添加和删除图层时,之前的属性动画将不会生效。所以我们使用CATransition来实现图层外观的过渡变化。

CATransition通过type和subtype来标记变换效果。type属性是一个NSString类型,可以被设置成如下类型:

  • kCATransitionFade
  • kCATransitionMoveIn
  • kCATransitionPush
  • kCATransitionReveal

kCATransitionFade创建一个平滑的淡入淡出效果。kCATransitionMoveIn中新图层从外部滑入,覆盖旧图层。kCATransitionPush中新图层从外部滑入,旧图层滑出。kCATransitionReveal中旧图层划出,显露出新图层。

subtype提供移动方向,默认kCATransitionFromLeft

  • kCATransitionFromRight
  • kCATransitionFromLeft
  • kCATransitionFromTop
  • kCATransitionFromBottom
1
2
3
4
5
6
//假设存在imageview和一个与其内容不一样的image
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
//添加过渡动画
[imageView.layer addAnimation:transition forKey:nil];
self.imageView.image = image;

对于CATransition动画,指定图层一次只能使用一个CATransition,所以-addAnimation:forKey:中,无论key设置什么,它的键都是“transition”,也就是常量kCATransition。

在隐式动画中,我们曾经用过渡来自定义动画的行为。事实上,我们设置layer的content属性时,默认的隐式动画就是过渡。

图层树的过渡动画

过渡动画中,并不关心图层到底哪个属性发生了变化。这意味着即使一个图层的图层树发生了变化,我们仍然可以利用过渡来进行动画效果。

1
2
3
4
5
6
7
8
//初始化三个view
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
[self.view addSubview:view];
UIView *view1 = [[UIView alloc] initWithFrame:view.bounds];
[view addSubview:view1];
view1.backgroundColor = [UIColor redColor];
UIView *view2 = [[UIView alloc] initWithFrame:view.bounds];
view2.backgroundColor = [UIColor yellowColor];
1
2
3
4
5
6
7
//添加动画
CATransition *trans = [CATransition animation];
trans.type = kCATransitionPush;
trans.duration = 3;
//将动画添加在superlayer上
[view.layer addAnimation:trans forKey:nil];
[view addSubview:view2];

我们可以看到黄色的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。

文章目录
  1. 1. 隐式动画
    1. 1.1. 事务
    2. 1.2. 视图图层
    3. 1.3. 隐式动画原理
  2. 2. 显式动画
    1. 2.1. 属性动画
      1. 2.1.1. CABasicAnimation
      2. 2.1.2. CAKeyframeAnimation
    2. 2.2. CAAnimationGroup
    3. 2.3. CATransition
      1. 2.3.1. 图层树的过渡动画
      2. 2.3.2. 自定义动画
      3. 2.3.3. 动画的取消
,