iOS-Core-Animation之八----顯式動(dòng)畫

如果想讓事情變得順利,只有靠自己--夏爾·紀(jì)堯姆

上一章介紹了隱式動(dòng)畫的概念。隱式動(dòng)畫是在iOS平臺(tái)創(chuàng)建動(dòng)態(tài)用戶界面的一種直接方式,也是UIKit動(dòng)畫機(jī)制的基礎(chǔ),不過它并不能涵蓋所有的動(dòng)畫類型。在這一章中,我們將要研究一下*顯式動(dòng)畫*,它能夠?qū)σ恍傩宰鲋付ǖ淖远x動(dòng)畫,或者創(chuàng)建非線性動(dòng)畫,比如沿著任意一條曲線移動(dòng)。

##屬性動(dòng)畫

首先我們來探討一下*屬性動(dòng)畫*。屬性動(dòng)畫作用于圖層的某個(gè)單一屬性,并指定了它的一個(gè)目標(biāo)值,或者一連串將要做動(dòng)畫的值。屬性動(dòng)畫分為兩種:*基礎(chǔ)*和*關(guān)鍵幀*。

###基礎(chǔ)動(dòng)畫

動(dòng)畫其實(shí)就是一段時(shí)間內(nèi)發(fā)生的改變,最簡單的形式就是從一個(gè)值改變到另一個(gè)值,這也是`CABasicAnimation`最主要的功能。`CABasicAnimation`是`CAPropertyAnimation`的一個(gè)子類,`CAPropertyAnimation`同時(shí)也是Core Animation所有動(dòng)畫類型的抽象基類。作為一個(gè)抽象類,`CAAnimation`本身并沒有做多少工作,它提供了一個(gè)計(jì)時(shí)函數(shù)(見第十章“緩沖”),一個(gè)委托(用于反饋動(dòng)畫狀態(tài))以及一個(gè)`removedOnCompletion`,用于標(biāo)識(shí)動(dòng)畫是否該在結(jié)束后自動(dòng)釋放(默認(rèn)`YES`,為了防止內(nèi)存泄露)。`CAAnimation`同時(shí)實(shí)現(xiàn)了一些協(xié)議,包括`CAAction`(允許`CAAnimation`的子類可以提供圖層行為),以及`CAMediaTiming`(第九章“圖層時(shí)間”將會(huì)詳細(xì)解釋)。

`CAPropertyAnimation`通過指定動(dòng)畫的`keyPath`作用于一個(gè)單一屬性,`CAAnimation`通常應(yīng)用于一個(gè)指定的`CALayer`,于是這里指的也就是一個(gè)圖層的`keyPath`了。實(shí)際上它是一個(gè)關(guān)鍵*路徑*(一些用點(diǎn)表示法可以在層級(jí)關(guān)系中指向任意嵌套的對(duì)象),而不僅僅是一個(gè)屬性的名稱,因?yàn)檫@意味著動(dòng)畫不僅可以作用于圖層本身的屬性,而且還包含了它的子成員的屬性,甚至是一些*虛擬*的屬性(后面會(huì)詳細(xì)解釋)。

`CABasicAnimation`繼承于`CAPropertyAnimation`,并添加了如下屬性:

id fromValue

id toValue

id byValue

從命名就可以得到很好的解釋:`fromValue`代表了動(dòng)畫開始之前屬性的值,`toValue`代表了動(dòng)畫結(jié)束之后的值,`byValue`代表了動(dòng)畫執(zhí)行過程中改變的值。

通過組合這三個(gè)屬性就可以有很多種方式來指定一個(gè)動(dòng)畫的過程。它們被定義成`id`類型而不是一些具體的類型是因?yàn)閷傩詣?dòng)畫可以用作很多不同種的屬性類型,包括數(shù)字類型,矢量,變換矩陣,甚至是顏色或者圖片。

`id`類型可以包含任意由`NSObject`派生的對(duì)象,但有時(shí)候你會(huì)希望對(duì)一些不直接從`NSObject`繼承的屬性類型做動(dòng)畫,這意味著你需要把這些值用一個(gè)對(duì)象來封裝,或者強(qiáng)轉(zhuǎn)成一個(gè)對(duì)象,就像某些功能和Objective-C對(duì)象類似的Core Foundation類型。但是如何從一個(gè)具體的數(shù)據(jù)類型轉(zhuǎn)換成id看起來并不明顯,一些普通的例子見表8.1。

表8.1用于`CAPropertyAnimation`的一些類型轉(zhuǎn)換

Type? ? ? ? ? | Object Type | Code Example

--------------|-------------|-----------------------------------------------------

CGFloat ? ? ? | NSNumber? ? | id obj = @(float);

CGPoint ? ? ? | NSValue ? ? | id obj =[NSValue valueWithCGPoint:point);

CGSize? ? ? ? | NSValue ? ? | id obj = [NSValue valueWithCGSize:size);

CGRect| NSValue ? ? | id obj = [NSValue valueWithCGRect:rect);

CATransform3D | NSValue ? ? | id obj = [NSValue valueWithCATransform3D:transform);

CGImageRef? ? | id? ? ? ? ? | id obj = (__bridge id)imageRef;

CGColorRef? ? | id? ? ? ? ? | id obj = (__bridge id)colorRef;

`fromValue`,`toValue`和`byValue`屬性可以用很多種方式來組合,但為了防止沖突,不能一次性同時(shí)指定這三個(gè)值。例如,如果指定了`fromValue`等于2,`toValue`等于4,`byValue`等于3,那么Core Animation就不知道結(jié)果到底是4(`toValue`)還是5(`fromValue + byValue`)了。他們的用法在`CABasicAnimation`頭文件中已經(jīng)描述的很清楚了,所以在這里就不重復(fù)了??偟恼f來,就是只需要指定`toValue`或者`byValue`,剩下的值都可以通過上下文自動(dòng)計(jì)算出來。

