iOS核心动画高级技巧 - 3

7. 隐式动画

隐式动画

按照个人意思去作,而不是我说的。 -- 埃德娜,辛普森面试

咱们在第一部分讨论了Core Animation除了动画以外能够作到的任何事情。可是动画是Core Animation库一个很是显著的特性。这一章咱们来看看它是怎么作到的。具体来讲,咱们先来讨论框架自动完成的隐式动画(除非你明确禁用了这个功能)。框架

7.1 事务

事务

Core Animation基于一个假设,说屏幕上的任何东西均可以(或者可能)作动画。动画并不须要你在Core Animation中手动打开,相反须要明确地关闭,不然他会一直存在。当你改变CALayer的一个可作动画的属性,它并不能马上在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值。这一切都是默认的行为,你不须要作额外的操做。这看起来这太棒了,彷佛不太真实,咱们来用一个demo解释一下:首先和第一章“图层树”同样建立一个蓝色的方块,而后添加一个按钮,随机改变它的颜色。代码见清单7.1。点击按钮,你会发现图层的颜色平滑过渡到一个新值,而不是跳变(图7.1)。dom

一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个个人iOS交流群:1012951431, 分享BAT,阿里面试题、面试经验,讨论技术, 你们一块儿交流学习成长!但愿帮助开发者少走弯路。ide

清单7.1 随机改变图层颜色函数

复制代码

7.2 完成块

完成块

基于UIView的block的动画容许你在动画结束的时候提供一个完成的动做。CATranscation接口提供的+setCompletionBlock:方法也有一样的功能。咱们来调整上个例子,在颜色变化结束以后执行一些操做。咱们来添加一个完成以后的block,用来在每次颜色变化结束以后切换到另外一个旋转90的动画。代码见清单7.3,运行结果见图7.2。学习

清单7.3 在颜色动画完成以后添加一个回调测试

- (IBAction)changeColor
{
    //begin a new transaction
    [CATransaction begin];
    //set the animation duration to 1 second
    [CATransaction setAnimationDuration:1.0];
    //add the spin animation on completion
    [CATransaction setCompletionBlock:^{
        //rotate the layer 90 degrees
        CGAffineTransform transform = self.colorLayer.affineTransform;
        transform = CGAffineTransformRotate(transform, M_PI_2);
        self.colorLayer.affineTransform = transform;
    }];
    //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;
    //commit the transaction
    [CATransaction commit];
}

 

7.3 图层行为

图层行为

如今来作个实验,试着直接对UIView关联的图层作动画而不是一个单独的图层。清单7.4是对清单7.2代码的一点修改,移除了colorLayer,而且直接设置layerView关联图层的背景色。动画

清单7.4 直接设置图层的属性atom

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    //set the color of our layerView backing layer directly
    self.layerView.layer.backgroundColor = [UIColor blueColor].CGColor;
}

- (IBAction)changeColor
{
    //begin a new transaction
    [CATransaction begin];
    //set the animation duration to 1 second
    [CATransaction setAnimationDuration:1.0];
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.layerView.layer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    //commit the transaction
    [CATransaction commit];
}

 

运行程序,你会发现当按下按钮,图层颜色瞬间切换到新的值,而不是以前平滑过渡的动画。发生了什么呢?隐式动画好像被UIView关联图层给禁用了。试想一下,若是UIView的属性都有动画特性的话,那么不管在何时修改它,咱们都应该能注意到的。因此,若是说UIKit创建在Core Animation(默认对全部东西都作动画)之上,那么隐式动画是如何被UIKit禁用掉呢?咱们知道Core Animation一般对CALayer的全部属性(可动画的属性)作动画,可是UIView把它关联的图层的这个特性关闭了。为了更好说明这一点,咱们须要知道隐式动画是如何实现的。咱们把改变属性时CALayer自动应用的动画称做行为,当CALayer的属性被修改时候,它会调用-actionForKey:方法,传递属性的名称。剩下的操做都在CALayer的头文件中有详细的说明,实质上是以下几步:spa

  • 图层首先检测它是否有委托,而且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。若是有,直接调用并返回结果。

  • 若是没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。

  • 若是actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。

  • 最后,若是在style里面也找不到对应的行为,那么图层将会直接调用定义了每一个属性的标准行为的-defaultActionForKey:方法。

因此一轮完整的搜索结束以后,-actionForKey:要么返回空(这种状况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值作动画。因而这就解释了UIKit是如何禁用隐式动画的:每一个UIView对它关联的图层都扮演了一个委托,而且提供了-actionForLayer:forKey的实现方法。当不在一个动画块的实现中,UIView对全部图层行为返回nil,可是在动画block范围以内,它就返回了一个非空值。

咱们能够用一个demo作个简单的实验(清单7.5)

清单7.5 测试UIView的actionForLayer:forKey:实现

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    //test layer action when outside of animation block
    NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
    //begin animation block
    [UIView beginAnimations:nil context:nil];
    //test layer action when inside of animation block
    NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
    //end animation block
    [UIView commitAnimations];
}

