事务
Core Animation基于一个假设,说屏幕上的任何东西都可以(或者可能)做动画。你并不需要在Core Animation中手动打开动画,但是你需要明确地关闭它,否则它会一直存在。
当你改变CALayer
一个可做动画的属性时,这个改变并不会立刻在屏幕上体现出来。相反,该属性会从先前的值平滑过渡到新的值。这一切都是默认的行为,你不需要做额外的操作。
接下来看一个例子,老样子,先上代码
@interface ViewController () @property (nonatomic, strong) CALayer *colorLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad];
// 创建colorLayer,注意一定要另外创建一个CALayer对象,不要用与视图关联的layer,原因我们后续会讲到
self.colorLayer = [CALayer layer]; self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); self.colorLayer.backgroundColor = [UIColor blueColor].CGColor; [self.view.layer addSublayer:self.colorLayer]; }
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
}
@end
运行之后的初始状态
这里为了方便,我们将改变layer背景色的代码放在了touchesBegan中,点击视图,我们可以看到layer的背景色缓慢的变化为一个新的颜色(系统默认的动画周期是0.25秒,可能不太明显),像这种不显示创建动画对象的方式,称之为隐式动画。
但当你改变一个属性,Core Animation是如何判断动画类型和持续时间的呢?实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为。
事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。
事务是通过CATransaction
类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。CATransaction
没有属性或者实例方法,并且也不能用+alloc
和-init
方法创建它。而是用类方法+begin
和+commit
分别来入栈或者出栈。任何可以做动画的图层属性都可以添加到栈顶的事务。
接下来我们对之前的代码进行更改,用CATransaction进行一些控制
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [CATransaction begin]; [CATransaction setAnimationDuration:1.0]; CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; [CATransaction commit]; }
我们通过CATransaction对动画周期做了设置,可以明显看出动画过程
完成块
CATransaction中有一个完成动画后的回调,我们添加上可以看看
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [CATransaction begin]; [CATransaction setAnimationDuration:1.0]; [CATransaction setCompletionBlock:^{ CGAffineTransform transform = self.colorLayer.affineTransform; transform = CGAffineTransformRotate(transform, M_PI_4); self.colorLayer.affineTransform = transform; }]; CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; [CATransaction commit]; }
运行后点击视图,可以看到如下图
注意旋转动画要比颜色渐变快得多,这是因为完成块是在颜色渐变的事务提交并出栈之后才被执行,于是,用默认的事务做变换,默认的时间也就变成了0.25秒。
图层行为
接下来就到了解释为什么不要直接操作与视图关联的图层,这里我们先添加一个按钮和一个视图,添加完之后这个样子
点击按钮时,要响应如下方法
@interface ViewController () @property (weak, nonatomic) IBOutlet UIView *animationView; @end - (IBAction)changeColor:(UIButton *)sender { [CATransaction begin]; [CATransaction setAnimationDuration:1.0]; CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; self.animationView.layer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; [CATransaction commit]; }
当点击按钮时,我们并没有看到视图的背景慢慢过渡到新颜色,而是立刻变化的,这是为什么呢?
试想一下,如果UIView
的属性都有动画特性的话,那么无论在什么时候修改它,我们都应该能注意到的。所以,如果说UIKit建立在Core Animation(默认对所有东西都做动画)之上,那么隐式动画是如何被UIKit禁用掉呢?
我们把改变属性时CALayer
自动应用的动画称作行为,当CALayer
的属性被修改时候,它会调用-actionForKey:
方法,传递属性的名称。剩下的操作都在CALayer
的头文件中有详细的说明,实质上是如下几步:
- 图层首先检测它是否有委托,并且是否实现
CALayerDelegate
协议指定的-actionForLayer:forKey
方法。如果有,直接调用并返回结果。 - 如果没有委托,或者委托没有实现
-actionForLayer:forKey
方法,图层接着检查包含属性名称对应行为映射的actions
字典。 - 如果
actions字典
没有包含对应的属性,那么图层接着在它的style
字典接着搜索属性名。 - 最后,如果在
style
里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:
方法。
所以一轮完整的搜索结束之后,-actionForKey:
要么返回空(这种情况下将不会有动画发生),要么是CAAction
协议对应的对象,最后CALayer
拿这个结果去对先前和当前的值做动画。
于是这就解释了UIKit是如何禁用隐式动画的:每个UIView
对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey
的实现方法。当不在一个动画块的实现中,UIView
对所有图层行为返回nil
,但是在动画block范围之内,它就返回了一个非空值。我们可以用一个demo做个简单的实验
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"Outside: %@", [self.animationView actionForLayer:self.animationView.layer forKey:@"backgroundColor"]); [UIView beginAnimations:nil context:nil]; NSLog(@"Inside: %@", [self.animationView actionForLayer:self.animationView.layer forKey:@"backgroundColor"]); [UIView commitAnimations]; // Do any additional setup after loading the view, typically from a nib. }
控制台打印结果
2017-11-02 15:26:05.397157+0800 Animation[3801:286395] Outside: <null> 2017-11-02 15:26:05.398090+0800 Animation[3801:286395] Inside: <CABasicAnimation: 0x6040004362a0>
所以跟猜测的一样,当属性在动画块之外发生改变,UIView
直接通过返回nil
来禁用隐式动画。但如果在动画块范围之内,根据动画具体类型返回相应的属性,在这个例子就是CABasicAnimation
当然返回nil
并不是禁用隐式动画唯一的办法,CATransaction
有个方法叫做+setDisableActions:
,可以用来对所有属性打开或者关闭隐式动画。
总结
这一章讨论了隐式动画,还有Core Animation对指定属性选择合适的动画行为的机制。同时你知道了UIKit是如何充分利用Core Animation的隐式动画机制来强化它的显式系统,以及动画是如何被默认禁用并且当需要的时候启用的。