iOS Core Animation Advanced Techniques(四):隐式动画和显式动画

隐式动画
数组

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

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

事务app

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

当你改变CALayer的一个可作动画的属性,它并不能马上在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值。这一切都是默认的行为,你不须要作额外的操做。dom

这看起来这太棒了,彷佛不太真实,咱们来用一个demo解释一下:首先和第一章“图层树”同样建立一个蓝色的方块,而后添加一个按钮,随机改变它的颜色。代码见清单7.1。点击按钮,你会发现图层的颜色平滑过渡到一个新值,而不是跳变(图7.1)。ide

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

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@property (nonatomic, weak) IBOutlet 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 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.1.jpeg

图7.1 添加一个按钮来控制图层颜色工具

这其实就是所谓的隐式动画。之因此叫隐式是由于咱们并无指定任何动画的类型。咱们仅仅改变了一个属性,而后Core Animation来决定如何而且什么时候去作动画。Core Animaiton一样支持显式动画,下章详细说明。oop

但当你改变一个属性,Core Animation是如何判断动画类型和持续时间的呢?实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为。

事务其实是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变能够作动画的图层属性都不会马上发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。

事务是经过CATransaction类来作管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,而且也不能用+alloc和-init方法建立它。可是能够用+begin和+commit分别来入栈或者出栈。

任何能够作动画的图层属性都会被添加到栈顶的事务,你能够经过+setAnimationDuration:方法设置当前事务的动画时间,或者经过+animationDuration方法来获取值(默认0.25秒)。

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

明白这些以后,咱们就能够轻松修改变色动画的时间了。咱们固然能够用当前事务的+setAnimationDuration:方法来修改动画时间,但在这里咱们首先起一个新的事务,因而修改时间就不会有别的反作用。由于修改当前事务的时间可能会致使同一时刻别的动画(如屏幕旋转),因此最好仍是在调整动画以前压入一个新的事务。

修改后的代码见清单7.2。运行程序,你会发现色块颜色比以前变得更慢了。

清单7.2 使用CATransaction控制动画时间

- (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.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    ?//commit the transaction
    [CATransaction commit];
}

若是你用过UIView的动画方法作过一些动画效果,那么应该对这个模式不陌生。UIView有两个方法,+beginAnimations:context:和+commitAnimations,和CATransaction的+begin和+commit方法相似。实际上在+beginAnimations:context:和+commitAnimations之间全部视图或者图层属性的改变而作的动画都是因为设置了CATransaction的缘由。

在iOS4中,苹果对UIView添加了一种基于block的动画方法:+animateWithDuration:animations:。这样写对作一堆的属性动画在语法上会更加简单,但实质上它们都是在作一样的事情。

CATransaction的+begin和+commit方法在+animateWithDuration:animations:内部自动调用,这样block中全部属性的改变都会被事务所包含。这样也能够避免开发者因为对+begin和+commit匹配的失误形成的风险。

完成块

基于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.2.jpeg

图7.2 颜色渐变之完成以后再作一次旋转

注意旋转动画要比颜色渐变快得多,这是由于完成块是在颜色渐变的事务提交并出栈以后才被执行,因而,用默认的事务作变换,默认的时间也就变成了0.25秒。

图层行为

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

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

@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的头文件中有详细的说明,实质上是以下几步:

  • 图层首先检测它是否有委托,而且是否实现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: $ LayerTest[21215:c07] Inside:

因而咱们能够预言,当属性在动画块以外发生改变,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;
@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.3.jpeg

图7.3 使用推动过渡的色值动画

呈现与模型

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.4.jpeg

图7.4 一个移动的图层是如何经过数据模型呈现的

大多数状况下,你不须要直接访问呈现图层,你能够经过和模型图层的交互,来让Core Animation更新显示。两种状况下呈现图层会变得颇有用,一个是同步动画,一个是处理用户交互。

  • 若是你在实现一个基于定时器的动画(见第11章“基于定时器的动画”),而不只仅是基于事务的动画,这个时候准确地知道在某一时刻图层显示在什么位置就会对正确摆放图层颇有用了。

  • 若是你想让你作动画的图层响应用户输入,你可使用-hitTest:方法(见第三章“图层几何学”)来判断指定图层是否被触摸,这时候对呈现图层而不是模型图层调用-hitTest:会显得更有意义,由于呈现图层表明了用户当前看到的图层位置,而不是当前动画结束以后的位置。

咱们能够用一个简单的案例来证实后者(见清单7.7)。在这个例子中,点击屏幕上的任意位置将会让图层平移到那里。点击图层自己能够随机改变它的颜色。咱们经过对呈现图层调用-hitTest:来判断是否被点击。

若是修改代码让-hitTest:直接做用于colorLayer而不是呈现图层,你会发现当图层移动的时候它并不能正确显示。这时候你就须要点击图层将要移动到的位置而不是图层自己来响应点击(这就是为何用呈现图层来响应交互的缘由)。

清单7.7 使用presentationLayer图层来判断当前图层位置

@interface ViewController ()
@property (nonatomic, strong) CALayer *colorLayer;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a red layer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(0, 0, 100, 100);
    self.colorLayer.position = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
    self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
    [self.view.layer addSublayer:self.colorLayer];
}
- (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]) {
        //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;
    } else {
        //otherwise (slowly) move the layer to new position
        [CATransaction begin];
        [CATransaction setAnimationDuration:4.0];
        self.colorLayer.position = point;
        [CATransaction commit];
    }
}
@end

总结

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

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

