iOS核心动画笔记7-隐式动画

隐式动画

1. 事务

Core Animation 基于一个假说, 屏幕上任何东西都可以(或者可能)做动画. 动画并不需要再Core Animation中手动打开, 相反的需要明确的关闭, 否则将一直存在.

当改变CALayer的一个可做动画的属性, 改变并不会立刻在屏幕上体现出来, 它会从从前的值平滑的过渡到新的值. 这一切都是默认的行为, 不需要我们做任何操作.

这就是隐式动画. 我们并不需要指定动画类型, 仅仅改变一个属性, 然后Core Animation会决定如何并且何时去做动画. 实际上, 当改变一个属性, 动画执行时间取决于当前事务的设置, 动画类型取决于图层的行为. 事务是Core Animation用来包含一系列属性动画集合的机制, 任何用指定事务去改变可以做动画的图层的属性都不会立刻发生改变, 而是当事务提交的时候, 开始一个动画过度到新的值.

事务是通过CATransaction类来管理的, 这个类管理了一叠不能访问的事务, 没有属性和实例方法, 并且不能alloc, init创建它, 但是可以用+begin, +commit分别来入栈和出栈. 任何可以做动画的图层属性都会被添加到栈顶的事务, 可以通过+setAnimationDuration:设置当前事务的动画时间, 或者 +animationDuration来获取值.

Core Animation在每个run loop周期自动开始一次新的事务(run loop是iOS负责收集用户输入, 处理定时器或者网络时事件并且重新绘制屏幕的东西), 即使不显式的用[CATransaction begin]开始一次事务, 任何一个run loop中属性的改变都会被集中起来, 然后做一次0.25秒的动画.

#pragma mark - 测试事务控制CALayer动画
- (void)changeButtonClick:(UIButton *)sender {
    
    // 开始一个新的事物
    [CATransaction begin];
    // 设置事务的 动画执行时间
    [CATransaction setAnimationDuration:1.f];
    // 禁用动画
    [CATransaction setDisableActions:NO];
    // 设置事务执行完成后的代码块
    [CATransaction setCompletionBlock:^{
        NSLog(@"Animation over");
        self.colorLayer.affineTransform = CGAffineTransformRotate(self.colorLayer.affineTransform, M_PI_4);
    }];
    
    //randomize the layer background color
    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;
    sender.layer.backgroundColor = self.colorLayer.backgroundColor;
    
    // 提交事务和动画
    [CATransaction commit];
}

UIView 通过几个类方法完成的动画效果, 实际上就是都是由于设置了 CATransaction的原因.iOS4中, 苹果味UIView的添加了基于block的动画写发, 事实上也是基于CATransaction的动画.

基于UIView的block的动画允许你在动画结束的时候提供一个完成的动作。CATranscation接口提供的+setCompletionBlock:方法也有同样的功能。如上代码所示.

2. 图层行为

如上第一节, 直接对CALayer对象修改动画属性都会引起动画效果, 但是测试发现, 直接对UIView关联的图层做动画而不是独立的图层, 这时候动画效果消失了. 隐式动画被UIView的关联图层给禁用了.

我们把改变属性时CALayer自动应用的动画称作行为, 当CALayer的属性被修改时候, 它会调用-actionForKey:方法, 传递属性的名称. 剩下的操作在CALayer头文件中有详细的说明, 有以下几个步骤:

  1. 首先图层检测它自己是否有委托, 并且实现CALayerDelegate协议指定的actionForLayer:forKey:方法, 如果有, 直接调用并返回结果.
  2. 如果没有委托或者魏国没有实现-actionForLayer:forKey:方法, 图层接着检查包含属性名称对应行为映射的actions字典.
  3. 如果actions字典没有包含对应的属性, 那么图层接着在它的style字典中搜索属性名.
  4. 如果style里面也找不到对应的行为, 那么图层将会直接调用定义了每个属性标准行为的-defaultActionForKey:方法.

所以经过一轮的搜索, -actionForKey:要么返回空(这时候将不会有动画发生), 要么是CAAction协议对应的对象, 最后CALayer拿着这个结果去对先前和当前的值做动画.

UIKit如何禁用隐式动画: 每个UIView对它关联的图层都扮演了一个委托, 并且提供了-actionForLayer:forKey:的实现方法. 当不在一个动画块的实现中, UIView对所有图层都返回nil, 但是在block范围之内, 就返回一个非空值. 于是我们可以预言, 当属性在动画块之外发生改变, UIView直接通过返回nil来禁用隐式动画. 但如果在动画块范围之内, 根据动画具体类型返回相应的属性.