舉個(gè)例子:我們修改一下第七章中的顏色漸變的動(dòng)畫,用顯式的`CABasicAnimation`來取代之前的隱式動(dòng)畫,代碼見清單8.1。

清單8.1通過`CABasicAnimation`來設(shè)置圖層背景色

```objective-c

@interface ViewController ()

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

@property (nonatomic, strong) IBOutlet 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

{

//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 to layer

[self.colorLayer addAnimation:animation forKey:nil];

}

@end

```

運(yùn)行程序,結(jié)果有點(diǎn)差強(qiáng)人意,點(diǎn)擊按鈕,的確可以使圖層動(dòng)畫過渡到一個(gè)新的顏色,然動(dòng)畫結(jié)束之后又立刻變回原始值。

這是因?yàn)閯?dòng)畫并沒有改變圖層的*模型*,而只是*呈現(xiàn)*(第七章)。一旦動(dòng)畫結(jié)束并從圖層上移除之后,圖層就立刻恢復(fù)到之前定義的外觀狀態(tài)。我們從沒改變過`backgroundColor`屬性,所以圖層就返回到原始的顏色。

當(dāng)之前在使用隱式動(dòng)畫的時(shí)候,實(shí)際上它就是用例子中`CABasicAnimation`來實(shí)現(xiàn)的(回憶第七章,我們在`-actionForLayer:forKey:`委托方法打印出來的結(jié)果就是`CABasicAnimation`)。但是在那個(gè)例子中,我們通過設(shè)置屬性來打開動(dòng)畫。在這里我們做了相同的動(dòng)畫,但是并沒有設(shè)置任何屬性的值(這就是為什么會(huì)立刻變回初始狀態(tài)的原因)。

把動(dòng)畫設(shè)置成一個(gè)圖層的行為(然后通過改變屬性值來啟動(dòng)動(dòng)畫)是到目前為止同步屬性值和動(dòng)畫狀態(tài)最簡單的方式了,假設(shè)由于某些原因我們不能這么做(通常因?yàn)閌UIView`關(guān)聯(lián)的圖層不能這么做動(dòng)畫),那么有兩種可以更新屬性值的方式:在動(dòng)畫開始之前或者動(dòng)畫結(jié)束之后。

動(dòng)畫之前改變屬性的值是最簡單的辦法,但這意味著我們不能使用`fromValue`這么好的特性了,而且要手動(dòng)將`fromValue`設(shè)置成圖層當(dāng)前的值。

于是在動(dòng)畫創(chuàng)建之前插入如下代碼,就可以解決問題了

animation.fromValue = (__bridge id)self.colorLayer.backgroundColor;

self.colorLayer.backgroundColor = color.CGColor;

這的確是可行的,但還是有些問題,如果這里已經(jīng)正在進(jìn)行一段動(dòng)畫,我們需要從*呈現(xiàn)*圖層那里去獲得`fromValue`,而不是模型圖層。另外,由于這里的圖層并不是`UIView`關(guān)聯(lián)的圖層,我們需要用`CATransaction`來禁用隱式動(dòng)畫行為,否則默認(rèn)的圖層行為會(huì)干擾我們的顯式動(dòng)畫(實(shí)際上,顯式動(dòng)畫通常會(huì)覆蓋隱式動(dòng)畫,但在文章中并沒有提到,所以為了安全最好這么做)。

更新之后的代碼如下:

```objective-c

CALayer *layer = self.colorLayer.presentationLayer ?:

self.colorLayer;

animation.fromValue = (__bridge id)layer.backgroundColor;

[CATransaction begin];

[CATransaction setDisableActions:YES];

self.colorLayer.backgroundColor = color.CGColor;

[CATransaction commit];

```

如果給每個(gè)動(dòng)畫都添加這些,代碼會(huì)顯得特別臃腫。幸運(yùn)的是,我們可以從`CABasicAnimation`去自動(dòng)設(shè)置這些。于是可以創(chuàng)建一個(gè)可復(fù)用的代碼。清單8.2修改了之前的示例,通過使用`CABasicAnimation`的一個(gè)函數(shù)來避免在每次動(dòng)畫時(shí)候都重復(fù)那些臃腫的代碼。

清單8.2修改動(dòng)畫立刻恢復(fù)到原始狀態(tài)的一個(gè)可復(fù)用函數(shù)

```objective-c

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

}

```

這種簡單的實(shí)現(xiàn)方式通過`toValue`而不是`byValue`來處理動(dòng)畫,不過這已經(jīng)是朝更好的解決方案邁出一大步了。你可以把它添加給`CALayer`作為一個(gè)分類,以方便更好地使用。

解決看起來如此簡單的一個(gè)問題都著實(shí)麻煩,但是別的方案會(huì)更加復(fù)雜。如果不在動(dòng)畫開始之前去更新目標(biāo)屬性,那么就只能在動(dòng)畫完全結(jié)束或者取消的時(shí)候更新它。這意味著我們需要精準(zhǔn)地在動(dòng)畫結(jié)束之后,圖層返回到原始值之前更新屬性。那么該如何找到這個(gè)點(diǎn)呢?

###CAAnimationDelegate

在第七章使用隱式動(dòng)畫的時(shí)候,我們可以在`CATransaction`完成塊中檢測到動(dòng)畫的完成。但是這種方式并不適用于顯式動(dòng)畫,因?yàn)檫@里的動(dòng)畫和事務(wù)并沒太多關(guān)聯(lián)。

那么為了知道一個(gè)顯式動(dòng)畫在何時(shí)結(jié)束,我們需要使用一個(gè)實(shí)現(xiàn)了`CAAnimationDelegate`協(xié)議的`delegate`。

`CAAnimationDelegate`在任何頭文件中都找不到,但是可以在`CAAnimation`頭文件或者蘋果開發(fā)者文檔中找到相關(guān)函數(shù)。在這個(gè)例子中,我們用`-animationDidStop:finished:`方法在動(dòng)畫結(jié)束之后來更新圖層的`backgroundColor`。

當(dāng)更新屬性的時(shí)候,我們需要設(shè)置一個(gè)新的事務(wù),并且禁用圖層行為。否則動(dòng)畫會(huì)發(fā)生兩次,一個(gè)是因?yàn)轱@式的`CABasicAnimation`,另一次是因?yàn)殡[式動(dòng)畫,具體實(shí)現(xiàn)見訂單8.3。

清單8.3動(dòng)畫完成之后修改圖層的背景色

```objective-c