若是想让事情变得顺利,只有靠本身 -- 夏尔·纪尧姆

上一章介绍了隐式动画的概念。隐式动画是在iOS平台建立动态用户界面的一种直接方式,也是UIKit动画机制的基础,不过它并不能涵盖全部的动画类型。在这一章中,咱们将要研究一下显式动画,它可以对一些属性作指定的自定义动画,或者建立非线性动画,好比沿着任意一条曲线移动。

属性动画

首先咱们来探讨一下属性动画。属性动画做用于图层的某个单一属性,并指定了它的一个目标值,或者一连串将要作动画的值。属性动画分为两种:基础和关键帧。

基础动画

动画其实就是一段时间内发生的改变,最简单的形式就是从一个值改变到另外一个值,这也是CABasicAnimation最主要的功能。CABasicAnimation是CAPropertyAnimation的一个子类,CAPropertyAnimation同时也是Core Animation全部动画类型的抽象基类。做为一个抽象类,CAAnimation自己并无作多少工做,它提供了一个计时函数(见第十章“缓冲”),一个委托(用于反馈动画状态)以及一个removedOnCompletion,用于标识动画是否该在结束后自动释放(默认YES,为了防止内存泄露)。CAAnimation同时实现了一些协议,包括CAAction(容许CAAnimation的子类能够提供图层行为),以及CAMediaTiming(第九章“图层时间”将会详细解释)。

CAPropertyAnimation经过指定动画的keyPath做用于一个单一属性,CAAnimation一般应用于一个指定的CALayer,因而这里指的也就是一个图层的keyPath了。实际上它是一个关键路径(一些用点表示法能够在层级关系中指向任意嵌套的对象),而不只仅是一个属性的名称,由于这意味着动画不只能够做用于图层自己的属性,并且还包含了它的子成员的属性,甚至是一些虚拟的属性(后面会详细解释)。

CABasicAnimation继承于CAPropertyAnimation,并添加了以下属性:

id fromValue 
id toValue 
id byValue

从命名就能够获得很好的解释:fromValue表明了动画开始以前属性的值,toValue表明了动画结束以后的值,byValue表明了动画执行过程当中改变的值。

经过组合这三个属性就能够有不少种方式来指定一个动画的过程。它们被定义成id类型而不是一些具体的类型是由于属性动画能够用做不少不一样种的属性类型,包括数字类型,矢量,变换矩阵,甚至是颜色或者图片。

id类型能够包含任意由NSObject派生的对象,但有时候你会但愿对一些不直接从NSObject继承的属性类型作动画,这意味着你须要把这些值用一个对象来封装,或者强转成一个对象,就像某些功能和Objective-C对象相似的Core Foundation类型。可是如何从一个具体的数据类型转换成id看起来并不明显,一些普通的例子见表8.1。

表8.1 用于CAPropertyAnimation的一些类型转换

56.jpg

fromValue,toValue和byValue属性能够用不少种方式来组合,但为了防止冲突,不能一次性同时指定这三个值。例如,若是指定了fromValue等于2,toValue等于4,byValue等于3,那么Core Animation就不知道结果究竟是4(toValue)仍是5(fromValue + byValue)了。他们的用法在CABasicAnimation头文件中已经描述的很清楚了,因此在这里就不重复了。总的说来,就是只须要指定toValue或者byValue,剩下的值均可以经过上下文自动计算出来。

举个例子:咱们修改一下第七章中的颜色渐变的动画,用显式的CABasicAnimation来取代以前的隐式动画,代码见清单8.1。

清单8.1 经过CABasicAnimation来设置图层背景色

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@property (nonatomic, strong) IBOutlet 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 it to our view
    [self.layerView.layer addSublayer:self.colorLayer];
}
- (IBAction)changeColor
{
    ?//create a new random color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
    //create a basic animation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"backgroundColor";
    animation.toValue = (__bridge id)color.CGColor;
    //apply animation to layer
    [self.colorLayer addAnimation:animation forKey:nil];
}
@end

运行程序,结果有点差强人意,点击按钮,的确可使图层动画过渡到一个新的颜色,然动画结束以后又马上变回原始值。

这是由于动画并无改变图层的模型,而只是呈现(第七章)。一旦动画结束并从图层上移除以后,图层就马上恢复到以前定义的外观状态。咱们从没改变过backgroundColor属性,因此图层就返回到原始的颜色。

当以前在使用隐式动画的时候,实际上它就是用例子中CABasicAnimation来实现的(回忆第七章,咱们在-actionForLayer:forKey:委托方法打印出来的结果就是CABasicAnimation)。可是在那个例子中,咱们经过设置属性来打开动画。在这里咱们作了相同的动画,可是并无设置任何属性的值(这就是为何会马上变回初始状态的缘由)。

把动画设置成一个图层的行为(而后经过改变属性值来启动动画)是到目前为止同步属性值和动画状态最简单的方式了,假设因为某些缘由咱们不能这么作(一般由于UIView关联的图层不能这么作动画),那么有两种能够更新属性值的方式:在动画开始以前或者动画结束以后。

动画以前改变属性的值是最简单的办法,但这意味着咱们不能使用fromValue这么好的特性了,并且要手动将fromValue设置成图层当前的值。

因而在动画建立以前插入以下代码,就能够解决问题了

animation.fromValue = (__bridge id)self.colorLayer.backgroundColor; 
self.colorLayer.backgroundColor = color.CGColor;