@end

 

运行程序,控制台显示结果以下:

$ LayerTest[21215:c07] Outside: <null>
$ LayerTest[21215:c07] Inside: <CABasicAnimation: 0x757f090>

 

因而咱们能够预言,当属性在动画块以外发生改变,UIView直接经过返回nil来禁用隐式动画。但若是在动画块范围以内,根据动画具体类型返回相应的属性,在这个例子就是CABasicAnimation(第八章“显式动画”将会提到)。固然返回nil并非禁用隐式动画惟一的办法,CATransacition有个方法叫作+setDisableActions:,能够用来对全部属性打开或者关闭隐式动画。若是在清单7.2的[CATransaction begin]以后添加下面的代码,一样也会阻止动画的发生:

复制代码[CATransaction setDisableActions:YES];

总结一下,咱们知道了以下几点

  • UIView关联的图层禁用了隐式动画,对这种图层作动画的惟一办法就是使用UIView的动画函数(而不是依赖CATransaction),或者继承UIView,并覆盖-actionForLayer:forKey:方法,或者直接建立一个显式动画(具体细节见第八章)。
  • 对于单独存在的图层,咱们能够经过实现图层的-actionForLayer:forKey:委托方法,或者提供一个actions字典来控制隐式动画。 咱们来对颜色渐变的例子使用一个不一样的行为,经过给colorLayer设置一个自定义的actions字典。咱们也可使用委托来实现,可是actions字典能够写更少的代码。那么到底改如何建立一个合适的行为对象呢?

行为一般是一个被Core Animation隐式调用的显式动画对象。这里咱们使用的是一个实现了CATransaction的实例,叫作推动过渡。

第八章中将会详细解释过渡,不过对于如今,知道CATransition响应CAAction协议,而且能够当作一个图层行为就足够了。结果很赞,不论在何时改变背景颜色,新的色块都是从左侧滑入,而不是默认的渐变效果。

清单7.6 实现自定义行为

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView;
@property (nonatomic, weak) IBOutlet CALayer *colorLayer;/*热心人发现这里应该改成@property (nonatomic, strong)  CALayer *colorLayer;不然运行结果不正确。
*/

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    //create sublayer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
    self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
    //add a custom action
    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];
}

- (IBAction)changeColor
{
    //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;
}

@end

 

 

7.4 呈现与模型

呈现与模型

CALayer的属性行为其实很不正常,由于改变一个图层的属性并无马上生效,而是经过一段时间渐变动新。这是怎么作到的呢?当你改变一个图层的属性,属性值的确是马上更新的(若是你读取它的数据,你会发现它的值在你设置它的那一刻就已经生效了),可是屏幕上并无立刻发生改变。这是由于你设置的属性并无直接调整图层的外观,相反,他只是定义了图层动画结束以后将要变化的外观。当设置CALayer的属性,其实是在定义当前事务结束以后图层如何显示的模型。Core Animation扮演了一个控制器的角色,而且负责根据图层行为和事务设置去不断更新视图的这些属性在屏幕上的状态。咱们讨论的就是一个典型的微型MVC模式。CALayer是一个链接用户界面(就是MVC中的view)虚构的类,可是在界面自己这个场景下,CALayer的行为更像是存储了视图如何显示和动画的数据模型。实际上,在苹果本身的文档中,图层树一般都是值的图层树模型。在iOS中,屏幕每秒钟重绘60次。若是动画时长比60分之一秒要长,Core Animation就须要在设置一次新值和新值生效之间,对屏幕上的图层进行从新组织。这意味着CALayer除了“真实”值(就是你设置的值)以外,必需要知道当前显示在屏幕上的属性值的记录。

每一个图层属性的显示值都被存储在一个叫作呈现图层的独立图层当中,他能够经过-presentationLayer方法来访问。这个呈现图层其实是模型图层的复制,可是它的属性值表明了在任何指定时刻当前外观效果。换句话说,你能够经过呈现图层的值来获取当前屏幕上真正显示出来的值(图7.4)。咱们在第一章中提到除了图层树,另外还有呈现树。呈现树经过图层树中全部图层的呈现图层所造成。注意呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候建立,因此在那以前调用-presentationLayer将会返回nil。你可能注意到有一个叫作–modelLayer的方法。在呈现图层上调用–modelLayer将会返回它正在呈现所依赖的 CALayer。一般在一个图层上调用-modelLayer会返回–self(实际上咱们已经建立的原始图层就是一种数据模型)。

7.5 总结

总结

这一章讨论了隐式动画,还有Core Animation对指定属性选择合适的动画行为的机制。同时你知道了UIKit是如何充分利用Core Animation的隐式动画机制来强化它的显式系统,以及动画是如何被默认禁用而且当须要的时候启用的。最后,你了解了呈现和模型图层,以及Core Animation是如何经过它们来判断出图层当前位置以及将要到达的位置。

在下一章中,咱们将研究Core Animation提供的显式动画类型,既能够直接对图层属性作动画,也能够覆盖默认的图层行为。

相关文章
相关标签/搜索