@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

```

對(duì)`CAAnimation`而言,使用委托模式而不是一個(gè)完成塊會(huì)帶來一個(gè)問題,就是當(dāng)你有多個(gè)動(dòng)畫的時(shí)候,無法在在回調(diào)方法中區(qū)分。在一個(gè)視圖控制器中創(chuàng)建動(dòng)畫的時(shí)候,通常會(huì)用控制器本身作為一個(gè)委托(如清單8.3所示),但是所有的動(dòng)畫都會(huì)調(diào)用同一個(gè)回調(diào)方法,所以你就需要判斷到底是那個(gè)圖層的調(diào)用。

考慮一下第三章的鬧鐘,“圖層幾何學(xué)”,我們通過簡單地每秒更新指針的角度來實(shí)現(xiàn)一個(gè)鐘,但如果指針動(dòng)態(tài)地轉(zhuǎn)向新的位置會(huì)更加真實(shí)。

我們不能通過隱式動(dòng)畫來實(shí)現(xiàn)因?yàn)檫@些指針都是`UIView`的實(shí)例,所以圖層的隱式動(dòng)畫都被禁用了。我們可以簡單地通過`UIView`的動(dòng)畫方法來實(shí)現(xiàn)。但如果想更好地控制動(dòng)畫時(shí)間,使用顯式動(dòng)畫會(huì)更好(更多內(nèi)容見第十章)。使用`CABasicAnimation`來做動(dòng)畫可能會(huì)更加復(fù)雜,因?yàn)槲覀冃枰赻-animationDidStop:finished:`中檢測指針狀態(tài)(用于設(shè)置結(jié)束的位置)。

動(dòng)畫本身會(huì)作為一個(gè)參數(shù)傳入委托的方法,也許你會(huì)認(rèn)為可以控制器中把動(dòng)畫存儲(chǔ)為一個(gè)屬性,然后在回調(diào)用比較,但實(shí)際上并不起作用,因?yàn)槲袀魅氲膭?dòng)畫參數(shù)是原始值的一個(gè)深拷貝,從而不是同一個(gè)值。

當(dāng)使用`-addAnimation:forKey:`把動(dòng)畫添加到圖層,這里有一個(gè)到目前為止我們都設(shè)置為`nil`的`key`參數(shù)。這里的鍵是`-animationForKey:`方法找到對(duì)應(yīng)動(dòng)畫的唯一標(biāo)識(shí)符,而當(dāng)前動(dòng)畫的所有鍵都可以用`animationKeys`獲取。如果我們對(duì)每個(gè)動(dòng)畫都關(guān)聯(lián)一個(gè)唯一的鍵,就可以對(duì)每個(gè)圖層循環(huán)所有鍵,然后調(diào)用`-animationForKey:`來比對(duì)結(jié)果。盡管這不是一個(gè)優(yōu)雅的實(shí)現(xiàn)。

幸運(yùn)的是,還有一種更加簡單的方法。像所有的`NSObject`子類一樣,`CAAnimation`實(shí)現(xiàn)了KVC(鍵-值-編碼)協(xié)議,于是你可以用`-setValue:forKey:`和`-valueForKey:`方法來存取屬性。但是`CAAnimation`有一個(gè)不同的性能:它更像一個(gè)`NSDictionary`,可以讓你隨意設(shè)置鍵值對(duì),即使和你使用的動(dòng)畫類所聲明的屬性并不匹配。

這意味著你可以對(duì)動(dòng)畫用任意類型打標(biāo)簽。在這里,我們給`UIView`類型的指針添加的動(dòng)畫,所以可以簡單地判斷動(dòng)畫到底屬于哪個(gè)視圖,然后在委托方法中用這個(gè)信息正確地更新鐘的指針(清單8.4)。

清單8.4使用KVC對(duì)動(dòng)畫打標(biāo)簽

```objective-c

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIImageView *hourHand;

@property (nonatomic, weak) IBOutlet UIImageView *minuteHand;

@property (nonatomic, weak) IBOutlet UIImageView *secondHand;

@property (nonatomic, weak) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad

{

[super viewDidLoad];

//adjust anchor points

self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);

self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);

self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);

//start timer

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];

//set initial hand positions

[self updateHandsAnimated:NO];

}

- (void)tick

{

[self updateHandsAnimated:YES];

}

- (void)updateHandsAnimated:(BOOL)animated

{

//convert time to hours, minutes and seconds

NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];

NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;

NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];

CGFloat hourAngle = (components.hour / 12.0) * M_PI * 2.0;

//calculate hour hand angle //calculate minute hand angle

CGFloat minuteAngle = (components.minute / 60.0) * M_PI * 2.0;

//calculate second hand angle

CGFloat secondAngle = (components.second / 60.0) * M_PI * 2.0;

//rotate hands

[self setAngle:hourAngle forHand:self.hourHand animated:animated];

[self setAngle:minuteAngle forHand:self.minuteHand animated:animated];

[self setAngle:secondAngle forHand:self.secondHand animated:animated];

}

- (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated

{

//generate transform

CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1);

if (animated) {

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

} else {

//set transform directly

handView.layer.transform = transform;

}

}

- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag

{

//set final position for hand view

UIView *handView = [anim valueForKey:@"handView"];

handView.layer.transform = [anim.toValue CATransform3DValue];

}

```