这的确是可行的,但仍是有些问题,若是这里已经正在进行一段动画,咱们须要从呈现图层那里去得到fromValue,而不是模型图层。另外,因为这里的图层并非UIView关联的图层,咱们须要用CATransaction来禁用隐式动画行为,不然默认的图层行为会干扰咱们的显式动画(实际上,显式动画一般会覆盖隐式动画,但在文章中并无提到,因此为了安全最好这么作)。

更新以后的代码以下:

CALayer *layer = self.colorLayer.presentationLayer ?:
self.colorLayer;
 animation.fromValue = (__bridge id)layer.backgroundColor;
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.colorLayer.backgroundColor = color.CGColor;
[CATransaction commit];

若是给每一个动画都添加这些,代码会显得特别臃肿。幸运的是,咱们能够从CABasicAnimation去自动设置这些。因而能够建立一个可复用的代码。清单8.2修改了以前的示例,经过使用CABasicAnimation的一个函数来避免在每次动画时候都重复那些臃肿的代码。

清单8.2 修改动画马上恢复到原始状态的一个可复用函数

- (void)applyBasicAnimation:(CABasicAnimation *)animation toLayer:(CALayer *)layer
{
    //set the from value (using presentation layer if available)
    animation.fromValue = [layer.presentationLayer ?: layer valueForKeyPath:animation.keyPath];
    //update the property in advance
    //note: this approach will only work if toValue != nil 
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    [layer setValue:animation.toValue forKeyPath:animation.keyPath];
    [CATransaction commit];
    //apply animation to layer
    [layer addAnimation:animation forKey:nil];
}
- (IBAction)changeColor
{
    //create a new random color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
    //create a basic animation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"backgroundColor";
    animation.toValue = (__bridge id)color.CGColor;
    //apply animation without snap-back
    [self applyBasicAnimation:animation toLayer:self.colorLayer];
}

这种简单的实现方式经过toValue而不是byValue来处理动画,不过这已是朝更好的解决方案迈出一大步了。你能够把它添加给CALaye做为一个分类,以方便更好地使用。

解决看起来如此简单的一个问题都着实麻烦,可是别的方案会更加复杂。若是不在动画开始以前去更新目标属性,那么就只能在动画彻底结束或者取消的时候更新它。这意味着咱们须要精准地在动画结束以后,图层返回到原始值以前更新属性。那么该如何找到这个点呢?

CAAnimationDelegate

在第七章使用隐式动画的时候,咱们能够在CATransaction完成块中检测到动画的完成。可是这种方式并不适用于显式动画,由于这里的动画和事务并没太多关联。

那么为了知道一个显式动画在什么时候结束,咱们须要使用一个实现了CAAnimationDelegate协议的delegate。

CAAnimationDelegate在任何头文件中都找不到,可是能够在CAAnimation头文件或者苹果开发者文档中找到相关函数。在这个例子中,咱们用-animationDidStop:finished:方法在动画结束以后来更新图层的backgroundColor。

当更新属性的时候,咱们须要设置一个新的事务,而且禁用图层行为。不然动画会发生两次,一个是由于显式的CABasicAnimation,另外一次是由于隐式动画,具体实现见订单8.3。

清单8.3 动画完成以后修改图层的背景色

@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 it to our view
    [self.layerView.layer addSublayer:self.colorLayer];
}
- (IBAction)changeColor
{
    //create a new random color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
    //create a basic animation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"backgroundColor";
    animation.toValue = (__bridge id)color.CGColor;
    animation.delegate = self;
    //apply animation to layer
    [self.colorLayer addAnimation:animation forKey:nil];
}
- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag
{
    //set the backgroundColor property to match animation toValue
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    self.colorLayer.backgroundColor = (__bridge CGColorRef)anim.toValue;
    [CATransaction commit];
}
@end

对CAAnimation而言,使用委托模式而不是一个完成块会带来一个问题,就是当你有多个动画的时候,没法在在回调方法中区分。在一个视图控制器中建立动画的时候,一般会用控制器自己做为一个委托(如清单8.3所示),可是全部的动画都会调用同一个回调方法,因此你就须要判断究竟是那个图层的调用。

考虑一下第三章的闹钟,“图层几何学”,咱们经过简单地每秒更新指针的角度来实现一个钟,但若是指针动态地转向新的位置会更加真实。

咱们不能经过隐式动画来实现由于这些指针都是UIView的实例,因此图层的隐式动画都被禁用了。咱们能够简单地经过UIView的动画方法来实现。但若是想更好地控制动画时间,使用显式动画会更好(更多内容见第十章)。使用CABasicAnimation来作动画可能会更加复杂,由于咱们须要在-animationDidStop:finished:中检测指针状态(用于设置结束的位置)。

动画自己会做为一个参数传入委托的方法,也许你会认为能够控制器中把动画存储为一个属性,而后在回调用比较,但实际上并不起做用,由于委托传入的动画参数是原始值的一个深拷贝,从而不是同一个值。

当使用-addAnimation:forKey:把动画添加到图层,这里有一个到目前为止咱们都设置为nil的key参数。这里的键是-animationForKey:方法找到对应动画的惟一标识符,而当前动画的全部键均可以用animationKeys获取。若是咱们对每一个动画都关联一个惟一的键,就能够对每一个图层循环全部键,而后调用-animationForKey:来比对结果。尽管这不是一个优雅的实现。

幸运的是,还有一种更加简单的方法。像全部的NSObject子类同样,CAAnimation实现了KVC(键-值-编码)协议,因而你能够用-setValue:forKey:和-valueForKey:方法来存取属性。可是CAAnimation有一个不一样的性能:它更像一个NSDictionary,可让你随意设置键值对,即便和你使用的动画类所声明的属性并不匹配。

