《iOS 三问》 -- iOS 动画之 CALayer 隐式动画

1 什么是隐式动画

iOS App 为什么能那么惊艳,一经出炉就靓瞎大家的眼睛,直接把 web 服务完爆之,靠的就是其流畅优美的动画。而实质上,我们 App 中的大部分动画都由系统帮我们默默实现的。比如页面 push、pop 时的转场动画,弹出对话框、打开 App 等的弹入弹出效果等,iOS 框架在底层帮我们做了很多事情使得 开发者可以花最少的精力给 App 做出动效,从而保证了每个 App 体验上的一致地优美 – 而 隐式动画 无疑是这个思想下最优雅的产物。

试想下,你只要改变一个 view 的背景色,系统就自动帮你生成 view 背景色的渐变效果,而无需写各种烦人的动画事务、动画参数配置等等。这样做还有个好处,就是让你的 App 不仅更美观更炫,而且能让变化之处引起用户的注意。你想想看,在一个这么小的屏幕下界面往往充斥着各种各样拥挤的信息,而改变处的一则动画,能够更吸引用户的注意,从而不会错过你想提醒用户要注意的事情!

1.1 隐式动画与非 RootLayer

我们来看一处简单的隐式动画,下面的代码中,系统将自动生成隐式动画:从黄色到红色渐变,并向右位置 200 个象素点位置

一处简单的隐式动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//view frame 作为动画元素 CALayer 的载体
UIView *viewFrame = [UIView new];

// 新建一个 Layer 添加到 View 载体上
CALayer *animLayer = [[CALayer alloc] init];
animLayer.backgroundColor = [UIColor yellowColor].CGColor;
animLayer.frame = CGRectMake(0, 0, 100, 100);
[viewFrame.layer addSublayer:animLayer];

// 改变动画 layer 的可动画属性,系统将自动生成隐式动画:从黄色到红色渐变,并向右位置 200 个象素点位置
animLayer.backgroundColor = [UIColor redColor].CGColor;
CGPoint p = animLayer.position;
p.x += 200;
animLayer.position = p;

但是并不是每个 layer 都有隐式动画,只有 非 Root layer 才可以。每一个 UIView 内部都默认关联着一个 CALayer,我们可用称这个 Layer 为 Root Layer(根层)。所有的非 Root Layer,也就是手动创建的 CALayer 对象,都存在着隐式动画。

  • 为什么要非 RootLayer 才有隐式动画?

对于喜欢深究的童鞋一定不会满足教条式的传授,对啊,为什么只有非 RootLayer 才有隐式动画呢?这点当我们介绍完 动画事务重绘时刻的本质 之后,就可以揭晓了。

1.2 可动画属性

那些会产生隐式动画的属性称为 Animatable Properties (可动画属性)。CALayer 支持的可动画属性在文章最上层的导图可以看到。
这里主要要注意的就是 frame 并不是可动画属性!!当你要对 layer 的位置或大小作隐式动画时,应该使用 boundsposition

2 动画事务与隐式动画的实现

CATransaction,动画事务,是 CoreAnimation 框架动画实现的重要角色,负责把一系列动画请求组合成一个独立的动画过程,用其 begincommit 方法包住 – 这一系列打包的动画组合我们称之为 事务 block(transaction block),这一切也可以使用显示动画手动编写,但是在 CA 框架为我们的代码自动生成事务 block 而不用手动写 begincommit,从而把我们对可动画属性的修改包装成一个动画事务。

2.1 begincommit 方法

通过上面的分析,我们了解到一个动画事务就像是一段封装好的代码,类似这样:

1
2
3
4
5
[CATransaction begin];

... transation block(可动画属性的修改,产生从初始值到目标值的动画)

[CATransaction commit];

实际上 CATransaction 的 begincommit 方法其实是一个事务栈操作。begin 是入栈一个动画事务,而 commit 则是将栈顶事务出栈。那么隐式动画是怎么产生的呢?那就简单了,只要把可以做动画的图层属性都添加到栈顶的事务就行了。

2.2 重绘时刻的本质

实际上我们所说的重绘时刻,除了用户显式地在代码里调用 setNeedsDisplay 之外,Core Animation 在每个 run loop 周期中自动开始一次新的动画事务(run loop 是 iOS 负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的事件循环),即使你不显式的用 [CATransaction begin] 开始一次事务,任何在一次 run loop 循环中属性的改变都会被集中起来,然后做一次 0.25 秒的动画 (也就是说这个默认的 CATransaction 动画的 duration 默认是 0.25)。

而在这个重绘时刻里屏幕更新 UI 具体会做什么呢:

  1. Layout,对屏幕中的 View 递归地布局 (也就是计算 view 们的位置及大小);
  2. 重绘 (ReDraw),调用 view 及 layer 的重绘方法重绘自己;
  3. 修改 view 的 layout 属性;
  4. 开始动画;
  5. 动画在子线程里进行,结束后调用 complete block;

2.3 设置当前动画事务

明白了隐式动画的本质是默认的动画事务之后,我们就可以对该动画事务进行配置,从而改变隐式动画的默认效果了。比较常用的有几个方法:

1
2
3
4
5
- setAnimationDuration :设置隐式动画的持续时间。

- setAnimationTimingFunction :设置隐式动画的计时函数。

- setCompletionBlock :设置隐式动画的完成回调。

比如

设置隐式动画的持续时间
1
2
3
4
5
[CATransaction setAnimationDuration:2];
animLayer.backgroundColor = [UIColor redColor].CGColor;
CGPoint p = animLayer.position;
p.x += 200;
animLayer.position = p;

如果你不想对其它动画事务产生副作用,也可以显示地起一个新的事务

显式地起一个新事务
1
2
3
4
5
6
7
[CATransaction begin]; // 新入栈一个事务

[CATransaction setAnimationDuration:1.0]; // 动画的持续时间

animLayer.backgroundColor = [UIColor redColor].CGColor; // 隐式动画

[CATransaction commit]; // 出栈并提交事务

2.3.1 关闭隐式动画

如果你想关闭隐式动画,很简单,在修改动画属性前调用 [CATransaction setDisableActions:YES]; 即可。

同样,如果你不想产生副作用,可以显示地起一个新动画事务。

2.4 为什么 root layer 不支持隐式动画

现在我们可以来解释一下为什么 root layer 不支持隐式动画了。原来就是UIView 把它的 RootLayer 的隐式动画给关闭了!因为每个 UIView 必有一个 rootLayer,我猜想 UIView 之所以这样设计,默认关闭 rootLayer 的隐式动画,是为了防止滥用隐式动画,以及过度使用之而带来的效率问题。

3 引用

【1】[Matt Neuburg - 《Programming iOS deep into views,view controllers and frameworks》]
【2】[Nick LockWood - 《iOS Core Animation: Advanced Techniques》]