1.CATransaction
事务;
UIView有两个方法,+beginAnimations:context:和+commitAnimations,和CATransaction的+begin 和+commit方法类似。实际上在+beginAnimations:context:和+commitAnimations之间所有视图或者图层属性的改变而做的动画都是由于设置了CATransaction的原因。
UIView封装动画中的块动画,对做一堆的属性动画在语法上会更加简单,但实质上它们都是在做同样的事情。
基于UIView的block的动画允许你在动画结束的时候提供一个完成的动作。CATranscation接口提供的+setCompletionBlock:方法也有同样的功能
2.属性动画实质:
当CALayer的属性被修改时候,它会调用-actionForKey:方法,传递属性的名称。剩下的操作都在CALayer的头文件中有详细的说明,实质上是如下几步:
-
图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。如果有,直接调用并返回结果。
-
如果没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
-
如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
-
最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。
所以一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。
通过下面一段代码,可以理解每个UIView对它关联的图层是如何禁用隐式动画的
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"Outside: %@", [self.view actionForLayer:self.view.layer forKey:@"backgroundColor"]); [UIView beginAnimations:nil context:nil]; NSLog(@"Inside: %@", [self.view actionForLayer:self.view.layer forKey:@"backgroundColor"]); [UIView commitAnimations]; }
运行结果:
2016-05-30 14:48:18.614 测试[10222:147396] Outside: <null> 2016-05-30 14:48:18.615 测试[10222:147396] Inside: <CABasicAnimation: 0x7f8f02403d10>
总结一下,我们知道了如下几点
-
UIView 关联的图层禁用了隐式动画,对这种图层做动画的唯一办法就是使用UIView的动画函数(而不是依赖CATransaction),或者继承 UIView,并覆盖-actionForLayer:forKey:方法,或者直接创建一个显式动画(具体细节见第八章)。
-
对于单独存在的图层,我们可以通过实现图层的-actionForLayer:forKey:委托方法,或者提供一个actions字典来控制隐式动画。
3.CATransition
CATransition继承CAAnimation,而CAAnimation响应CAAction协议;
CATransition *transition = [CATransition animation]; transition.type = kCATransitionPush; transition.subtype = kCATransitionFromLeft; self.colorLayer.actions = @{@"backgroundColor": transition};
这种方法,只是针对单独的图层,不能用在UIView关联的图层;
4.presentationLayer
每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,他可以通过-presentationLayer方法来访问。这个呈现图层实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值
注意呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用-presentationLayer将会返回nil。
在呈现图层上调用–modelLayer将会返回它正在呈现所依赖的CALayer。通常在一个图层上调用-modelLayer会返回–self(实际上我们已经创建的原始图层就是一种数据模型)。
5.显式动画
在iOS中核心动画分为几类:基础动画、关键帧动画、动画组、转场动画。各个类的关系大致如下:
CAAnimation:核心动画的基础类,不能直接使用,负责动画运行时间、速度的控制。CAAnimation本身并没有做多少工作,它提供了一个计时函数,一个委托(用于反馈动画状态)以及一个 removedOnCompletion,用于标识动画是否该在结束后自动释放(默认YES,为了防止内存泄露)。CAAnimation同时实现了一些 协议,包括CAAction(允许CAAnimation的子类可以提供图层行为),以及CAMediaTiming
CAPropertyAnimation:属性动画的基类(通过属性进行动画设置,注意是可动画属性),不能直接使用。通过指定动画的keyPath作用于一个单一属性,CAAnimation通常应用于一个指定的CALayer,于是这里指的也就是一个图层的 keyPath了。实际上它是一个关键路径(一些用点表示法可以在层级关系中指向任意嵌套的对象),而不仅仅是一个属性的名称,因为这意味着动画不仅可以 作用于图层本身的属性,而且还包含了它的子成员的属性,甚至是一些虚拟的属性
CAAnimationGroup:动画组,动画组是一种组合模式设计,可以通过动画组来进行所有动画行为的统一控制,组中所有动画效果可以并发执行。
CATransition:过渡(转场)动画,主要通过滤镜进行动画效果设置。过渡并不像属性动画那样平滑地在两个值之间做动画,而是影响到整个图层的变化。过渡动画首先展示之前的图层外观,然后通过一个交换过渡到新的外观。
CABasicAnimation:基础动画,通过属性修改进行动画参数控制,只有初始状态和结束状态。
CAKeyframeAnimation:关键帧动画,同样是通过属性进行动画参数控制,但是同基础动画不同的是它可以有多个状态控制。CAKeyFrameAnimation添加了一个rotationMode的属性。设置它为常量kCAAnimationRotateAuto,图层将会根据曲线的切线自动旋转
基础动画、关键帧动画都属于属性动画,就是通过修改属性值产生动画效果,开发人员只需要设置初始值和结束值,中间的过程动画(又叫“补间动画”)由 系统自动计算产生。和基础动画不同的是关键帧动画可以设置多个属性值,每两个属性中间的补间动画由系统自动完成,因此从这个角度而言基础动画又可以看成是 有两个关键帧的关键帧动画。
- (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]; }
6.虚拟属性
CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"transform.rotation"; animation.duration = 2.0; animation.byValue = @(M_PI * 2); [shipLayer addAnimation:animation forKey:nil];
用transform.rotation而不是transform做动画的好处如下:
-
我们可以不通过关键帧一步旋转多于180度的动画。
-
可以用相对值而不是绝对值旋转(设置byValue而不是toValue)。
-
可以不用创建CATransform3D,而是使用一个简单的数值来指定角度。
-
不会和transform.position或者transform.scale冲突(同样是使用关键路径来做独立的动画属性)。
transform.rotation 属性有一个奇怪的问题是它其实并不存在。这是因为CATransform3D并不是一个对象,它实际上是一个结构体,也没有符合KVC相关属 性,transform.rotation实际上是一个CALayer用于处理动画变换的虚拟属性。
7.动画组
//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.过渡动画
CAAnimation有一个type和subtype来标识变换效果。type属性是一个NSString类型,可以被设置成如下类型:
kCATransitionFade
kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal
通过subtype来控制它们的方向,提供了如下四种类型:
kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom
隐式过渡
CATransision 可以对图层任何变化平滑过渡的事实使得它成为那些不好做动画的属性图层行为的理想候选。苹果当然意识到了这点,并且当设置了CALayer的 content属性的时候,CATransition的确是默认的行为。但是对于视图关联的图层,或者是其他隐式动画的行为,这个特性依然是被禁用的,但 是对于你自己创建的图层,这意味着对图层contents图片做的改动都会自动附上淡入淡出的动画。
对图层树的动画
CATransition并不作用于指定的图层属性,这就是说你可以在即使不能准确得知改变了什么的情况下对图层做动画,例如,在不知道 UITableView哪一行被添加或者删除的情况下,直接就可以平滑地刷新它,或者在不知道UIViewController内部的视图层级的情况下对 两个不同的实例做过渡动画。
这些例子和我们之前所讨论的情况完全不同,因为它们不涉及到图层的属性,而且是整个图层树的改变--我们在这种动画的过程中手动在层级关系中添加或者移除图层。
这里用到了一个小诡计,要确保CATransition添加到的图层在过渡动画发生时不会在树状结构中被移除,否则CATransition将会和图层一起被移除。一般来说,你只需要将动画添加到被影响图层的superlayer。
- (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]; }
自定义动画
奇怪的是苹果通过UIView +transitionFromView:toView:duration:options:completion: 和+transitionWithView:duration:options:animations:方法提供了Core Animation的过渡特性。但是这里的可用的过渡选项和CATransition的type属性提供的常量完全不同。UIView过渡方法中 options参数可以由如下常量指定:
UIViewAnimationOptionTransitionFlipFromLeft
UIViewAnimationOptionTransitionFlipFromRight
UIViewAnimationOptionTransitionCurlUp
UIViewAnimationOptionTransitionCurlDown
UIViewAnimationOptionTransitionCrossDissolve
UIViewAnimationOptionTransitionFlipFromTop
UIViewAnimationOptionTransitionFlipFromBottom
过渡动画做基础的原则就是对原始的图层外观截图,然后添加一段动画,平滑过渡到图层改变之后那个截图的效果。如果我们知道如何对图层截图,我们就可以使用属性动画来代替CATransition或者是UIKit的过渡方法来实现动画。
事实证明,对图层做截图还是很简单的。CALayer有一个-renderInContext:方法,可以通过把它绘制到Core Graphics的上下文中捕获当前内容的图片,然后在另外的视图中显示出来。如果我们把这个截屏视图置于原始视图之上,就可以遮住真实视图的所有变化, 于是重新创建了一个简单的过渡效果。
清单8.14演示了一个基本的实现。我们对当前视图状态截图,然后在我们改变原始视图的背景色的时候对截图快速转动并且淡出,图8.5展示了我们自定义的过渡效果。
为 了让事情更简单,我们用UIView -animateWithDuration:completion:方法来实现。虽然用CABasicAnimation可以达到同样的效果,但是那样的 话我们就需要对图层的变换和不透明属性创建单独的动画,然后当动画结束的是哦户在CAAnimationDelegate中把coverView从屏幕中 移除。
清单8.14 用renderInContext:创建自定义过渡效果
- (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]; }]; }
CAMediaTiming协议
duration
repeatCount
repeatDuration
autoreverses
注意repeatCount和repeatDuration可能会相互冲突,所以你只要对其中一个指定非零值。对两个属性都设置非0值的行为没有被定义。
beginTime
指定了动画开始之前的的延迟时间。这里的延迟从动画添加到可见图层的那一刻开始测量,默认是0(就是说动画会立刻执行)。
speed
是一个时间的倍数,默认1.0,减少它会减慢图层/动画的时间,增加它会加快速度。如果2.0的速度,那么对于一个duration为1的动画,实际上在0.5秒的时候就已经完成了。
timeOffset
和beginTime类似,但是和增加beginTime导致的延迟动画不同,增加timeOffset只是让动画快进到某一点,例如,对于一个持续1秒的动画来说,设置timeOffset为0.5意味着动画将从一半的地方开始。
fillMode
fillMode是一个NSString类型,可以接受如下四种常量:
kCAFillModeForwards
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved
默认是kCAFillModeRemoved,当动画不再播放的时候就显示图层模型指定的值剩下的三种类型向前,向后或者即向前又向后去填充动画状态,使得动画在开始前或者结束后仍然保持开始和结束那一刻的值。
这就对避免在动画结束的时候急速返回提供另一种方案(见第八章)。但是记住了,当用它来解决这个问题的时候,需要把removeOnCompletion设置为NO,另外需要给动画添加一个非空的键,于是可以在不需要动画的时候把它从图层上移除。
暂停,倒回和快进(图层的speed)
设置动画的speed属性为0可以暂停动画,但在动画被添加到图层之后不太可能再修改它了,所以不能对正在进行的动画使用这个属性。给图层添加一个 CAAnimation实际上是给动画对象做了一个不可改变的拷贝,所以对原始动画对象属性的改变对真实的动画并没有作用。相反,直接用 -animationForKey:来检索图层正在进行的动画可以返回正确的动画对象,但是修改它的属性将会抛出异常。
如果移除图层正在进行的动画,图层将会急速返回动画之前的状态。但如果在动画移除之前拷贝呈现图层到模型图层,动画将会看起来暂停在那里。但是不好的地方在于之后就不能再恢复动画了。
一个简单的方法是可以利用CAMediaTiming来暂停图层本身。如果把图层的speed设置成0,它会暂停任何添加到图层上的动画。类似的,设置speed大于1.0将会快进,设置成一个负值将会倒回动画。
通过增加主窗口图层的speed,可以暂停整个应用程序的动画。这对UI自动化提供了好处,我们可以加速所有的视图动画来进行自动化测试(注意对于在主窗口之外的视图并不会被影响,比如UIAlertview)。可以在app delegate设置如下进行验证:
self.window.layer.speed = 100;
你也可以通过这种方式来减速,但其实也可以在模拟器通过切换慢速动画来实现。
手动动画(图层的timeOffset)
timeOffset一个很有用的功能在于你可以它可以让你手动控制动画进程,通过设置speed为0,可以禁用动画的自动播放,然后来使用timeOffset来来回显示动画序列。这可以使得运用手势来手动控制动画变得很简单。
@property (nonatomic, strong) CALayer *doorLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //add the door self.doorLayer = [CALayer layer]; self.doorLayer.frame = CGRectMake(0, 0, 128, 256); self.doorLayer.position = CGPointMake(150 - 64, 150); self.doorLayer.anchorPoint = CGPointMake(0, 0.5); self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"bjl_list_02"].CGImage; [self.view.layer addSublayer:self.doorLayer]; //apply perspective transform CATransform3D perspective = CATransform3DIdentity; perspective.m34 = -1.0 / 500.0; self.view.layer.sublayerTransform = perspective; //add pan gesture recognizer to handle swipes UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init]; [pan addTarget:self action:@selector(pan:)]; [self.view addGestureRecognizer:pan]; //pause all layer animations self.doorLayer.speed = 0.0; //apply swinging animation (which won't play because layer is paused) CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"transform.rotation.y"; animation.toValue = @(-M_PI_2); animation.duration = 1.0; [self.doorLayer addAnimation:animation forKey:nil]; } - (void)pan:(UIPanGestureRecognizer *)pan { //get horizontal component of pan gesture CGFloat x = [pan translationInView:self.view].x; //convert from points to animation duration //using a reasonable scale factor x /= 200.0f; //update timeOffset and clamp result CFTimeInterval timeOffset = self.doorLayer.timeOffset; timeOffset = MIN(0.999, MAX(0.0, timeOffset - x)); self.doorLayer.timeOffset = timeOffset; //reset pan gesture [pan setTranslation:CGPointZero inView:self.view]; }