这意味着你能够对动画用任意类型打标签。在这里,咱们给UIView类型的指针添加的动画,因此能够简单地判断动画到底属于哪一个视图,而后在委托方法中用这个信息正确地更新钟的指针(清单8.4)。

清单8.4 使用KVC对动画打标签

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *hourHand;
@property (nonatomic, weak) IBOutlet UIImageView *minuteHand;
@property (nonatomic, weak) IBOutlet UIImageView *secondHand;
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //adjust anchor points
    self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
    self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
    self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
    //start timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
    //set initial hand positions
    [self updateHandsAnimated:NO];
}
- (void)tick
{
    [self updateHandsAnimated:YES];
}
- (void)updateHandsAnimated:(BOOL)animated
{
    //convert time to hours, minutes and seconds
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
    NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
    CGFloat hourAngle = (components.hour / 12.0) * M_PI * 2.0;
    //calculate hour hand angle //calculate minute hand angle
    CGFloat minuteAngle = (components.minute / 60.0) * M_PI * 2.0;
    //calculate second hand angle
    CGFloat secondAngle = (components.second / 60.0) * M_PI * 2.0;
    //rotate hands
    [self setAngle:hourAngle forHand:self.hourHand animated:animated];
    [self setAngle:minuteAngle forHand:self.minuteHand animated:animated];
    [self setAngle:secondAngle forHand:self.secondHand animated:animated];
}
- (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated
{
    //generate transform
    CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1);
    if (animated) {
        //create transform animation
        CABasicAnimation *animation = [CABasicAnimation animation];
        [self updateHandsAnimated:NO];
        animation.keyPath = @"transform";
        animation.toValue = [NSValue valueWithCATransform3D:transform];
        animation.duration = 0.5;
        animation.delegate = self;
        [animation setValue:handView forKey:@"handView"];
        [handView.layer addAnimation:animation forKey:nil];
    } else {
        //set transform directly
        handView.layer.transform = transform;
    }
}
- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag
{
    //set final position for hand view
    UIView *handView = [anim valueForKey:@"handView"];
    handView.layer.transform = [anim.toValue CATransform3DValue];
}

咱们成功的识别出每一个图层中止动画的时间,而后更新它的变换到一个新值,很好。

不幸的是,即便作了这些,仍是有个问题,清单8.4在模拟器上运行的很好,但当真正跑在iOS设备上时,咱们发如今-animationDidStop:finished:委托方法调用以前,指针会迅速返回到原始值,这个清单8.3图层颜色发生的状况同样。

问题在于回调方法在动画完成以前已经被调用了,但不能保证这发生在属性动画返回初始状态以前。这同时也很好地说明了为何要在真实的设备上测试动画代码,而不只仅是模拟器。

咱们能够用一个fillMode属性来解决这个问题,下一章会详细说明,这里知道在动画以前设置它比在动画结束以后更新属性更加方便。

关键帧动画

CABasicAnimation揭示了大多数隐式动画背后依赖的机制,这的确颇有趣,可是显式地给图层添加CABasicAnimation相较于隐式动画而言,只能说费力不讨好。

CAKeyframeAnimation是另外一种UIKit没有暴露出来但功能强大的类。和CABasicAnimation相似,CAKeyframeAnimation一样是CAPropertyAnimation的一个子类,它依然做用于单一的一个属性,可是和CABasicAnimation不同的是,它不限制于设置一个起始和结束的值,而是能够根据一连串随意的值来作动画。

关键帧起源于传动动画,意思是指主导的动画在显著改变发生时重绘当前帧(也就是关键帧),每帧之间剩下的绘制(能够经过关键帧推算出)将由熟练的艺术家来完成。CAKeyframeAnimation也是一样的道理:你提供了显著的帧,而后Core Animation在每帧之间进行插入。

咱们能够用以前使用颜色图层的例子来演示,设置一个颜色的数组,而后经过关键帧动画播放出来(清单8.5)

清单8.5 使用CAKeyframeAnimation应用一系列颜色的变化

- (IBAction)changeColor
{
    //create a keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"backgroundColor";
    animation.duration = 2.0;
    animation.values = @[
                         (__bridge id)[UIColor blueColor].CGColor,
                         (__bridge id)[UIColor redColor].CGColor,
                         (__bridge id)[UIColor greenColor].CGColor,
                         (__bridge id)[UIColor blueColor].CGColor ];
    //apply animation to layer
    [self.colorLayer addAnimation:animation forKey:nil];
}

注意到序列中开始和结束的颜色都是蓝色,这是由于CAKeyframeAnimation并不能自动把当前值做为第一帧(就像CABasicAnimation那样把fromValue设为nil)。动画会在开始的时候忽然跳转到第一帧的值,而后在动画结束的时候忽然恢复到原始的值。因此为了动画的平滑特性,咱们须要开始和结束的关键帧来匹配当前属性的值。

固然能够建立一个结束和开始值不一样的动画,那样的话就须要在动画启动以前手动更新属性和最后一帧的值保持一致,就和以前讨论的同样。

咱们用duration属性把动画时间从默认的0.25秒增长到2秒,以便于动画作的不那么快。运行它,你会发现动画经过颜色不断循环,但效果看起来有些奇怪。缘由在于动画以一个恒定的步调在运行。当在每一个动画之间过渡的时候并无减速,这就产生了一个略微奇怪的效果,为了让动画看起来更天然,咱们须要调整一下缓冲,第十章将会详细说明。

