【iOS进阶】之深入浅出理解和使用 Core Animation的隐式动画

目录

1 何为隐式动画

1.1特点

1.2 隐式动画如何进行?何时产生?

2 开发者如何利用隐式动画

2.1 CATransaction 没有实例方法,只有类方法。

2.2 UIView的动画方法的实现

3 UIView与隐式动画那若即若离的关系

3.1 CALayer的隐式动画如何产生?

3.2 UIView如何禁用其layer的隐式动画

4 控制UIView或CALayer的隐式动画效果

4.1.  控制UIView的动画(不仅仅是隐式动画)

4.2. 控制单独的图层的隐式动画

5 总结

6 参考文献


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隐式动画及事务

5. 设置CALayer的delegate时需要注意的问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值