对图层树的动画
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 <QuartzCore/QuartzCore.h> @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 使用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 通过开始和停止按钮控制的旋转动画
总结
这一章中,我们涉及了属性动画(你可以对单独的图层属性动画有更加具体的控制),动画组(把多个属性动画组合成一个独立单元)以及过度(影响整个图层,可以用来对图层的任何内容做任何类型的动画,包括子图层的添加和移除)。
在第九章中,我们继续学习CAMediaTiming协议,来看一看Core Animation是怎样处理逝去的时间。