提供一个数组的值就能够按照颜色变化作动画,但通常这不是直观的方式去描述一段运用。CAKeyframeAnimation有另外一种方式去指定动画,就是使用CGPath。path属性能够用一种直观的方式,使用Core Graphics函数定义运动的序列来绘制动画。

咱们来用一个宇宙飞船沿着一个简单曲线的实例演示一下。为了建立路径,咱们须要使用一个三次贝塞尔曲线,它是一种使用开始点,结束点和另外两个控制点来定义形状的曲线,能够经过使用一个基于C的Core Graphics绘图指令来建立,不过用UIKit提供的UIBezierPath类会更简单。

咱们此次用CAShapeLayer来在屏幕上绘制曲线,尽管对动画来讲并非必须的,但这会让咱们的动画更加形象。绘制完CGPath以后,咱们用它来建立一个CAKeyframeAnimation,而后用它来应用到咱们的宇宙飞船。代码见清单8.6,结果见图8.1。

清单8.6 沿着一个贝塞尔曲线对图层作动画

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a path
    UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
    [bezierPath moveToPoint:CGPointMake(0, 150)];
    [bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
    //draw the path using a CAShapeLayer
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.containerView.layer addSublayer:pathLayer];
    //add the ship
    CALayer *shipLayer = [CALayer layer];
    shipLayer.frame = CGRectMake(0, 0, 64, 64);
    shipLayer.position = CGPointMake(0, 150);
    shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:shipLayer];
    //create the keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 4.0;
    animation.path = bezierPath.CGPath;
    [shipLayer addAnimation:animation forKey:nil];
}
@end

8.1.jpeg

图8.1 沿着一个贝塞尔曲线移动的宇宙飞船图片

运行示例,你会发现飞船的动画有些不太真实,这是由于当它运动的时候永远指向右边,而不是指向曲线切线的方向。你能够调整它的affineTransform来对运动方向作动画,但极可能和其它的动画冲突。

幸运的是,苹果预见到了这点,而且给CAKeyFrameAnimation添加了一个rotationMode的属性。设置它为常量kCAAnimationRotateAuto(清单8.7),图层将会根据曲线的切线自动旋转(图8.2)。

清单8.7 经过rotationMode自动对齐图层到曲线

- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a path
    ...
    //create the keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 4.0;
    animation.path = bezierPath.CGPath;
    animation.rotationMode = kCAAnimationRotateAuto;
    [shipLayer addAnimation:animation forKey:nil];
}

8.2.jpeg

图8.2 匹配曲线切线方向的飞船图层

虚拟属性

以前提到过属性动画其实是针对于关键路径而不是一个键,这就意味着能够对子属性甚至是虚拟属性作动画。可是虚拟属性究竟是什么呢?

考虑一个旋转的动画:若是想要对一个物体作旋转的动画,那就须要做用于transform属性,由于CALayer没有显式提供角度或者方向之类的属性,代码如清单8.8所示

清单8.8 用transform属性对图层作动画

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //add the ship
    CALayer *shipLayer = [CALayer layer];
    shipLayer.frame = CGRectMake(0, 0, 128, 128);
    shipLayer.position = CGPointMake(150, 150);
    shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:shipLayer];
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform";
    animation.duration = 2.0;
    animation.toValue = [NSValue valueWithCATransform3D: CATransform3DMakeRotation(M_PI, 0, 0, 1)];
    [shipLayer addAnimation:animation forKey:nil];
}
@end

这么作是可行的,但看起来更由于是运气而不是设计的缘由,若是咱们把旋转的值从M_PI(180度)调整到2 * M_PI(360度),而后运行程序,会发现这时候飞船彻底不动了。这是由于这里的矩阵作了一次360度的旋转,和作了0度是同样的,因此最后的值根本没变。

如今继续使用M_PI,但此次用byValue而不是toValue。也许你会认为这和设置toValue结果同样,由于0 + 90度 == 90度,但实际上飞船的图片变大了,并无作任何旋转,这是由于变换矩阵不能像角度值那样叠加。

那么若是须要独立于角度以外单独对平移或者缩放作动画呢?因为都须要咱们来修改transform属性,实时地从新计算每一个时间点的每一个变换效果,而后根据这些建立一个复杂的关键帧动画,这一切都是为了对图层的一个独立作一个简单的动画。

幸运的是,有一个更好的解决方案:为了旋转图层,咱们能够对transform.rotation关键路径应用动画,而不是transform自己(清单8.9)。

清单8.9 对虚拟的transform.rotation属性作动画

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //add the ship
    CALayer *shipLayer = [CALayer layer];
    shipLayer.frame = CGRectMake(0, 0, 128, 128);
    shipLayer.position = CGPointMake(150, 150);
    shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:shipLayer];
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation";
    animation.duration = 2.0;
    animation.byValue = @(M_PI * 2);
    [shipLayer addAnimation:animation forKey:nil];
}
@end

结果运行的特别好,用transform.rotation而不是transform作动画的好处以下:

  • 咱们能够不经过关键帧一步旋转多于180度的动画。

  • 能够用相对值而不是绝对值旋转(设置byValue而不是toValue)。

  • 能够不用建立CATransform3D,而是使用一个简单的数值来指定角度。

  • 不会和transform.position或者transform.scale冲突(一样是使用关键路径来作独立的动画属性)。

