隐式动画

我们在前面介绍了图层的相关知识,现在我们开始学本次的重点动画
注意
CALayer呈现图层<-->模型图层
我们在创建CALayer对象存储了我们设置的属性值(只是存值所以是模型图层),但是模型图层显示或者说是呈现时就会基于模型图层创建一个呈现图层,然后我们所能看到的都是基于这个呈现图层的.

事务

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

当我们改变了CALayer的一个可做动画的属性时,但是他并不会立刻在屏幕上显示出来。它默认会从先前的值平滑过渡到新值,不需要做额外操作。这就是隐式动画,之所以叫隐式是因为我们并没有指定任何动画的类型.我们改变属性之后是Core Animation来决定如何并且何时去做动画(这是隐式当然也可以显式动画).

隐式动画的动画执行的时间取决于当前事务的设置,动画类型取决于图层行为。

事务Core Animation用来包含一系列属性动画集合的机制,当用事务去改变可以做动画的图层属性都不会立刻变化,而是等到这个事务提交之后,才开始图层属性过渡到新值的动画.

我们用CATransaction类来管理事务,这个类管理的事务并不能访问,也没有属性或者实例方法,并且也不能用+alloc-init方法创建它。但是可以用 +begin+commit 分别来入栈或者出栈。

任何可以做动画的图层属性的改变都会被添加到目前栈顶事务中.我们可以用+setAnimationDuration:设置 或者+animationDuration获取当前的栈顶事务的动画时间(默认0.25秒)

Core Animation会在每个run loop周期中自动开始一次新的事务,,即使你不显式的用[CATransaction begin] 开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。

/*我们在设置某个属性动画的事务属性(比如时间)时,我们通常会自己创建一个新的事务,
否则会有可能影响到事务中其他属性动画*/
[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0];

self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
//commit the transaction
[CATransaction commit];

补充
UIView有两个方法,+beginAnimations:context:+commitAnimations,就是通过设置 CATransaction实现的,至于UIView的另外一个+animateWithDuration:animations:,只是将这些属性动画做成了一个block以避免开发者由于对+begin+commit 匹配的失误.

完成块

我们在做基于UIViewblock动画时可以在动画结束时提供一个完成动作.
CATranscation接口提供的+setCompletionBlock:方法也有同样的功能。

[CATransaction begin];
[CATransaction setAnimationDuration:1.0];
[CATransaction setCompletionBlock:^{
//rotate the layer 90 degrees
CGAffineTransform transform = self.colorLayer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
self.colorLayer.affineTransform = transform;
}];
self.colorLayer.backgroundColor = [UIColor redColor];
[CATransaction commit];
/**
注意旋转动画要比颜色渐变快得多,这是因为完成块是在颜色渐变的事务提交并出
栈之后才被执行,于是,用默认的事务做变换,默认的时间也就变成了0.25秒。
*/

图层行为

UIKit默认禁用了隐式动画.即UIView把它关联的图层属性改编动画特性关闭了

我们先来看一下隐式动画是怎么实现的:
我们把改变属性时CALayer自动应用的动画称作行为.
当我们改变了CALayer的属性时,会调用-actionForKey:方法,传递属性的名称。然后

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

-actionForKey:是用来找这个属性对应的动画(action),要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。

UIKit禁用隐式动画的方法就是,:每个UIView 对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey 的实现方法。当不在一个动画块的实现中,UIView 对所有图层行为返回 nil,但是在动画block范围之内,它就返回了一个非空值。

注意

  1. 这里所说的关联是指UIView关联的图层即宿主图层,而对于其他add的图层并不是关联的图层所以是没有禁用隐式动画的

当然对于CATransacition我们可以用+setDisableActions:来选择关闭/打开隐式动画
[CATransaction setDisableActions:YES]; //关闭隐式动画

  • UIView 关联的图层禁用了隐式动画,对这种图层做动画的办法就是使用 UIView 的动画函数(而不是依赖 CATransaction ),或者继承 UIView ,并覆盖-actionForLayer:forKey:方法,或者直接创建一个显式动画(具体细节见第八章)。
  • 对于单独存在的图层,我们可以通过实现图层的-actionForLayer:forKey:委托方法,或者提供一个actions 字典来控制隐式动画(actions 字典可以写更少的代码)。

行为通常是一个被Core Animation隐式调用的显式动画对象。
我们来改变一个图层属性的行为:

/**
我们使用的是一个实现了 CATransaction 的实例,叫做推进过渡
改变了CALayer的背景颜色属性行为.改变背景颜色,新的色块都是从左侧滑入,
而不是默认的渐变效果
*/
CATransition *transition = [CATransition animation];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
self.colorLayer.actions = @{@"backgroundColor": transition};
//add it to our view
[self.layerView.layer addSublayer:self.colorLayer];

呈现与模型

我们在改变一个CALayer的属性时,属性值确实会立即更新(这个属性值在设置时就会立刻生效)但是苹果并没有立即呈现出来。

当设置CALayer的属性,实际上是在定义当前事务结束之后图层如何显示的模型。而Core Animation则负责根据图层行为事务设置去更新视图的这些属性在屏幕上的状态.

我们的手机屏幕是每60秒重绘一次,如果我们要做的动画时长大于1/60,就需要在新值之前对屏幕重绘,即知道当前属性值.每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,可以通过-presentationLayer方法来访问.

图层树(模型图层树)<--->呈现树:呈现树通过图层树中所有图层的呈现图层所形成。只有在图层首次在屏幕上呈现时候才会被创建,在之前获取呈现图层时会返回nil的.

在模型图层上调用- presentationLayer能获取呈现图层,同样的调用–modelLayer是会获取模型图层(通常是self).

通常我们只需要和模型图层交互即可让Core Animation更新显示.
但是以下两种情况下呈现图层会很有效:

  • 实现一个基于定时器的动画,而不仅仅是基于事务的动画,这个时候准确地知道在某一时刻图层显示在什么位置就会对正确摆放图层很有用了。
  • 如果你想让你做动画的图层响应用户输入,你可以使用-hitTest:方法来判断指定图层是否被触摸,这时候对呈现图层而不是模型图层调用 -hitTest: 会显得更有意义
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //get the touch point
    CGPoint point = [[touches anyObject] locationInView:self.view];
    //check if we've tapped the moving layer
    if ([self.colorLayer.presentationLayer hitTest:point]) {
        self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
    } else {
        //otherwise (slowly) move the layer to new position
        [CATransaction begin];
        [CATransaction setAnimationDuration:4.0];
        self.colorLayer.position = point;
        [CATransaction commit];
    }
}