我們成功的識(shí)別出每個(gè)圖層停止動(dòng)畫的時(shí)間,然后更新它的變換到一個(gè)新值,很好。

不幸的是,即使做了這些,還是有個(gè)問題,清單8.4在模擬器上運(yùn)行的很好,但當(dāng)真正跑在iOS設(shè)備上時(shí),我們發(fā)現(xiàn)在`-animationDidStop:finished:`委托方法調(diào)用之前,指針會(huì)迅速返回到原始值,這個(gè)清單8.3圖層顏色發(fā)生的情況一樣。

問題在于回調(diào)方法在動(dòng)畫完成之前已經(jīng)被調(diào)用了,但不能保證這發(fā)生在屬性動(dòng)畫返回初始狀態(tài)之前。這同時(shí)也很好地說明了為什么要在真實(shí)的設(shè)備上測試動(dòng)畫代碼,而不僅僅是模擬器。

我們可以用一個(gè)`fillMode`屬性來解決這個(gè)問題,下一章會(huì)詳細(xì)說明,這里知道在動(dòng)畫之前設(shè)置它比在動(dòng)畫結(jié)束之后更新屬性更加方便。

###關(guān)鍵幀動(dòng)畫

`CABasicAnimation`揭示了大多數(shù)隱式動(dòng)畫背后依賴的機(jī)制,這的確很有趣,但是顯式地給圖層添加`CABasicAnimation`相較于隱式動(dòng)畫而言,只能說費(fèi)力不討好。

`CAKeyframeAnimation`是另一種UIKit沒有暴露出來但功能強(qiáng)大的類。和`CABasicAnimation`類似,`CAKeyframeAnimation`同樣是`CAPropertyAnimation`的一個(gè)子類,它依然作用于單一的一個(gè)屬性,但是和`CABasicAnimation`不一樣的是,它不限制于設(shè)置一個(gè)起始和結(jié)束的值,而是可以根據(jù)一連串隨意的值來做動(dòng)畫。

*關(guān)鍵幀*起源于傳動(dòng)動(dòng)畫,意思是指主導(dǎo)的動(dòng)畫在顯著改變發(fā)生時(shí)重繪當(dāng)前幀(也就是*關(guān)鍵*幀),每幀之間剩下的繪制(可以通過關(guān)鍵幀推算出)將由熟練的藝術(shù)家來完成。`CAKeyframeAnimation`也是同樣的道理:你提供了顯著的幀,然后Core Animation在每幀之間進(jìn)行插入。

我們可以用之前使用顏色圖層的例子來演示,設(shè)置一個(gè)顏色的數(shù)組,然后通過關(guān)鍵幀動(dòng)畫播放出來(清單8.5)

清單8.5使用`CAKeyframeAnimation`應(yīng)用一系列顏色的變化

```objective-c

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

}

```

注意到序列中開始和結(jié)束的顏色都是藍(lán)色,這是因?yàn)閌CAKeyframeAnimation`并不能自動(dòng)把當(dāng)前值作為第一幀(就像`CABasicAnimation`那樣把`fromValue`設(shè)為`nil`)。動(dòng)畫會(huì)在開始的時(shí)候突然跳轉(zhuǎn)到第一幀的值,然后在動(dòng)畫結(jié)束的時(shí)候突然恢復(fù)到原始的值。所以為了動(dòng)畫的平滑特性,我們需要開始和結(jié)束的關(guān)鍵幀來匹配當(dāng)前屬性的值。

當(dāng)然可以創(chuàng)建一個(gè)結(jié)束和開始值不同的動(dòng)畫,那樣的話就需要在動(dòng)畫啟動(dòng)之前手動(dòng)更新屬性和最后一幀的值保持一致,就和之前討論的一樣。

我們用`duration`屬性把動(dòng)畫時(shí)間從默認(rèn)的0.25秒增加到2秒,以便于動(dòng)畫做的不那么快。運(yùn)行它,你會(huì)發(fā)現(xiàn)動(dòng)畫通過顏色不斷循環(huán),但效果看起來有些*奇怪*。原因在于動(dòng)畫以一個(gè)*恒定的步調(diào)*在運(yùn)行。當(dāng)在每個(gè)動(dòng)畫之間過渡的時(shí)候并沒有減速,這就產(chǎn)生了一個(gè)略微奇怪的效果,為了讓動(dòng)畫看起來更自然,我們需要調(diào)整一下*緩沖*,第十章將會(huì)詳細(xì)說明。

提供一個(gè)數(shù)組的值就可以按照顏色變化做動(dòng)畫,但一般這不是直觀的方式去描述一段運(yùn)用。`CAKeyframeAnimation`有另一種方式去指定動(dòng)畫,就是使用`CGPath`。`path`屬性可以用一種直觀的方式,使用Core Graphics函數(shù)定義運(yùn)動(dòng)的序列來繪制動(dòng)畫。

我們來用一個(gè)宇宙飛船沿著一個(gè)簡單曲線的實(shí)例演示一下。為了創(chuàng)建路徑,我們需要使用一個(gè)*三次貝塞爾曲線*,它是一種使用開始點(diǎn),結(jié)束點(diǎn)和另外兩個(gè)*控制點(diǎn)*來定義形狀的曲線,可以通過使用一個(gè)基于C的Core Graphics繪圖指令來創(chuàng)建,不過用UIKit提供的`UIBezierPath`類會(huì)更簡單。

我們這次用`CAShapeLayer`來在屏幕上繪制曲線,盡管對(duì)動(dòng)畫來說并不是必須的,但這會(huì)讓我們的動(dòng)畫更加形象。繪制完`CGPath`之后,我們用它來創(chuàng)建一個(gè)`CAKeyframeAnimation`,然后用它來應(yīng)用到我們的宇宙飛船。代碼見清單8.6,結(jié)果見圖8.1。

清單8.6沿著一個(gè)貝塞爾曲線對(duì)圖層做動(dòng)畫

```objective-c

