iOS Core Animation: Advanced Techniques中文译本
原文链接:https://zsisme.gitbooks.io/ios-/content/index.html
7. 隐式动画
● 动画是Core Animation库一个非常显著的特性,我们先来讨论框架自动完成的隐式动画(除非你明确禁用了这个功能)。
7.1事务
● Core Animation基于一个假设,说屏幕上的任何东西都可以(或者可能)做动画。动画并不需要你在Core Animation中手动打开,相反需要明确地关闭,否则他会一直存在。
● 当你改变
CALayer
的一个可做动画的属性,它并不能立刻在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值。这一切都是默认的行为,你不需要做额外的操作。● 这其实就是所谓的隐式动画。之所以叫隐式是因为我们并没有指定任何动画的类型。我们仅仅改变了一个属性,然后Core Animation来决定如何并且何时去做动画。Core Animaiton同样支持显式动画,下章详细说明。
● 实际上 动画执行的时间 取决于当前事务的设置,动画类型取决于图层行为。
事务实际上是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:
方法来修改动画时间,但在这里我们首先起一个新的事务,于是修改时间就不会有别的副作用。因为修改当前事务的时间可能会导致同一时刻 别的动画(如屏幕旋转),所以最好还是在调整动画之前压入一个新的事务。修改动画时间关键代码:
- (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
有两个这样的动画方法,+beginAnimations:context:
和+commitAnimations
。 和CATransaction
的+begin
和+commit
方法类似。实际上在
+beginAnimations:context:
和+commitAnimations
之间所有视图或者图层属性的改变而做的动画都是由于设置了CATransaction
的原因。
CATransaction
的+begin
和+commit
方法在+animateWithDuration:animations:
内部自动调用,这样block中所有属性的改变都会被事务所包含。这样也可以避免开发者由于对+begin
和+commit
匹配的失误造成的风险。● 在iOS4中,苹果对UIView添加了一种基于block的动画方法:
+animateWithDuration:animations:
。这样写对做一堆的属性动画在语法上会更加简单,但实质上它们都是在做同样的事情。
7.2 完成块
● 基于
UIView
的block的动画允许你在动画结束的时候提供一个完成的动作。
CATranscation
接口提供的+setCompletionBlock:
方法也有同样的功能。使用block的关键代码:
- (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 颜色渐变之完成之后再做一次旋转
注意旋转动画要比颜色渐变快得多,这是因为完成块是在颜色渐变的事务提交并出栈之后才被执行,于是,用默认的事务做变换,默认的时间也就变成了0.25秒。因为代码中颜色渐变的动画是设置了1.0秒
7.4 图层行为
● 试着直接对UIView关联的图层做动画而不是一个单独的图层。清单7.4是对清单7.2代码的一点修改,移除了
colorLayer
,并且直接设置layerView
关联图层的背景色。对UIView绑定的图层做动画,关键代码:
- (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]; }
● UIKit建立在Core Animation(默认对所有东西都做动画)之上,但是隐式动画是被UIKit禁用掉了。
我们把改变 属性 时
CALayer
自动应用的动画称作 ★ 行为 ,当CALayer
的属性被修改时候,它会调用-actionForKey:
方法,传递属性的名称。剩下的操作都在CALayer
的头文件中有详细的说明,实质上是如下几步:
- 图层首先 “检测” 它是否有 “委托” ,并且是否实现
CALayerDelegate
协议 指定的-actionForLayer:forKey
方法。如果有,直接调用并返回结果。- 如果没有委托,或者委托没有实现
-actionForLayer:forKey
方法,图层接着 “检查 ” 包含属性名称对应行为映射的
“actions
字典”。- 如果
actions字典
没有包含对应的属性,那么图层接着在它的style
字典 接着搜索属性名。- 最后,如果在
style
里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:
方法。所以一轮完整的搜索结束之后,
-actionForKey:
要么返回空(这种情况下将不会有动画发生),要么是CAAction
协议对应的对象,最后CALayer
拿这个结果去对先前和当前的值做动画。UIKit是如何禁用隐式动画的:每个
UIView
对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey
的实现方法。当 不在 一个动画块的实现中,UIView
对所有图层行为返回nil
,但是在动画block范围之内,它就返回了一个非空值。当属性在动画块之外发生改变,
UIView
直接通过返回nil
来禁用隐式动画。但如果在动画块范围之内,根据动画具体类型返回相应的属性● 当然返回
nil
并不是 禁用隐式动画 唯一的办法,CATransacition
有个方法叫做+setDisableActions:
,可以用来对所有属性打开或者关闭隐式动画。如果在清单7.2的[CATransaction begin]
之后添加下面的代码,同样也会阻止动画的发生:[CATransaction setDisableActions:YES];
● 总结一下,我们知道了如下几点
UIView
关联的图层禁用了隐式动画,对这种图层做动画的唯一办法就是使用UIView
的动画函数(而不是依赖CATransaction
),或者继承UIView
,并覆盖-actionForLayer:forKey:
方法,或者直接创建一个显式动画(具体细节见第八章)。- 对于单独存在的图层,我们可以通过实现图层的
-actionForLayer:forKey:
委托方法,或者提供一个actions
字典来控制隐式动画。● 我们来对颜色渐变的例子使用一个不同的行为,通过给
colorLayer
设置一个★自定义的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.3 使用推进过渡的色值动画
7.4 呈现与模型
● 当你改变一个图层的属性,属性值的确是立刻更新的(如果你读取它的数据,你会发现它的值在你设置它的那一刻就已经生效了),但是屏幕上并没有马上发生改变。这是因为你设置的属性并没有直接调整图层的外观,相反,他只是定义了图层动画 结束之后 将要变化的外观。
● 当设置
CALayer
的属性,实际上是在定义当前事务 结束之后 图层如何显示的模型。Core Animation扮演了一个控制器的角色,并且负责根据图层行为和事务设置去不断 更新 视图的这些属性在屏幕上的 状态。● 在iOS中,屏幕每秒钟重绘60次。如果动画时长比60分之一秒要长,Core Animation就需要在设置一次新值和新值生效之间,对屏幕上的图层进行重新组织。这意味着
CALayer
除了“真实”值(就是你设置的值)之外,必须要知道当前显示在屏幕上的属性值的 记录。● 我们讨论的就是一个典型的微型MVC模式。
CALayer
是一个连接用户界面(就是MVC中的view)虚构的类,但是在界面本身这个场景下,CALayer
的行为更像是存储了视图如何显示和动画的 数据模型 。实际上,在苹果自己的文档中,图层树通常都是值的图层树模型。● 每个图层属性的“显示值”都被存储在一个叫做“呈现图层”的独立图层当中,他可以通过
-presentationLayer
方法来访问。这个“呈现图层”实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值(图7.4)。● 我们在第一章中提到除了图层树,另外还有“呈现树”。呈现树通过图层树中所有图层的呈现图层所形成。注意呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用
-presentationLayer
将会返回nil
。● 你可能注意到有一个叫做
–modelLayer
的方法。在呈现图层上调用–modelLayer
将会返回它正在呈现所依赖的CALayer
。通常在一个图层上调用-modelLayer
会返回–self
(实际上我们已经创建的原始图层就是一种数据模型)。![]()
图7.4 一个移动的图层是如何通过数据模型呈现的
两种情况下呈现图层会变得很有用,一个是同步动画,一个是处理用户交互。
- 如果你在实现一个基于定时器的动画(见第11章“基于定时器的动画”),而不仅仅是基于事务的动画,这个时候准确地知道在某一时刻图层显示在什么位置就会对正确摆放图层很有用了。
- 如果你想让你做动画的图层响应用户输入,你可以使用
-hitTest:
方法(见第三章“图层几何学”)来判断指定图层是否被触摸,这时候对 呈现图层 而不是 模型图层 调用-hitTest:
会显得更有意义,因为呈现图层代表了用户当前看到的图层位置,而不是当前动画结束之后的位置。关键代码:
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; }
8. 显式动画
● 显式动画,它能够对一些属性做指定的自定义动画,或者创建非线性动画,比如沿着任意一条曲线移动。
8.1 属性动画
●
CAAnimationDelegate
在任何头文件中都找不到,但是可以在CAAnimation
头文件或者苹果开发者文档中找到相关函数。在这个例子中,我们用-animationDidStop:finished:
方法在动画结束之后来更新图层的backgroundColor
。● 当更新属性的时候,我们需要设置一个新的事务,并且禁用图层行为。否则动画会发生两次,一个是因为 ★ 显式的
CABasicAnimation
,另一次是因为隐式动画,具体实现见清单8.3。清单8.3 动画完成之后修改图层的背景色,关键代码:
@implementation ViewController - (IBAction)changeColor { 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所示),但是所有的动画都会调用同一个回调方法,所以你就需要判断到底是那个图层的调用。● 动画本身会作为一个参数传入委托的方法
-animationDidStop:finished:
,也许你会认为可以控制器中把动画存储为一个属性,然后在回调用比较,但实际上并不起作用,因为委托传入的动画参数是原始值的一个深拷贝,从而不是同一个值。● 当使用
-addAnimation:forKey:
把动画添加到图层,这里有一个到目前为止我们都设置为nil
的key
参数。这里的键是-animationForKey:
方法找到对应动画的唯一标识符,而当前动画的所有键都可以用animationKeys
获取。如果我们对每个动画都关联一个唯一的键,就可以对每个图层循环所有键,然后调用-animationForKey:
来比对结果。尽管这不是一个优雅的实现。● 一种更加简单的方法 ,
CAAnimation
实现了KVC(键-值-编码)协议,于是你可以用-setValue:forKey:
和-valueForKey:
方法来存取属性。但是CAAnimation
有一个不同的性能:它更像一个NSDictionary
,可以让你随意设置键值对,即使和你使用的动画类所声明的属性并不匹配。这意味着你可以对动画用任意类型打标签。清单8.4 使用KVC对动画打标签,关键代码:
//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];
我们发现在
-animationDidStop:finished:
委托方法调用之前,时钟的时针会迅速返回到原始值,这个清单8.3图层颜色发生的情况一样。问题在于,”回调方法“ 在动画完成之前 “已经被调用” 了,但不能保证 这发生在属性动画返回初始状态 之前。我们可以用一个
fillMode
属性来解决这个问题,下一章会详细说明,这里知道在动画之前设置它比在动画结束之后更新属性更加方便。
关键帧动画
●
CAKeyframeAnimation
是另一种UIKit没有暴露出来但功能强大的类。和CABasicAnimation
类似,CAKeyframeAnimation
同样是CAPropertyAnimation
的一个子类,它依然作用于单一的一个属性,但是和CABasicAnimation
不一样的是,它不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。● 关键帧 起源于传动动画,意思是指主导的动画在显著改变发生时重绘当前帧(也就是关键帧),每帧之间剩下的绘制(可以通过关键帧推算出)将由熟练的艺术家来完成。
CAKeyframeAnimation
也是同样的道理:你提供了显著的帧,然后Core Animation在每帧之间进行插入。● 我们