transform.rotation属性有一个奇怪的问题是它其实并不存在。这是由于CATransform3D并非一个对象,它其实是一个结构体,也没有符合KVC相关属性,transform.rotation其实是一个CALayer用于处理动画变换的虚拟属性。

你不能够直接设置transform.rotation或者transform.scale,他们不能被直接使用。当你对他们作动画时,Core Animation自动地根据经过CAValueFunction来计算的值来更新transform属性。

CAValueFunction用于把咱们赋给虚拟的transform.rotation简单浮点值转换成真正的用于摆放图层的CATransform3D矩阵值。你能够经过设置CAPropertyAnimation的valueFunction属性来改变,因而你设置的函数将会覆盖默认的函数。

CAValueFunction看起来彷佛是对那些不能简单相加的属性(例如变换矩阵)作动画的很是有用的机制,但因为CAValueFunction的实现细节是私有的,因此目前不能经过继承它来自定义。你能够经过使用苹果目前已近提供的常量(目前都是和变换矩阵的虚拟属性相关,因此没太多使用场景了,由于这些属性都有了默认的实现方式)。

动画组

CABasicAnimation和CAKeyframeAnimation仅仅做用于单独的属性,而CAAnimationGroup能够把这些动画组合在一块儿。CAAnimationGroup是另外一个继承于CAAnimation的子类,它添加了一个animations数组的属性,用来组合别的动画。咱们把清单8.6那种关键帧动画和调整图层背景色的基础动画组合起来(清单8.10),结果如图8.3所示。

清单8.10 组合关键帧动画和基础动画

- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a path
    UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
    [bezierPath moveToPoint:CGPointMake(0, 150)];
    [bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
    //draw the path using a CAShapeLayer
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.containerView.layer addSublayer:pathLayer];
    //add a colored layer
    CALayer *colorLayer = [CALayer layer];
    colorLayer.frame = CGRectMake(0, 0, 64, 64);
    colorLayer.position = CGPointMake(0, 150);
    colorLayer.backgroundColor = [UIColor greenColor].CGColor;
    [self.containerView.layer addSublayer:colorLayer];
    //create the position animation
    CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animation];
    animation1.keyPath = @"position";
    animation1.path = bezierPath.CGPath;
    animation1.rotationMode = kCAAnimationRotateAuto;
    //create the color animation
    CABasicAnimation *animation2 = [CABasicAnimation animation];
    animation2.keyPath = @"backgroundColor";
    animation2.toValue = (__bridge id)[UIColor redColor].CGColor;
    //create group animation
    CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
    groupAnimation.animations = @[animation1, animation2]; 
    groupAnimation.duration = 4.0;
    //add the animation to the color layer
    [colorLayer addAnimation:groupAnimation forKey:nil];
}

8.3.jpeg

图8.3 关键帧路径和基础动画的组合

过渡

有时候对于iOS应用程序来讲,但愿能经过属性动画来对比较难作动画的布局进行一些改变。好比交换一段文本和图片,或者用一段网格视图来替换,等等。属性动画只对图层的可动画属性起做用,因此若是要改变一个不能动画的属性(好比图片),或者从层级关系中添加或者移除图层,属性动画将不起做用。

因而就有了过渡的概念。过渡并不像属性动画那样平滑地在两个值之间作动画,而是影响到整个图层的变化。过渡动画首先展现以前的图层外观,而后经过一个交换过渡到新的外观。

为了建立一个过渡动画,咱们将使用CATransition,一样是另外一个CAAnimation的子类,和别的子类不一样,CAAnimation有一个type和subtype来标识变换效果。type属性是一个NSString类型,能够被设置成以下类型:

kCATransitionFade 
kCATransitionMoveIn 
kCATransitionPush 
kCATransitionReveal

到目前为止你只能使用上述四种类型,但你能够经过一些别的方法来自定义过渡效果,后续会详细介绍。

默认的过渡类型是kCATransitionFade,当你在改变图层属性以后,就建立了一个平滑的淡入淡出效果。

咱们在第七章的例子中就已经用到过kCATransitionPush,它建立了一个新的图层,从边缘的一侧滑动进来,把旧图层从另外一侧推出去的效果。

kCATransitionMoveIn和kCATransitionReveal与kCATransitionPush相似,都实现了一个定向滑动的动画,可是有一些细微的不一样,kCATransitionMoveIn从顶部滑动进入,但不像推送动画那样把老土层推走,然而kCATransitionReveal把原始的图层滑动出去来显示新的外观,而不是把新的图层滑动进入。

后面三种过渡类型都有一个默认的动画方向,它们都从左侧滑入,可是你能够经过subtype来控制它们的方向,提供了以下四种类型:

kCATransitionFromRight 
kCATransitionFromLeft 
kCATransitionFromTop 
kCATransitionFromBottom

一个简单的用CATransition来对非动画属性作动画的例子如清单8.11所示,这里咱们对UIImage的image属性作修改,可是隐式动画或者CAPropertyAnimation都不能对它作动画,由于Core Animation不知道如何在插图图片。经过对图层应用一个淡入淡出的过渡,咱们能够忽略它的内容来作平滑动画(图8.4),咱们来尝试修改过渡的type常量来观察其它效果。

清单8.11 使用CATransition来对UIImageView作动画

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *imageView;
@property (nonatomic, copy) NSArray *images;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //set up images
    self.images = @[[UIImage imageNamed:@"Anchor.png"],
                    [UIImage imageNamed:@"Cone.png"],
                    [UIImage imageNamed:@"Igloo.png"],
                    [UIImage imageNamed:@"Spaceship.png"]];
}
- (IBAction)switchImage
{
    //set up crossfade transition
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionFade;
    //apply transition to imageview backing layer
    [self.imageView.layer addAnimation:transition forKey:nil];
    //cycle to next image
    UIImage *currentImage = self.imageView.image;
    NSUInteger index = [self.images indexOfObject:currentImage];
    index = (index + 1) % [self.images count];
    self.imageView.image = self.images[index];
}
@end