@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

```

圖8.1沿著一個(gè)貝塞爾曲線移動(dòng)的宇宙飛船圖片

運(yùn)行示例,你會(huì)發(fā)現(xiàn)飛船的動(dòng)畫有些不太真實(shí),這是因?yàn)楫?dāng)它運(yùn)動(dòng)的時(shí)候永遠(yuǎn)指向右邊,而不是指向曲線切線的方向。你可以調(diào)整它的`affineTransform`來對(duì)運(yùn)動(dòng)方向做動(dòng)畫,但很可能和其它的動(dòng)畫沖突。

幸運(yùn)的是,蘋果預(yù)見到了這點(diǎn),并且給`CAKeyFrameAnimation`添加了一個(gè)`rotationMode`的屬性。設(shè)置它為常量`kCAAnimationRotateAuto`(清單8.7),圖層將會(huì)根據(jù)曲線的切線自動(dòng)旋轉(zhuǎn)(圖8.2)。

清單8.7通過`rotationMode`自動(dòng)對(duì)齊圖層到曲線

```objective-c

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

}

```

圖8.2匹配曲線切線方向的飛船圖層

###虛擬屬性

之前提到過屬性動(dòng)畫實(shí)際上是針對(duì)于關(guān)鍵*路徑*而不是一個(gè)鍵,這就意味著可以對(duì)子屬性甚至是*虛擬屬性*做動(dòng)畫。但是*虛擬*屬性到底是什么呢?

考慮一個(gè)旋轉(zhuǎn)的動(dòng)畫:如果想要對(duì)一個(gè)物體做旋轉(zhuǎn)的動(dòng)畫,那就需要作用于`transform`屬性,因?yàn)閌CALayer`沒有顯式提供角度或者方向之類的屬性,代碼如清單8.8所示

清單8.8用`transform`屬性對(duì)圖層做動(dòng)畫

```objective-c

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

animation.duration = 2.0;

animation.toValue = [NSValue valueWithCATransform3D: CATransform3DMakeRotation(M_PI, 0, 0, 1)];

[shipLayer addAnimation:animation forKey:nil];

}

@end

```

這么做是可行的,但看起來更因?yàn)槭沁\(yùn)氣而不是設(shè)計(jì)的原因,如果我們把旋轉(zhuǎn)的值從`M_PI`(180度)調(diào)整到`2 * M_PI`(360度),然后運(yùn)行程序,會(huì)發(fā)現(xiàn)這時(shí)候飛船完全不動(dòng)了。這是因?yàn)檫@里的矩陣做了一次360度的旋轉(zhuǎn),和做了0度是一樣的,所以最后的值根本沒變。

現(xiàn)在繼續(xù)使用`M_PI`,但這次用`byValue`而不是`toValue`。也許你會(huì)認(rèn)為這和設(shè)置`toValue`結(jié)果一樣,因?yàn)? + 90度== 90度,但實(shí)際上飛船的圖片變大了,并沒有做任何旋轉(zhuǎn),這是因?yàn)樽儞Q矩陣不能像角度值那樣疊加。

那么如果需要獨(dú)立于角度之外單獨(dú)對(duì)平移或者縮放做動(dòng)畫呢?由于都需要我們來修改`transform`屬性,實(shí)時(shí)地重新計(jì)算每個(gè)時(shí)間點(diǎn)的每個(gè)變換效果,然后根據(jù)這些創(chuàng)建一個(gè)復(fù)雜的關(guān)鍵幀動(dòng)畫,這一切都是為了對(duì)圖層的一個(gè)獨(dú)立做一個(gè)簡單的動(dòng)畫。

幸運(yùn)的是,有一個(gè)更好的解決方案:為了旋轉(zhuǎn)圖層,我們可以對(duì)`transform.rotation`關(guān)鍵路徑應(yīng)用動(dòng)畫,而不是`transform`本身(清單8.9)。

清單8.9對(duì)虛擬的`transform.rotation`屬性做動(dòng)畫

```objective-c

@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

```

結(jié)果運(yùn)行的特別好,用`transform.rotation`而不是`transform`做動(dòng)畫的好處如下:

*我們可以不通過關(guān)鍵幀一步旋轉(zhuǎn)多于180度的動(dòng)畫。

*可以用相對(duì)值而不是絕對(duì)值旋轉(zhuǎn)(設(shè)置`byValue`而不是`toValue`)。

*可以不用創(chuàng)建`CATransform3D`,而是使用一個(gè)簡單的數(shù)值來指定角度。

*不會(huì)和`transform.position`或者`transform.scale`沖突(同樣是使用關(guān)鍵路徑來做獨(dú)立的動(dòng)畫屬性)。

