8. 顯式動畫
8.1 屬性動畫
CAAnimationDelegate在任何頭文件中都找不到,但是可以在CAAnimation頭文件或者蘋果開發者文檔中找到相關函數。在這個例子中,我們用-animationDidStop:finished:
方法在動畫結束之后來更新圖層的backgroundColor。
當更新屬性的時候,我們需要設置一個新的事務,并且禁用圖層行為。否則動畫會發生兩次,一個是因為顯式的CABasicAnimation,另一次是因為隱式動畫。
@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
如果,僅僅是想要在動畫做完之后,保留原來的位置。那么可以添加如下代碼animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
對CAAnimation而言,使用委托模式而不是一個完成塊會帶來一個問題,就是當你有多個動畫的時候,無法在在回調方法中區分。在一個視圖控制器中創建動畫的時候,通常會用控制器本身作為一個委托,但是所有的動畫都會調用同一個回調方法,所以你就需要判斷到底是那個圖層的調用。
關鍵幀動畫
CAKeyframeAnimation是另一種UIKit沒有暴露出來但功能強大的類。和CABasicAnimation類似,CAKeyframeAnimation同樣是CAPropertyAnimation的一個子類,它依然作用于單一的一個屬性,但是和CABasicAnimation不一樣的是,它不限制于設置一個起始和結束的值,而是可以根據一連串隨意的值來做動畫。
- (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)。動畫會在開始的時候突然跳轉到第一幀的值,然后在動畫結束的時候突然恢復到原始的值。所以為了動畫的平滑特性,我們需要開始和結束的關鍵幀來匹配當前屬性的值。
沿著一個貝塞爾曲線對圖層做動畫
@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
運行示例,你會發現飛船的動畫有些不太真實,這是因為當它運動的時候永遠指向右邊,而不是指向曲線切線的方向。你可以調整它的affineTransform來對運動方向做動畫,但很可能和其它的動畫沖突。
幸運的是,蘋果預見到了這點,并且給CAKeyFrameAnimation
添加了一個rotationMode
的屬性。設置它為常量kCAAnimationRotateAuto
,圖層將會根據曲線的切線自動旋轉。
- (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];
}
虛擬屬性
@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的實現細節是私有的,所以目前不能通過繼承它來自定義。你可以通過使用蘋果目前已近提供的常量(目前都是和變換矩陣的虛擬屬性相關,所以沒太多使用場景了,因為這些屬性都有了默認的實現方式)。
8.2 動畫組
CABasicAnimation和CAKeyframeAnimation僅僅作用于單獨的屬性,而CAAnimationGroup可以把這些動畫組合在一起。CAAnimationGroup是另一個繼承于CAAnimation的子類,它添加了一個animations數組的屬性,用來組合別的動畫。
- (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 過渡
有時候對于iOS應用程序來說,希望能通過屬性動畫來對比較難做動畫的布局進行一些改變。比如交換一段文本和圖片,或者用一段網格視圖來替換,等等。屬性動畫只對圖層的可動畫屬性起作用,所以如果要改變一個不能動畫的屬性(比如圖片),或者從層級關系中添加或者移除圖層,屬性動畫將不起作用。
為了創建一個過渡動畫,我們將使用CATransition,同樣是另一個CAAnimation的子類,和別的子類不同,CATransition有一個type和subtype來標識變換效果。type屬性是一個NSString類型,可以被設置成如下類型:
kCATransitionFade
kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal
kCATransitionPush,它創建了一個新的圖層,從邊緣的一側滑動進來,把舊圖層從另一側推出去的效果。
kCATransitionMoveIn和kCATransitionReveal與kCATransitionPush類似,都實現了一個定向滑動的動畫,但是有一些細微的不同,kCATransitionMoveIn從頂部滑動進入,但不像推送動畫那樣把老土層推走,然而kCATransitionReveal把原始的圖層滑動出去來顯示新的外觀,而不是把新的圖層滑動進入。
后面三種過渡類型都有一個默認的動畫方向,它們都從左側滑入,但是你可以通過subtype來控制它們的方向,提供了如下四種類型:
kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom
一個簡單的用CATransition來對非動畫屬性做動畫的,這里我們對UIImage的image屬性做修改,但是隱式動畫或者CAPropertyAnimation都不能對它做動畫,因為Core Animation不知道如何在插圖圖片。通過對圖層應用一個淡入淡出的過渡,我們可以忽略它的內容來做平滑動畫,我們來嘗試修改過渡的type常量來觀察其它效果。
@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。
隱式過渡
CATransision可以對圖層任何變化平滑過渡的事實使得它成為那些不好做動畫的屬性圖層行為的理想候選。蘋果當然意識到了這點,并且當設置了CALayer的content屬性的時候,CATransition的確是默認的行為。但是對于視圖關聯的圖層,或者是其他隱式動畫的行為,這個特性依然是被禁用的,但是對于你自己創建的圖層,這意味著對圖層contents圖片做的改動都會自動附上淡入淡出的動畫。
對圖層樹的動畫
CATransition并不作用于指定的圖層屬性,這就是說你可以在即使不能準確得知改變了什么的情況下對圖層做動畫,例如,在不知道UITableView哪一行被添加或者刪除的情況下,直接就可以平滑地刷新它,或者在不知道UIViewController內部的視圖層級的情況下對兩個不同的實例做過渡動畫。
這些例子和我們之前所討論的情況完全不同,因為它們不僅涉及到圖層的屬性,而且是整個圖層樹的改變--我們在這種動畫的過程中手動在層級關系中添加或者移除圖層。
這里用到了一個小詭計,要確保CATransition添加到的圖層在過渡動畫發生時不會在樹狀結構中被移除,否則CATransition將會和圖層一起被移除。一般來說,你只需要將動畫添加到被影響圖層的superlayer。
如何在UITabBarController切換標簽的時候添加淡入淡出的動畫。這里我們建立了默認的標簽應用程序模板,然后用
UITabBarControllerDelegate
的-tabBarController:didSelectViewController:
方法來應用過渡動畫。我們把動畫添加到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
自定義動畫
蘋果通過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類型完全沒關系。
@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中是可行的,也許文檔是想表達這個意思)。
但這并不意味著在iOS上就不能實現自定義的過渡效果了。這只是意味著你需要做一些額外的工作。就像之前提到的那樣,過渡動畫做基礎的原則就是對原始的圖層外觀截圖,然后添加一段動畫,平滑過渡到圖層改變之后那個截圖的效果。如果我們知道如何對圖層截圖,我們就可以使用屬性動畫來代替CATransition或者是UIKit的過渡方法來實現動畫。
事實證明,對圖層做截圖還是很簡單的。CALayer有一個-renderInContext:方法,可以通過把它繪制到Core Graphics的上下文中捕獲當前內容的圖片,然后在另外的視圖中顯示出來。如果我們把這個截屏視圖置于原始視圖之上,就可以遮住真實視圖的所有變化,于是重新創建了一個簡單的過渡效果。
我們對當前視圖狀態截圖,然后在我們改變原始視圖的背景色的時候對截圖快速轉動并且淡出
為了讓事情更簡單,我們用UIView -animateWithDuration:completion:
方法來實現。雖然用CABasicAnimation可以達到同樣的效果,但是那樣的話我們就需要對圖層的變換和不透明屬性創建單獨的動畫,然后當動畫結束的是哦戶在CAAnimationDelegate中把coverView從屏幕中移除。
@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
這里有個警告:-renderInContext:捕獲了圖層的圖片和子圖層,但是不能對子圖層正確地處理變換效果,而且對視頻和OpenGL內容也不起作用。但是用CATransition,或者用私有的截屏方式就沒有這個限制了。
8.4 在動畫過程中取消動畫
之前提到過,你可以用-addAnimation:forKey:
方法中的key參數來在添加動畫之后檢索一個動畫,使用如下方法:
- (CAAnimation *)animationForKey:(NSString *)key;
但并不支持在動畫運行過程中修改動畫,所以這個方法主要用來檢測動畫的屬性,或者判斷它是否被添加到當前圖層中。
為了終止一個指定的動畫,你可以用如下方法把它從圖層移除掉:
- (void)removeAnimationForKey:(NSString *)key;
或者移除所有動畫:
- (void)removeAllAnimations;
動畫一旦被移除,圖層的外觀就立刻更新到當前的模型圖層的值。一般說來,動畫在結束之后被自動移除,除非設置removedOnCompletion
為NO
,如果你設置動畫在結束之后不被自動移除,那么當它不需要的時候你要手動移除它;否則它會一直存在于內存中,直到圖層被銷毀。
我們來擴展之前旋轉飛船的示例,這里添加一個按鈕來停止或者啟動動畫。這一次我們用一個非nil的值作為動畫的鍵,以便之后可以移除它。-animationDidStop:finished:
方法中的flag參數表明了動畫是自然結束還是被打斷,我們可以在控制臺打印出來。如果你用停止按鈕來終止動畫,它會打印NO,如果允許它完成,它會打印YES。
@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