CATransaction有个方法叫做: +setDisableActions:, 可以用来对所有属性打开或者关闭隐式动画. 在 [CATransaction begin] 之后添加[CATransaction setDisableActions:YES];代码, 就可以阻止动画的发生.

总结一下:

  1. UIView的关联图层禁用了隐式动画, 对这种图层做动画唯一办法就是使用UIView的动画函数(而不是依赖CATransaction), 或者继承UIView, 覆盖 -actionForLayer:forKey:方法, 或者直接创建一个显式动画.
  2. 对于单独存在的图层, 我们可以通过实现图层的-actionForLayer:forKey:委托方法, 或者提供一个actions字典来控制隐式动画.

对于单独的图层, 设置actions字典要比实现代理更加简单一点. 行为通常是一个被Core Animation隐式调用的显式动画对象, 比如使用CATransition, 因为它可以相应CAAction协议, 并且可以作为图层行为. 通过此对象, 我们可以自定义隐式动画的动画样式. demo:

#pragma mark - 测试过渡动画
- (void)changeButtonClick1:(UIButton *)sender {
    
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionPush;
    transition.subtype = kCATransitionFromLeft;
    self.colorLayer.actions =  @{@"backgroundColor": transition};
    
    
    //randomize the layer background color
    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;
    sender.layer.backgroundColor = self.colorLayer.backgroundColor;
}

3. 呈现与模型

CALayer的属性行为很不寻常, 因为改变一个图层的属性而没有立即生效, 而是通过一段时间渐变更新, 这是怎么做到的呢? 这一小节做简单的笔记.

当改变图层属性时候, 属性值是立即被更新的(当读取它的数据时候, 会发现它的值在设置那一刻就已经生效了), 但是屏幕并没有立即发生变化. 这是因为你设置的属性并没有直接调整图层的外观, 相反, 它只是定义了图层动画结束之后将要改变的外观.

当设置CALayer的属性, 实际上是在定义当当前事务结果时候图层将如何显示的模型. Core Animation扮演一个控制器的角色, 并且负责根据图层行为和事务设置去不断更新视图的这些属性在屏幕上的状态.

在iOS中, 屏幕每秒钟别重绘60次. 如果动画时长比六十分之一秒要长, CoreAnimation就需要在设置新值和新值生效之间, 对屏幕图层进行重新组织, 就意味着, CALayer除了真实值, 之外, 必须谁知道当前显示在屏幕上的属性值的记录.

每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层中, 可以通过-presentationLayer方法来访问. 这个呈现图层实际上是模型图层的复制, 但是它的属性值, 代表了在任何指定时刻当前外观的效果. 换句话说, 你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值.

除了图层树, 还有呈现树, 呈现树是通过图层树的呈现图层形成的. 呈现树仅仅是图层首次被提交(第一次在屏幕上显示)的时候创建, 所以在那之前调用 -presentationLayer 将会返回nil.

一般情况下, 我们不需要直接访问呈现图层, 我们一般都通过和模型图层的交互让CoreAnimation更显显示. 但有时候还是需要用到呈现图层:

  1. 如果要实现一个基于定时器的动画, 而不仅仅是基于事务的动画, 这时候准确地知道某一时刻图层显示在什么位置就会对正确摆放图层很有用.
  2. 如果想让做动画的图层相应用户的输入, 可以使用图层的 -hitTest: 方法来判断图层是否被触摸, 这时候使用呈现图层而不是模型图层调用-hitTest:会更有意义, 因为呈现图层代表用户当前看到的位置, 而不是动画结束之后的位置.

小demo:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    CGPoint point = [[touches anyObject] locationInView:self.view];
    
    if ([self.moveLayer.presentationLayer hitTest:point]) {
        CGFloat red = arc4random() / (CGFloat)INT_MAX;
        CGFloat green = arc4random() / (CGFloat)INT_MAX;
        CGFloat blue = arc4random() / (CGFloat)INT_MAX;
        
        [CATransaction begin];
        [CATransaction setAnimationDuration:1];
        self.moveLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1].CGColor;
        [CATransaction commit];
    }else{
        [CATransaction begin];
        [CATransaction setAnimationDuration:4];
        self.moveLayer.position = point;
        [CATransaction commit];
    }
}

4. 总结

  1. 讨论了隐式动画, CoreAnimation对指定属性选择合适动画行为的机制.
  2. UIKit利用CoreAnimation的隐式动画机制强化显示系统, 以及动画如何被默认禁用和当需要时候启用.
  3. 了解了呈现图层和模型图层.
  4. CoreAnimation如何判断图层当前位置以及要到达的位置.

转载于:https://my.oschina.net/whforever/blog/731224

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值