`transform.rotation`屬性有一個(gè)奇怪的問題是它其實(shí)*并不存在*。這是因?yàn)閌CATransform3D`并不是一個(gè)對(duì)象,它實(shí)際上是一個(gè)結(jié)構(gòu)體,也沒有符合KVC相關(guān)屬性,`transform.rotation`實(shí)際上是一個(gè)`CALayer`用于處理動(dòng)畫變換的*虛擬*屬性。

你不可以直接設(shè)置`transform.rotation`或者`transform.scale`,他們不能被直接使用。當(dāng)你對(duì)他們做動(dòng)畫時(shí),Core Animation自動(dòng)地根據(jù)通過`CAValueFunction`來計(jì)算的值來更新`transform`屬性。

`CAValueFunction`用于把我們賦給虛擬的`transform.rotation`簡單浮點(diǎn)值轉(zhuǎn)換成真正的用于擺放圖層的`CATransform3D`矩陣值。你可以通過設(shè)置`CAPropertyAnimation`的`valueFunction`屬性來改變,于是你設(shè)置的函數(shù)將會(huì)覆蓋默認(rèn)的函數(shù)。

`CAValueFunction`看起來似乎是對(duì)那些不能簡單相加的屬性(例如變換矩陣)做動(dòng)畫的非常有用的機(jī)制,但由于`CAValueFunction`的實(shí)現(xiàn)細(xì)節(jié)是私有的,所以目前不能通過繼承它來自定義。你可以通過使用蘋果目前已近提供的常量(目前都是和變換矩陣的虛擬屬性相關(guān),所以沒太多使用場景了,因?yàn)檫@些屬性都有了默認(rèn)的實(shí)現(xiàn)方式)。

##動(dòng)畫組

`CABasicAnimation`和`CAKeyframeAnimation`僅僅作用于單獨(dú)的屬性,而`CAAnimationGroup`可以把這些動(dòng)畫組合在一起。`CAAnimationGroup`是另一個(gè)繼承于`CAAnimation`的子類,它添加了一個(gè)`animations`數(shù)組的屬性,用來組合別的動(dòng)畫。我們把清單8.6那種關(guān)鍵幀動(dòng)畫和調(diào)整圖層背景色的基礎(chǔ)動(dòng)畫組合起來(清單8.10),結(jié)果如圖8.3所示。

清單8.10組合關(guān)鍵幀動(dòng)畫和基礎(chǔ)動(dòng)畫

```objective-c

- (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關(guān)鍵幀路徑和基礎(chǔ)動(dòng)畫的組合

##過渡

有時(shí)候?qū)τ趇OS應(yīng)用程序來說,希望能通過屬性動(dòng)畫來對(duì)比較難做動(dòng)畫的布局進(jìn)行一些改變。比如交換一段文本和圖片,或者用一段網(wǎng)格視圖來替換,等等。屬性動(dòng)畫只對(duì)圖層的可動(dòng)畫屬性起作用,所以如果要改變一個(gè)不能動(dòng)畫的屬性(比如圖片),或者從層級(jí)關(guān)系中添加或者移除圖層,屬性動(dòng)畫將不起作用。

于是就有了過渡的概念。過渡并不像屬性動(dòng)畫那樣平滑地在兩個(gè)值之間做動(dòng)畫,而是影響到整個(gè)圖層的變化。過渡動(dòng)畫首先展示之前的圖層外觀,然后通過一個(gè)交換過渡到新的外觀。

為了創(chuàng)建一個(gè)過渡動(dòng)畫,我們將使用`CATransition`,同樣是另一個(gè)`CAAnimation`的子類,和別的子類不同,`CAAnimation`有一個(gè)`type`和`subtype`來標(biāo)識(shí)變換效果。`type`屬性是一個(gè)`NSString`類型,可以被設(shè)置成如下類型:

kCATransitionFade

kCATransitionMoveIn

kCATransitionPush

kCATransitionReveal

到目前為止你只能使用上述四種類型,但你可以通過一些別的方法來自定義過渡效果,后續(xù)會(huì)詳細(xì)介紹。

默認(rèn)的過渡類型是`kCATransitionFade`,當(dāng)你在改變圖層屬性之后,就創(chuàng)建了一個(gè)平滑的淡入淡出效果。

我們在第七章的例子中就已經(jīng)用到過`kCATransitionPush`,它創(chuàng)建了一個(gè)新的圖層,從邊緣的一側(cè)滑動(dòng)進(jìn)來,把舊圖層從另一側(cè)推出去的效果。

`kCATransitionMoveIn`和`kCATransitionReveal`與`kCATransitionPush`類似,都實(shí)現(xiàn)了一個(gè)定向滑動(dòng)的動(dòng)畫,但是有一些細(xì)微的不同,`kCATransitionMoveIn`從頂部滑動(dòng)進(jìn)入,但不像推送動(dòng)畫那樣把老土層推走,然而`kCATransitionReveal`把原始的圖層滑動(dòng)出去來顯示新的外觀,而不是把新的圖層滑動(dòng)進(jìn)入。

后面三種過渡類型都有一個(gè)默認(rèn)的動(dòng)畫方向,它們都從左側(cè)滑入,但是你可以通過`subtype`來控制它們的方向,提供了如下四種類型:

kCATransitionFromRight

kCATransitionFromLeft

kCATransitionFromTop

kCATransitionFromBottom

一個(gè)簡單的用`CATransition`來對(duì)非動(dòng)畫屬性做動(dòng)畫的例子如清單8.11所示,這里我們對(duì)`UIImage`的`image`屬性做修改,但是隱式動(dòng)畫或者`CAPropertyAnimation`都不能對(duì)它做動(dòng)畫,因?yàn)镃ore Animation不知道如何在插圖圖片。通過對(duì)圖層應(yīng)用一個(gè)淡入淡出的過渡,我們可以忽略它的內(nèi)容來做平滑動(dòng)畫(圖8.4),我們來嘗試修改過渡的`type`常量來觀察其它效果。

清單8.11使用`CATransition`來對(duì)`UIImageView`做動(dòng)畫

```objective-c

@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

```

