iOS-隐式动画

        在前面的学习中,我们讨论了CoreAnimation除了动画外可以做到的任何事情。但是动画是CoreAnimation的一个非常显著的特性。本文我们讨论下CoreAnimation框架自动完成的隐式动画。

事务

       CoreAnimation基于一个假设,屏幕上的任何东西都可以做动画。动画并不需要在CoreAnimation中手动打开,如果不关闭,它会一直存在。比如你改变了CALayer的一个可做动画的属性,它并不能立刻在屏幕是那个体现出来。相反,它是从先前的值平滑过渡到新的值。着一切都是默认行为,不需要做额外的操作。看起来好牛逼啊。

@interface ViewController ()

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

@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 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

        这个变化其实就是隐式动画,之所以叫隐式是因为我们并没有指定任何动画类型。我们仅仅改变了一个属性,然后CoreAnimation来决定如何并何时去做动画。当我们改变一个属性的时候,CoreAnimation是如何判断动画类型和持续时间的呢?实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为。

        事物实际上是CoreAnimation用来包含一系列属性动画集合的机制,任何指定事物去改变可以做动画的图层属性都不会立刻发生变化,而是当事物一旦提交的时候开始用一个动画过度到新值。

        事务是通过CATransaction类来管理的,这个类的设计有点奇怪,不像名字看的那样管理一个简单事务,而是管理一堆你不能访问的事务。CATransaction没有属性或者实例方法,不能用+alloc和-init方法去管理它。但是可以+begin和+commit分别来入栈和出栈。

       任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(0.25s)。

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

明白这些后,我们就可以轻松的修改变色动画时间,我们当然可以用当前事务的+setAnimationDuration:方法来修改动画时间,当我们首先起一个新的事务,于是修改时间就不会别的副作用。因为修改当前事务的时间有可能导致同一时刻别的动画,所以最好还是在调整动画之前压入一个新的事务。

使用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动画允许我们在动画结束时候提供一个 完成的动作。CATranscatiion接口提供的 +setCompletionBlock:方法也有同样的功能。我们修改上面的例子,在颜色变化结束之后执行一些操作,转换到另外一个旋转90度的动画

[CATransaction setCompletionBlock:^{//变色动画完成后的旋转动画

//rotate the layer 90 degrees

CGAffineTransform transform = self.colorLayer.affineTransform;

transform = CGAffineTransformRotate(transform, M_PI_2);

self.colorLayer.affineTransform = transform;

}];

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

图层行为

现在做一个实验,试着对UIViw关联的图层做动画而不是一个单独的图层

@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];

}

        运行程序,点击按钮,我们发现图层的颜色瞬间切换到新的值,而不是之前平滑过渡的动画。好像隐式动画被uivew的关联图层给禁用了。试想,如果UIView的属性都有动画特性的话,那么无论什么时候修改它,我们都能够注意到的。所以如果说UIKit建立在CoreAnimation之上,那么隐式动画是如何被禁掉的呢?

       我们知道CoreAnimation通常对CAlayer的所有属性(可动画的属性)做动画,但是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范围之内,它就返回了一个非空值。我们可以用一个demo做个简单的实验。

@interface ViewController ()

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

@end

- (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来禁用了隐式动画。但在动画块之内,根据动画具体类型返回相应属性。

当然返回nil并不是禁用隐式动画唯一的办法,CATransacition有个方法叫做+setDisableActions:,可以用来对所有属性打开或者关闭隐式动画。如果在清单7.2的[CATransaction begin]之后添加下面的代码,同样也会阻止动画的发生:

[CATransaction setDisableActions:YES];

总结如下:

1、UIView关联的图层禁用了隐式动画,对这种图层做动画的唯一方法就是使用UIView的动画函数,而不是依赖CATransaction,或者继承UIView,并覆盖actionForlayer:forkey方法,或者直接创建一个显式动画。

2、对于单独存在的的图层,我们可以通过实现图层-actionForLayer:forkey:委托方法,或者提供一个actions字典来控制隐式动画。

我来对颜色渐变的例子是有一个不同的行为,设置一个自定义的actions字典。

@interface ViewController ()

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

@property (nonatomic, strong)  CALayer *colorLayer

@end

- (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

一种推进过多的颜色变化效果。

呈现与模型

       CAlayer的属性行为其实很不正常,因为改变一个图层的属性,并没有立刻生效,而是通过一段时间渐变更新,怎么做到的呀。

        当我们改变一个图层的属性时候,属性值的确是立刻更新的,但是屏幕并没有马上发生改变,这是因为你设置的属性并没有直接调整图层的外观,他只是定义了图层动画结束之后将要变化的外观。设置CAlayer的属性,实际上是在定义当前事务结束之后图层如何显示的模型。CoreAnimation扮演了一个控制器的角色,并且负责根据图层行为和事务设置去不断更新视图这些属性的在屏幕上的状态。

        我们讨论的就是一个典型的的微型MVC模式。CAlayer是一个连接用户界面,相当于view,但是在界面本身这个场景下,CAlayer的行为更像是存储了视图如何显示和动画的数据模型。在iOS中屏幕每秒重绘60次。如果动画时长超过1/60;CoreAnimation就需要在设置一次新值和新值生效之间对图层重新组织。这就是说CAlayer除了真实值之外必须知道当前属性值的记录。

       每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,他可以通过-presentationlayer方法来访问。这个呈现图层是模型图层的复制,但是他的属性值代表了在任何时刻已指定当前外观效果。在“图层”我们提到了图层树,另外还有呈现树。呈现树通过图层树中所有图层的呈现图层所形成。呈现图层仅仅当图层首次被提交的时候创建,所以在那之前调用-presentationlayer将返回nil。

你可能注意到有一个叫做–modelLayer的方法。在呈现图层上调用–modelLayer将会返回它正在呈现所依赖的CALayer。通常在一个图层上调用-modelLayer会返回–self(实际上我们已经创建的原始图层就是一种数据模型)。

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

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

1、如果你在实现一个基于定时器的动画,而不仅仅是基于事务的动画,这个时候准确地知道在某一时刻图层显示在什么位置就会对正确摆放图层很有用了。

2、如果你想让你做动画的图层响应用户输入,你可以使用-hitTest:方法(见“图层几何学”)来判断指定图层是否被触摸,这时候对呈现图层而不是模型图层调用-hitTest:会显得更有意义,因为呈现图层代表了用户当前看到的图层位置,而不是当前动画结束之后的位置。

做一个简单的例子,点击屏幕上任意位置将会让图层平移到那里。点击图层本身可以随机改变它的颜色。我们通过呈现图层调用-hitTest:来判断是否被点击。如果修改代码让-hitTest:直接作用于colorlayer而不是呈现层,当图层移动的时候它并不能正确显示。这个时候就需要点击图层将要移动到的位置而不是图层本身来响应点击(这就是为什么用呈现图层来响应交互的原因)

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

@interface ViewController ()

@property (nonatomic, strong) CALayer *colorLayer;

@end

- (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];

}

}

本文讨论了,隐式动画,还有CoreAnimtion对制定属性选择合适动画行为的机制。同时知道了UIKit是如何充分利用CoreAnimation的隐式动画机制来强化它的显示系统,以及动画是如何被默认禁止。

推荐阅读更多精彩内容