你能够从代码中看出,过渡动画和以前的属性动画或者动画组添加到图层上的方式一致,都是经过-addAnimation:forKey:方法。可是和属性动画不一样的是,对指定的图层一次只能使用一次CATransition,所以,不管你对动画的键设置什么值,过渡动画都会对它的键设置成“transition”,也就是常量kCATransition。

8.4.jpeg

图8.4 使用CATransition对图像平滑淡入淡出

隐式过渡

CATransision能够对图层任何变化平滑过渡的事实使得它成为那些很差作动画的属性图层行为的理想候选。苹果固然意识到了这点,而且当设置了CALayer的content属性的时候,CATransition的确是默认的行为。可是对于视图关联的图层,或者是其余隐式动画的行为,这个特性依然是被禁用的,可是对于你本身建立的图层,这意味着对图层contents图片作的改动都会自动附上淡入淡出的动画。

咱们在第七章使用CATransition做为一个图层行为来改变图层的背景色,固然backgroundColor属性能够经过正常的CAPropertyAnimation来实现,但这不是说不能够用CATransition来实行。

对图层树的动画

CATransition并不做用于指定的图层属性,这就是说你能够在即便不能准确得知改变了什么的状况下对图层作动画,例如,在不知道UITableView哪一行被添加或者删除的状况下,直接就能够平滑地刷新它,或者在不知道UIViewController内部的视图层级的状况下对两个不一样的实例作过渡动画。

这些例子和咱们以前所讨论的状况彻底不一样,由于它们不只涉及到图层的属性,并且是整个图层树的改变--咱们在这种动画的过程当中手动在层级关系中添加或者移除图层。

这里用到了一个小诡计,要确保CATransition添加到的图层在过渡动画发生时不会在树状结构中被移除,不然CATransition将会和图层一块儿被移除。通常来讲,你只须要将动画添加到被影响图层的superlayer。

在清单8.2中,咱们展现了如何在UITabBarController切换标签的时候添加淡入淡出的动画。这里咱们创建了默认的标签应用程序模板,而后用UITabBarControllerDelegate的-tabBarController:didSelectViewController:方法来应用过渡动画。咱们把动画添加到UITabBarController的视图图层上,因而在标签被替换的时候动画不会被移除。

清单8.12 对UITabBarController作动画

#import "AppDelegate.h"
#import "FirstViewController.h" 
#import "SecondViewController.h"
#import @implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
    UIViewController *viewController1 = [[FirstViewController alloc] init];
    UIViewController *viewController2 = [[SecondViewController alloc] init];
    self.tabBarController = [[UITabBarController alloc] init];
    self.tabBarController.viewControllers = @[viewController1, viewController2];
    self.tabBarController.delegate = self;
    self.window.rootViewController = self.tabBarController;
    [self.window makeKeyAndVisible];
    return YES;
}
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
{
    ?//set up crossfade transition
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionFade;
    //apply transition to tab bar controller's view
    [self.tabBarController.view.layer addAnimation:transition forKey:nil];
}
@end

自定义动画

咱们证明了过渡是一种对那些不太好作平滑动画属性的强大工具,可是CATransition的提供的动画类型太少了。

更奇怪的是苹果经过UIView +transitionFromView:toView:duration:options:completion:和+transitionWithView:duration:options:animations:方法提供了Core Animation的过渡特性。可是这里的可用的过渡选项和CATransition的type属性提供的常量彻底不一样。UIView过渡方法中options参数能够由以下常量指定:

UIViewAnimationOptionTransitionFlipFromLeft 
UIViewAnimationOptionTransitionFlipFromRight
UIViewAnimationOptionTransitionCurlUp 
UIViewAnimationOptionTransitionCurlDown
UIViewAnimationOptionTransitionCrossDissolve 
UIViewAnimationOptionTransitionFlipFromTop 
UIViewAnimationOptionTransitionFlipFromBottom

除了UIViewAnimationOptionTransitionCrossDissolve以外,剩下的值和CATransition类型彻底不要紧。你能够用以前例子修改过的版原本测试一下(见清单8.13)。

清单8.13 使用UIKit提供的方法来作过渡动画

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *imageView;
@property (nonatomic, copy) NSArray *images;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad]; //set up images
    self.images = @[[UIImage imageNamed:@"Anchor.png"],
                    [UIImage imageNamed:@"Cone.png"],
                    [UIImage imageNamed:@"Igloo.png"],
                    [UIImage imageNamed:@"Spaceship.png"]];
- (IBAction)switchImage
{
    [UIView transitionWithView:self.imageView duration:1.0
                       options:UIViewAnimationOptionTransitionFlipFromLeft
                    animations:^{
                        //cycle to next image
                        UIImage *currentImage = self.imageView.image;
                        NSUInteger index = [self.images indexOfObject:currentImage];
                        index = (index + 1) % [self.images count];
                        self.imageView.image = self.images[index];
                    }
                    completion:NULL];
}
@end

文档暗示过在iOS5(带来了Core Image框架)以后,能够经过CATransition的filter属性,用CIFilter来建立其它的过渡效果。然是直到iOS6都作不到这点。试图对CATransition使用Core Image的滤镜彻底没效果(可是在Mac OS中是可行的,也许文档是想表达这个意思)。