你可以從代碼中看出,過渡動(dòng)畫和之前的屬性動(dòng)畫或者動(dòng)畫組添加到圖層上的方式一致,都是通過`-addAnimation:forKey:`方法。但是和屬性動(dòng)畫不同的是,對(duì)指定的圖層一次只能使用一次`CATransition`,因此,無論你對(duì)動(dòng)畫的鍵設(shè)置什么值,過渡動(dòng)畫都會(huì)對(duì)它的鍵設(shè)置成“transition”,也就是常量`kCATransition`。

圖8.4使用`CATransition`對(duì)圖像平滑淡入淡出

###隱式過渡

`CATransision`可以對(duì)圖層任何變化平滑過渡的事實(shí)使得它成為那些不好做動(dòng)畫的屬性圖層行為的理想候選。蘋果當(dāng)然意識(shí)到了這點(diǎn),并且當(dāng)設(shè)置了`CALayer`的`content`屬性的時(shí)候,`CATransition`的確是默認(rèn)的行為。但是對(duì)于視圖關(guān)聯(lián)的圖層,或者是其他隱式動(dòng)畫的行為,這個(gè)特性依然是被禁用的,但是對(duì)于你自己創(chuàng)建的圖層,這意味著對(duì)圖層`contents`圖片做的改動(dòng)都會(huì)自動(dòng)附上淡入淡出的動(dòng)畫。

我們在第七章使用`CATransition`作為一個(gè)圖層行為來改變圖層的背景色,當(dāng)然`backgroundColor`屬性可以通過正常的`CAPropertyAnimation`來實(shí)現(xiàn),但這不是說不可以用`CATransition`來實(shí)行。

###對(duì)圖層樹的動(dòng)畫

`CATransition`并不作用于指定的圖層屬性,這就是說你可以在即使不能準(zhǔn)確得知改變了什么的情況下對(duì)圖層做動(dòng)畫,例如,在不知道`UITableView`哪一行被添加或者刪除的情況下,直接就可以平滑地刷新它,或者在不知道`UIViewController`內(nèi)部的視圖層級(jí)的情況下對(duì)兩個(gè)不同的實(shí)例做過渡動(dòng)畫。

這些例子和我們之前所討論的情況完全不同,因?yàn)樗鼈儾粌H涉及到圖層的屬性,而且是整個(gè)*圖層樹*的改變--我們在這種動(dòng)畫的過程中手動(dòng)在層級(jí)關(guān)系中添加或者移除圖層。

這里用到了一個(gè)小詭計(jì),要確保`CATransition`添加到的圖層在過渡動(dòng)畫發(fā)生時(shí)不會(huì)在樹狀結(jié)構(gòu)中被移除,否則`CATransition`將會(huì)和圖層一起被移除。一般來說,你只需要將動(dòng)畫添加到被影響圖層的`superlayer`。

在清單8.2中,我們展示了如何在`UITabBarController`切換標(biāo)簽的時(shí)候添加淡入淡出的動(dòng)畫。這里我們建立了默認(rèn)的標(biāo)簽應(yīng)用程序模板,然后用`UITabBarControllerDelegate`的`-tabBarController:didSelectViewController:`方法來應(yīng)用過渡動(dòng)畫。我們把動(dòng)畫添加到`UITabBarController`的視圖圖層上,于是在標(biāo)簽被替換的時(shí)候動(dòng)畫不會(huì)被移除。

清單8.12對(duì)`UITabBarController`做動(dòng)畫

```objective-c

#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

```

###自定義動(dòng)畫

我們證實(shí)了過渡是一種對(duì)那些不太好做平滑動(dòng)畫屬性的強(qiáng)大工具,但是`CATransition`的提供的動(dòng)畫類型太少了。

更奇怪的是蘋果通過`UIView +transitionFromView:toView:duration:options:completion:`和`+transitionWithView:duration:options:animations:`方法提供了Core Animation的過渡特性。但是這里的可用的過渡選項(xiàng)和`CATransition`的`type`屬性提供的常量*完全不同*。`UIView`過渡方法中`options`參數(shù)可以由如下常量指定:

UIViewAnimationOptionTransitionFlipFromLeft

UIViewAnimationOptionTransitionFlipFromRight

UIViewAnimationOptionTransitionCurlUp

UIViewAnimationOptionTransitionCurlDown

UIViewAnimationOptionTransitionCrossDissolve

UIViewAnimationOptionTransitionFlipFromTop

UIViewAnimationOptionTransitionFlipFromBottom

除了`UIViewAnimationOptionTransitionCrossDissolve`之外,剩下的值和`CATransition`類型完全沒關(guān)系。你可以用之前例子修改過的版本來測試一下(見清單8.13)。

清單8.13使用UIKit提供的方法來做過渡動(dòng)畫

```objective-c

@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`來創(chuàng)建其它的過渡效果。然是直到iOS6都做不到這點(diǎn)。試圖對(duì)`CATransition`使用Core Image的濾鏡完全沒效果(但是在Mac OS中是可行的,也許文檔是想表達(dá)這個(gè)意思)。

因此,根據(jù)要實(shí)現(xiàn)的效果,你只用關(guān)心是用`CATransition`還是用`UIView`的過渡方法就可以了。希望下個(gè)版本的iOS系統(tǒng)可以通過`CATransition`很好的支持Core Image的過渡濾鏡效果(或許甚至?xí)行碌姆椒ǎ?/p>

但這并不意味著在iOS上就不能實(shí)現(xiàn)自定義的過渡效果了。這只是意味著你需要做一些額外的工作。就像之前提到的那樣,過渡動(dòng)畫做基礎(chǔ)的原則就是對(duì)原始的圖層外觀截圖,然后添加一段動(dòng)畫,平滑過渡到圖層改變之后那個(gè)截圖的效果。如果我們知道如何對(duì)圖層截圖,我們就可以使用屬性動(dòng)畫來代替`CATransition`或者是UIKit的過渡方法來實(shí)現(xiàn)動(dòng)畫。

