目录
2.1 CATransaction 没有实例方法,只有类方法。
iOS中的动画,最大的动画分类有2种:隐式动画和显式动画。这两者相同,但又有所不同:隐式动画跟id<CAAction>有关(称为行为);显式动画跟CAAnimation及其子类有关(CAAnimation遵循CAAction协议)。
1 何为隐式动画
1.1特点
(1)系统自带的
(2)发生在CALayer改变(属性值被更改,或者图层树发生改变)。【注意】:为什么是CALayer,而不是UIView?下面会说到)
隐式动画是系统自己创建的动画。我们不需要做任何设置,由系统去决定这个动画如何进行,何时产生。
【体现】:当我们改变CALayer的动画属性(背景色、透明度等)的时候,CALayer的改变会有一个过渡的过程(动画)。这个过渡的动画,就是系统的隐式动画。
self.regLayer = [CALayer layer];
self.regLayer.frame = CGRectMake(50, 100, 150, 80);
self.regLayer.backgroundColor = [UIColor yellowColor].CGColor;
.....................
- (void)btnClick {
//当图层regLayer的属性(backgroundColor)发生改变,隐式动画就会产生
self.regLayer.backgroundColor = [UIColor redColor].CGColor;
}
1.2 隐式动画如何进行?何时产生?
这涉及到一个概念:事务。
(1)何为CATransaction(事务)
代表一个原子操作。里面可以包含一系列属性动画,相当于是一个动画集合。
主线程维护了一个事务栈。每一个runloop开始的时候系统都会开启一个新的事务(隐式事务),放在栈顶。当前runloop里的所有动画属性的改变、创建的动画对象等跟动画有关的东西都会被集中到处于栈顶事务里,在当前runloop即将进入休眠时,栈里的事务就会被系统提交执行。从而产生隐式动画(默认持续时间为0.25秒)。
系统在线程的runloop注册了一个监听者,在即将休眠(beforeWating)和即将退出loop(Exit)的时候发生回调。优先级式2000000,比其他的observer的优先级都低(优先级高的先执行)。当回调触发,在回调中,系统就把栈里的所有事务里的动画合并,提交到渲染进程。
然而对于多个动画事务合并,经测试:相同属性动画的合并(如0-6-10-21,变化直接从0-21 进行过渡变化),不同的属性的动画保留,如下栗子:
#import "VOLImplicitCAViewController.h"
@interface VOLImplicitCAViewController ()
@property(nonatomic, weak) CALayer *regLayer;
@end
@implementation VOLImplicitCAViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
[self initialUI];
}
- (void)initialUI {
CALayer *regLayer = [CALayer layer];
regLayer.frame = CGRectMake(50, 100, 150, 80);
regLayer.backgroundColor = [UIColor yellowColor].CGColor;
regLayer.position = CGPointMake(200, 200);
[self.view.layer addSublayer:regLayer];
self.regLayer = regLayer;
UIButton *btn = [UIButton new];
btn.frame = CGRectMake(100, 700, 100, 100);
btn.backgroundColor = [UIColor blackColor];
[btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[btn setTitle:@"改变" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
- (void)btnClick {
[CATransaction begin];
[CATransaction setAnimationDuration:7.0];
self.regLayer.backgroundColor = [UIColor redColor].CGColor;
self.regLayer.position = CGPointMake(200, 400);
[CATransaction commit];
[CATransaction begin];
self.regLayer.position = CGPointMake(200, 550);
[CATransaction commit];
[CATransaction begin];
self.regLayer.position = CGPointMake(100, 550);
[CATransaction commit];
}
@end
上述代码, 按理说,动画的路径应该是:先往下移动,再往左移动
然而,代码运行之后,则是直接往左下角移动,如图
因此,建议如果要做多个有先后次序的动画,应该使用显示动画里的组动画CAAnimationGroup。否则可能得到意想不到的动画效果。
2 开发者如何利用隐式动画
开发者可以通过设置事务(CATransaction)来控制系统的产生的隐式动画。
2.1 CATransaction 没有实例方法,只有类方法。
(1)+begin(显式事务)
开始一个事务,相当于push,入栈。该语句之后是我们的设置代码。
(2)+commit
提交一个事务。相当于pop,出栈。
【注意】:
- 事务可以嵌套,但是内部的事务commit后不会被执行。只有最外层的事务commit,整个动画才会被执行。
- begin 和 commit是成对出现的,
我们的属性改变、事务的设置都放在begin 和commit之间。
[CATransaction begin];
[CATransaction setAnimationDuration:7.0];
self.regLayer.backgroundColor = [UIColor redColor].CGColor;
self.regLayer.position = CGPointMake(200, 200);
[CATransaction commit];
(3)+setAnimationDuration
设置当前事务动画的持续时间(系统默认的持续时间是0.25秒)
(4)+animationDuration
获取当前事务的动画的持续时间(系统默认的持续时间是0.25秒)
【注意】
- 当前事务:指的是事务栈上处于栈顶的事务。
- 在一对事务语句里面,“当前事务”就是只这个begin commit 包含的事务。
- 如果我们没有自己push一个事务,那么这个当前事务就是系统维护的事务栈的处于栈顶的事务。
因此,当我们要自己设置隐式动画的时候,应该自己begin一个事务,在这个事务里做设置,否则我们的设置将影响其他的事务,产生我们不期待的动画。
(5)+setCompletionBlock:
设置事务完成后执行的block。
2.2 UIView的动画方法的实现
UIView有两个设置动画的老方法:+beginAnimations:context:和+commitAnimations(用法和CATransaction的+begin和+commit方法类似)
UIView的新的block动画类方法+animateWithDuration:animations:
其内部实现就是设置了CATransaction。
而UIView的带有完成block的类方法,其完成block是设置了CATransaction的+setCompletionBlock
3 UIView与隐式动画那若即若离的关系
我们知道,UIView内部由一个CALayer(layer属性)。在屏幕上显示的,做动画的其实是CALayer,并非UIView。UIView是其内部管理的layer的代理。所有对UIView所做的设置/读取其实都是在设置/读取其内部的layer的对应属性。然而,改变UIView的背景色等属性,或则改变UIView.layer的背景色等属性,即使这些改变的代码语句是放在CATransaction的begin/commit之间,也不会产生隐式动画。这是因为UIView禁用了其管理的layer的隐式动画。
3.1 CALayer的隐式动画如何产生?
当CALayer的属性发生改变,系统会向CALayer读取一个action(既实现了CAAction协议的对象,称为行为)用来执行动画。CALayer返回的action步骤如下:
(1)CALayer判断自己有没有实现了CALayerDelegate协议的代理,如果有,判断该代理是否实现了CALayerDelegate协议的-actionForLayer:forKey方法。如果有实现该方法,调用该方法并返回结果。
(2)如果步骤(1)中,自己没有代理,或者有代理但是代理没有实现-actionForLayer:forKey方法,则CALayer检查其actions字典(该字典是【属性名称:行为】的映射)。如果查找有值,返回结果。
(3)actions字典查找不到,则CALayer检查其style字典(该字典是【属性名称:行为】的映射)。如果查找有值,返回结果。
(4)如果style字典仍然没查找到,则图层直接调用实现了每个属性标准动画行为的方法:-defaultActionForKey:方法。
【总结】:上述步骤,只要返回非nil,就会中断查找并将值返回。如果返回的是nil,就会接着往下查找。 注意:[NSNull null] 不是nil,返回它会中断查找过程,但是会返回空给系统,系统会依然沿着上述步骤查找动画。
3.2 UIView如何禁用其layer的隐式动画
UIView/UIView.layer的动画属性更改在CATransaction的begin/commit之间的时候没有动画产生,但是在UIView的动画类方法,是有动画产生的。我们平时用的动画,就是用的UIView的类方法。
例子1:没有动画产生,背景色瞬间变色
#import "VOLLayerAndViewViewController.h"
@interface VOLLayerAndViewViewController ()
@property(nonatomic, weak) UIView *colorView;
@end
@implementation VOLLayerAndViewViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *colorView = [[UIView alloc] initWithFrame:CGRectMake(80, 100, 100, 100)];
colorView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:colorView];
self.colorView = colorView;
UIButton *colorViewbtn = [UIButton new];
colorViewbtn.frame = CGRectMake(80, 100 + 500, 100, 50);
colorViewbtn.backgroundColor = [UIColor blackColor];
[colorViewbtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[colorViewbtn setTitle:@"改变View" forState:UIControlStateNormal];
[colorViewbtn addTarget:self action:@selector(colorViewbtnClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:colorViewbtn];
}
- (void)colorViewbtnClick {
[self normalChange];
}
- (void)normalChange {
//改变UIView的属性(backgroundColor),没有隐式动画产生
self.colorView.backgroundColor = [UIColor redColor];
}
@end
因此,UIView作为其layer的代理,实现了CALayerDelegate协议的-actionForLayer:forKey方法,但是在动画block之外,返回的是nil。在动画block里之内则返回了响应的行为对象。我们可以在UIView动画提交前后打印其动画对象验证:
#import "VOLLayerAndViewViewController.h"
@interface VOLLayerAndViewViewController ()
@property(nonatomic, weak) UIView *colorView;
@end
@implementation VOLLayerAndViewViewController
- (void)viewDidLoad {
[super viewDidLoad];
........
UIButton *colorViewbtn = [UIButton new];
colorViewbtn.frame = CGRectMake(80, 100 + 500, 100, 50);
colorViewbtn.backgroundColor = [UIColor blackColor];
[colorViewbtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[colorViewbtn setTitle:@"改变View" forState:UIControlStateNormal];
[colorViewbtn addTarget:self action:@selector(colorViewbtnClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:colorViewbtn];
}
- (void)colorViewbtnClick {
[self printAnimation];
}
- (void)printAnimation {
NSLog(@"block外---上--- %@", [self.colorView actionForLayer:self.colorView.layer forKey:@"backgroundColor"]);
[UIView beginAnimations:nil context:nil];
NSLog(@"block内------ %@", [self.colorView actionForLayer:self.colorView.layer forKey:@"backgroundColor"]);
[UIView commitAnimations];
NSLog(@"block外---下--- %@", [self.colorView actionForLayer:self.colorView.layer forKey:@"backgroundColor"]);
}
@end
2019-04-22 14:13:42.645028+0800 LayerPractice[42139:4887056] block外---上--- <null>
2019-04-22 14:13:42.645232+0800 LayerPractice[42139:4887056] block内------ <CABasicAnimation: 0x600002fd34c0>
2019-04-22 14:13:42.645333+0800 LayerPractice[42139:4887056] block外---下--- <null>
另外,除了用-actionForLayer:forKey禁用CALayer的隐式动画,还有一个禁用CALayer隐式动画的方法。
CATransaction的类方法:
+ (void)setDisableActions:(BOOL)flag;
- flag为YES,对当前CATransaction的所有属性禁用隐式动画
- flag为NO,对当前CATransaction的所有属性打开隐式动画
【注意】:还是那句话,对CATransaction的设置,最要是先自己push一个事务。否则会得到不期待的效果。
4 控制UIView或CALayer的隐式动画效果
控制隐式动画,主要参考隐式动画的产生路径(3.1 CALayer的隐式动画如何产生?),通过介入这些路径,达到控制隐式动画的目的。UIView和CALayer的控制隐式动画的方式是不一样的。
4.1. 控制UIView的动画(不仅仅是隐式动画)
由于UIView禁用了其图层的隐式动画,因此对于关联了UIView的图层,除了通过自定义UIView并且覆盖-actionForLayer:forKey的实现来控制隐式动画外,没有别的方法来。这里给出的是UIView做动画的方法,而不仅仅是控制隐式动画的方法。
(1)使用UIView的动画方法(CAtransition没用),如
+ (void)animateWithDuration:animations:completion:
[UIView beginAnimations:nil context:nil];
[UIView commitAnimations];
........
等等的UIView动画方法
(2)自定义UIView,覆盖-actionForLayer:forKey的实现(控制隐式动画)
对于我们要设置的特殊属性动画,创建并返回一个自定义的动画。其他不需要做特殊动画的属性,直接调用[super actionForLayer:layer forKey:event]返回
@interface VOLImplictView : UIView
@end
#import "VOLImplictView.h"
@implementation VOLImplictView
- (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if ([event isEqualToString:@"backgroundColor"]) {
//可以任意修改动画
CABasicAnimation *animation = [CABasicAnimation animation];
animation.duration = 5.0f;
return animation;
}
//对于不需要自定义/修改的动画,直接调用[super actionForLayer:layer forKey:event]返回
return [super actionForLayer:layer forKey:event];
}
@end
==================================================
@interface VOLImplicitViewController ()
@property(nonatomic, weak) VOLImplictView *implictView;
@end
@implementation VOLImplicitViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
VOLImplictView *implictView = [[VOLImplictView alloc] initWithFrame:CGRectMake(50, 200, 200, 200)];
implictView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:implictView];
self.implictView = implictView;
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(50, 500, 100, 50)];
btn.backgroundColor = [UIColor blueColor];
[btn setTitle:@"点击改变" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
- (void)btnClick {
self.implictView.backgroundColor = [UIColor brownColor];
self.implictView.frame = CGRectMake(50, 200, 250, 250);
}
(3)使用显式动画
显式动画有多种,这里暂且不给栗子先。
4.2. 控制单独的图层的隐式动画
对于单独的图层(直接创建的CALayer),控制其隐式动画的方法(不限于CALayer的可动画属性,自定义的属性也可以定义其对应的行为,让其触发)如下:
(1)CATransaction。上述给的栗子有相关的方法,这里不在赘叙。
(2)实现图层的-actionForLayer:forKey:方法。有3中实现:
- 自定义CALayer并且实现-actionForLayer:forKey:方法。
- 将layer的delegate指向单独的CALayer,该CALayer实现actionForLayer:forKey:
- 创建一个继承自NSObject的工具类,然后将其设置为layer的代理,由这个工具类去实现动画
下面的栗子为自定义CALayer并且实现-actionForLayer:forKey:方法。
@interface VOLDelegateLayer : CALayer
@end
@implementation VOLDelegateLayer
- (instancetype)init {
if ((self = [super init])) {
self.delegate = self;
}
return self;
}
#pragma mark - CALayerDelegate
- (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
NSLog(@"___________________%@", event);
if ([event isEqualToString:@"backgroundColor"]) {
CATransition *transition = [CATransition animation];
transition.type = kCATransitionReveal;
transition.subtype = kCATransitionFromBottom;
return transition;
}
return nil;
}
@end
===============================================
@interface VOLLayerDelegateViewController ()
@property(nonatomic, weak) VOLDelegateLayer *layer;
@end
@implementation VOLLayerDelegateViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
VOLDelegateLayer *layer = [VOLDelegateLayer layer];
layer.frame = CGRectMake(80 + 150, 180, 100, 100);
layer.backgroundColor = [UIColor yellowColor].CGColor;
[self.view.layer addSublayer:layer];
self.layer = layer;
UIButton *btn = [UIButton new];
btn.frame = CGRectMake(100, 700, 100, 50);
btn.backgroundColor = [UIColor blackColor];
[btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[btn setTitle:@"引起动画" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
- (void)btnClick {
self.layer.backgroundColor = [UIColor redColor].CGColor;
}
@end
效果如下:当改变背景色,layer有向下揭盖上一层layer的效果(由transition.type = kCATransitionReveal;
transition.subtype = kCATransitionFromBottom;控制)
【注意】:CALayer的代理不要设置为UIView/UIViewController
UIView已经把自己设置为其layer的代理,会出现野指针crash,或则会影响UIView自带的layer
设置为UIViewController的话,会在push之后启动了动画,然后再POP出来,产生crash。原因是野指针(已亲测复现该崩溃,见demo的VOLAnimationViewController类)。(应该是Viewcontroller被释放了)。
(3)给CALayer提供一个actions字典
actions字典默认是没有的。我们可以给CALayer提供一个actions字典。
@interface VOLActionsViewController ()
@property(nonatomic, weak) CALayer *layer;
@end
@implementation VOLActionsViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(80 + 150, 180, 100, 100);
layer.backgroundColor = [UIColor yellowColor].CGColor;
[self.view.layer addSublayer:layer];
self.layer = layer;
UIButton *btn = [UIButton new];
btn.frame = CGRectMake(100, 700, 100, 50);
btn.backgroundColor = [UIColor blackColor];
[btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[btn setTitle:@"引起动画" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
- (void)btnClick {
[self actionChangeForAnimate];
}
- (void)actionChangeForAnimate {
CATransition *transition = [CATransition animation];
transition.type = kCATransitionReveal;
transition.subtype = kCATransitionFromTop;
self.layer.actions = @{@"backgroundColor": transition};
// self.layer.actions = @{@"backgroundColor" : [NSNull null]};
self.layer.backgroundColor = [UIColor redColor].CGColor;
}
@end
(4)自定义CALayer,覆盖最后的默认的动画提供方法:+defaultActionForKey:
@interface VOLLayer : CALayer
@end
@implementation VOLLayer
+ (id<CAAction>)defaultActionForKey:(NSString *)event {
if ([event isEqualToString:@"backgroundColor"]) {
CATransition *theAnimation = [[CATransition alloc] init];
theAnimation.duration = 1.0;
//设置颜色渐变的同时,伴随图层从右侧push进入的效果
theAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
theAnimation.type = kCATransitionPush;
theAnimation.subtype = kCATransitionFromRight;
return theAnimation;
}
return [super defaultActionForKey:event];
}
@end
===================================
@interface VOLDefaultActionViewController ()
@property(nonatomic, weak) VOLLayer *layer;
@end
@implementation VOLDefaultActionViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
VOLLayer *layer = [VOLLayer layer];
layer.frame = CGRectMake(80 + 150, 180, 100, 100);
layer.backgroundColor = [UIColor yellowColor].CGColor;
[self.view.layer addSublayer:layer];
self.layer = layer;
UIButton *btn = [UIButton new];
btn.frame = CGRectMake(100, 700, 100, 50);
btn.backgroundColor = [UIColor blackColor];
[btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[btn setTitle:@"引起动画" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
- (void)btnClick {
self.layer.backgroundColor = [UIColor brownColor].CGColor;
}
@end
效果如下(这是一个渐变的过程,但是我这个GIF制作出来,渐变效果没了,晕)
5 总结
隐式动画是系统通过一系列查找步骤,获取默认的动画对象所做的动画。可以通过介入这些步骤,达到控制隐式动画的目的。
最后遗留一个问题:
根据系统获取默认动画对象的步骤,第3个步骤的CALayer的style的字段如何设置动画?我自定义一个包含动画对象的字典,动画是设置不出来的。目前我也没找到相关的通过设置CALayer的style字段来设置动画。希望好心人给我一下提示,我们一起学习共同进步哈!^ ^
6 参考文献
1. CALayer的隐式动画
2. iOS 保持界面流畅的技巧
3. iOS动画原理--隐式动画
4. 你所不知道的CALayer隐式动画及事务