所以,根据要实现的效果,你只用关心是用CATransition仍是用UIView的过渡方法就能够了。但愿下个版本的iOS系统能够经过CATransition很好的支持Core Image的过渡滤镜效果(或许甚至会有新的方法)。

但这并不意味着在iOS上就不能实现自定义的过渡效果了。这只是意味着你须要作一些额外的工做。就像以前提到的那样,过渡动画作基础的原则就是对原始的图层外观截图,而后添加一段动画,平滑过渡到图层改变以后那个截图的效果。若是咱们知道如何对图层截图,咱们就可使用属性动画来代替CATransition或者是UIKit的过渡方法来实现动画。

事实证实,对图层作截图仍是很简单的。CALayer有一个-renderInContext:方法,能够经过把它绘制到Core Graphics的上下文中捕获当前内容的图片,而后在另外的视图中显示出来。若是咱们把这个截屏视图置于原始视图之上,就能够遮住真实视图的全部变化,因而从新建立了一个简单的过渡效果。

清单8.14演示了一个基本的实现。咱们对当前视图状态截图,而后在咱们改变原始视图的背景色的时候对截图快速转动而且淡出,图8.5展现了咱们自定义的过渡效果。

为了让事情更简单,咱们用UIView -animateWithDuration:completion:方法来实现。虽然用CABasicAnimation能够达到一样的效果,可是那样的话咱们就须要对图层的变换和不透明属性建立单独的动画,而后当动画结束的是哦户在CAAnimationDelegate中把coverView从屏幕中移除。

清单8.14 用renderInContext:建立自定义过渡效果

@implementation ViewController
- (IBAction)performTransition
{
    //preserve the current view snapshot
    UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
    [self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext();
    //insert snapshot view in front of this one
    UIView *coverView = [[UIImageView alloc] initWithImage:coverImage];
    coverView.frame = self.view.bounds;
    [self.view addSubview:coverView];
    //update the view (we'll simply randomize the layer background color)
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
    //perform animation (anything you like)
    [UIView animateWithDuration:1.0 animations:^{
        //scale, rotate and fade the view
        CGAffineTransform transform = CGAffineTransformMakeScale(0.01, 0.01);
        transform = CGAffineTransformRotate(transform, M_PI_2);
        coverView.transform = transform;
        coverView.alpha = 0.0;
    } completion:^(BOOL finished) {
        //remove the cover view now we're finished with it
        [coverView removeFromSuperview];
    }];
}
@end

8.5.jpeg

图8.5 使用renderInContext:建立自定义过渡效果

这里有个警告:-renderInContext:捕获了图层的图片和子图层,可是不能对子图层正确地处理变换效果,并且对视频和OpenGL内容也不起做用。可是用CATransition,或者用私有的截屏方式就没有这个限制了。

在动画过程当中取消动画

以前提到过,你能够用-addAnimation:forKey:方法中的key参数来在添加动画以后检索一个动画,使用以下方法:

- (CAAnimation *)animationForKey:(NSString *)key;

但并不支持在动画运行过程当中修改动画,因此这个方法主要用来检测动画的属性,或者判断它是否被添加到当前图层中。

为了终止一个指定的动画,你能够用以下方法把它从图层移除掉:

- (void)removeAnimationForKey:(NSString *)key;

或者移除全部动画:

- (void)removeAllAnimations;

动画一旦被移除,图层的外观就马上更新到当前的模型图层的值。通常说来,动画在结束以后被自动移除,除非设置removedOnCompletion为NO,若是你设置动画在结束以后不被自动移除,那么当它不须要的时候你要手动移除它;不然它会一直存在于内存中,直到图层被销毁。

咱们来扩展以前旋转飞船的示例,这里添加一个按钮来中止或者启动动画。这一次咱们用一个非nil的值做为动画的键,以便以后能够移除它。-animationDidStop:finished:方法中的flag参数代表了动画是天然结束仍是被打断,咱们能够在控制台打印出来。若是你用中止按钮来终止动画,它会打印NO,若是容许它完成,它会打印YES。

清单8.15是更新后的示例代码,图8.6显示告终果。

清单8.15 开始和中止一个动画

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) CALayer *shipLayer;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //add the ship
    self.shipLayer = [CALayer layer];
    self.shipLayer.frame = CGRectMake(0, 0, 128, 128);
    self.shipLayer.position = CGPointMake(150, 150);
    self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:self.shipLayer];
}
- (IBAction)start
{
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation";
    animation.duration = 2.0;
    animation.byValue = @(M_PI * 2);
    animation.delegate = self;
    [self.shipLayer addAnimation:animation forKey:@"rotateAnimation"];
}
- (IBAction)stop
{
    [self.shipLayer removeAnimationForKey:@"rotateAnimation"];
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    //log that the animation stopped
    NSLog(@"The animation stopped (finished: %@)", flag? @"YES": @"NO");
}
@end

8.6.jpeg

图8.6 经过开始和中止按钮控制的旋转动画

总结

这一章中,咱们涉及了属性动画(你能够对单独的图层属性动画有更加具体的控制),动画组(把多个属性动画组合成一个独立单元)以及过分(影响整个图层,能够用来对图层的任何内容作任何类型的动画,包括子图层的添加和移除)。

在第九章中,咱们继续学习CAMediaTiming协议,来看一看Core Animation是怎样处理逝去的时间。

相关文章
相关标签/搜索