事實(shí)證明,對(duì)圖層做截圖還是很簡單的。`CALayer`有一個(gè)`-renderInContext:`方法,可以通過把它繪制到Core Graphics的上下文中捕獲當(dāng)前內(nèi)容的圖片,然后在另外的視圖中顯示出來。如果我們把這個(gè)截屏視圖置于原始視圖之上,就可以遮住真實(shí)視圖的所有變化,于是重新創(chuàng)建了一個(gè)簡單的過渡效果。

清單8.14演示了一個(gè)基本的實(shí)現(xiàn)。我們對(duì)當(dāng)前視圖狀態(tài)截圖,然后在我們改變原始視圖的背景色的時(shí)候?qū)貓D快速轉(zhuǎn)動(dòng)并且淡出,圖8.5展示了我們自定義的過渡效果。

為了讓事情更簡單,我們用`UIView -animateWithDuration:completion:`方法來實(shí)現(xiàn)。雖然用`CABasicAnimation`可以達(dá)到同樣的效果,但是那樣的話我們就需要對(duì)圖層的變換和不透明屬性創(chuàng)建單獨(dú)的動(dòng)畫,然后當(dāng)動(dòng)畫結(jié)束的是哦戶在`CAAnimationDelegate`中把`coverView`從屏幕中移除。

清單8.14用`renderInContext:`創(chuàng)建自定義過渡效果

```objective-c

@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:`創(chuàng)建自定義過渡效果

這里有個(gè)警告:`-renderInContext:`捕獲了圖層的圖片和子圖層,但是不能對(duì)子圖層正確地處理變換效果,而且對(duì)視頻和OpenGL內(nèi)容也不起作用。但是用`CATransition`,或者用私有的截屏方式就沒有這個(gè)限制了。

##在動(dòng)畫過程中取消動(dòng)畫

之前提到過,你可以用`-addAnimation:forKey:`方法中的`key`參數(shù)來在添加動(dòng)畫之后檢索一個(gè)動(dòng)畫,使用如下方法:

- (CAAnimation *)animationForKey:(NSString *)key;

但并不支持在動(dòng)畫運(yùn)行過程中修改動(dòng)畫,所以這個(gè)方法主要用來檢測動(dòng)畫的屬性,或者判斷它是否被添加到當(dāng)前圖層中。

為了終止一個(gè)指定的動(dòng)畫,你可以用如下方法把它從圖層移除掉:

- (void)removeAnimationForKey:(NSString *)key;

或者移除所有動(dòng)畫:

- (void)removeAllAnimations;

動(dòng)畫一旦被移除,圖層的外觀就立刻更新到當(dāng)前的模型圖層的值。一般說來,動(dòng)畫在結(jié)束之后被自動(dòng)移除,除非設(shè)置`removedOnCompletion`為`NO`,如果你設(shè)置動(dòng)畫在結(jié)束之后不被自動(dòng)移除,那么當(dāng)它不需要的時(shí)候你要手動(dòng)移除它;否則它會(huì)一直存在于內(nèi)存中,直到圖層被銷毀。

我們來擴(kuò)展之前旋轉(zhuǎn)飛船的示例,這里添加一個(gè)按鈕來停止或者啟動(dòng)動(dòng)畫。這一次我們用一個(gè)非`nil`的值作為動(dòng)畫的鍵,以便之后可以移除它。`-animationDidStop:finished:`方法中的`flag`參數(shù)表明了動(dòng)畫是自然結(jié)束還是被打斷,我們可以在控制臺(tái)打印出來。如果你用停止按鈕來終止動(dòng)畫,它會(huì)打印`NO`,如果允許它完成,它會(huì)打印`YES`。

清單8.15是更新后的示例代碼,圖8.6顯示了結(jié)果。

清單8.15開始和停止一個(gè)動(dòng)畫

```objective-c

@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通過開始和停止按鈕控制的旋轉(zhuǎn)動(dòng)畫

##總結(jié)

這一章中,我們涉及了屬性動(dòng)畫(你可以對(duì)單獨(dú)的圖層屬性動(dòng)畫有更加具體的控制),動(dòng)畫組(把多個(gè)屬性動(dòng)畫組合成一個(gè)獨(dú)立單元)以及過度(影響整個(gè)圖層,可以用來對(duì)圖層的任何內(nèi)容做任何類型的動(dòng)畫,包括子圖層的添加和移除)。

在第九章中,我們繼續(xù)學(xué)習(xí)`CAMediaTiming`協(xié)議,來看一看Core Animation是怎樣處理逝去的時(shí)間。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 本文轉(zhuǎn)載自:http://www.cocoachina.com/ios/20150105/10812.html 為...
    idiot_lin閱讀 1,293評(píng)論 0 1
  • 顯式動(dòng)畫 顯式動(dòng)畫,它能夠?qū)σ恍傩宰鲋付ǖ淖远x動(dòng)畫,或者創(chuàng)建非線性動(dòng)畫,比如沿著任意一條曲線移動(dòng)。 屬性動(dòng)畫 ...
    清風(fēng)沐沐閱讀 1,975評(píng)論 1 5
  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果,實(shí)現(xiàn)這些動(dòng)畫的過程并不復(fù)雜,今天將帶大家一窺ios動(dòng)畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,566評(píng)論 6 30
  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果,實(shí)現(xiàn)這些動(dòng)畫的過程并不復(fù)雜,今天將帶大家一窺iOS動(dòng)畫全貌。在這里你可以看...
    F麥子閱讀 5,141評(píng)論 5 13
  • 書寫的很好,翻譯的也棒!感謝譯者,感謝感謝! iOS-Core-Animation-Advanced-Techni...
    錢噓噓閱讀 2,317評(píng)論 0 6