如果想讓事情變得順利,只有靠自己 -- 夏爾·紀堯姆
上一章介紹了隱式動畫的概念。隱式動畫是在iOS平台創建動態用戶界面的一種直接方式,也是UIKit動畫機制的基礎,不過它並不能涵蓋所有的動畫類型。在這一章中,我們將要研究一下*顯式動畫*,它能夠對一些屬性做指定的自定義動畫,或者創建非線性動畫,比如沿著任意一條曲線移動。
##屬性動畫
首先我們來探討一下*屬性動畫*。屬性動畫作用於圖層的某個單一屬性,並指定了它的一個目標值,或者一連串將要做動畫的值。屬性動畫分為兩種:*基礎*和*關鍵幀*。
###基礎動畫
動畫其實就是一段時間內發生的改變,最簡單的形式就是從一個值改變到另一個值,這也是`CABasicAnimation`最主要的功能。`CABasicAnimation`是`CAPropertyAnimation`的一個子類,`CAPropertyAnimation`同時也是Core Animation所有動畫類型的抽象基類。作為一個抽象類,`CAAnimation`本身並沒有做多少工作,它提供了一個計時函數(見第十章“緩沖”),一個委托(用於反饋動畫狀態)以及一個`removedOnCompletion`,用於標識動畫是否該在結束後自動釋放(默認`YES`,為了防止內存洩露)。`CAAnimation`同時實現了一些協議,包括`CAAction`(允許`CAAnimation`的子類可以提供圖層行為),以及`CAMediaTiming`(第九章“圖層時間”將會詳細解釋)。
`CAPropertyAnimation`通過指定動畫的`keyPath`作用於一個單一屬性,`CAAnimation`通常應用於一個指定的`CALayer`,於是這裡指的也就是一個圖層的`keyPath`了。實際上它是一個關鍵*路徑*(一些用點表示法可以在層級關系中指向任意嵌套的對象),而不僅僅是一個屬性的名稱,因為這意味著動畫不僅可以作用於圖層本身的屬性,而且還包含了它的子成員的屬性,甚至是一些*虛擬*的屬性(後面會詳細解釋)。
`CABasicAnimation`繼承於`CAPropertyAnimation`,並添加了如下屬性:
id fromValue
id toValue
id byValue
從命名就可以得到很好的解釋:`fromValue`代表了動畫開始之前屬性的值,`toValue`代表了動畫結束之後的值,`byValue`代表了動畫執行過程中改變的值。
通過組合這三個屬性就可以有很多種方式來指定一個動畫的過程。它們被定義成`id`類型而不是一些具體的類型是因為屬性動畫可以用作很多不同種的屬性類型,包括數字類型,矢量,變換矩陣,甚至是顏色或者圖片。
`id`類型可以包含任意由`NSObject`派生的對象,但有時候你會希望對一些不直接從`NSObject`繼承的屬性類型做動畫,這意味著你需要把這些值用一個對象來封裝,或者強轉成一個對象,就像某些功能和Objective-C對象類似的Core Foundation類型。但是如何從一個具體的數據類型轉換成id看起來並不明顯,一些普通的例子見表8.1。
表8.1 用於`CAPropertyAnimation`的一些類型轉換
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`屬性可以用很多種方式來組合,但為了防止沖突,不能一次性同時指定這三個值。例如,如果指定了`fromValue`等於2,`toValue`等於4,`byValue`等於3,那麼Core Animation就不知道結果到底是4(`toValue`)還是5(`fromValue + byValue`)了。他們的用法在`CABasicAnimation`頭文件中已經描述的很清楚了,所以在這裡就不重復了。總的說來,就是只需要指定`toValue`或者`byValue`,剩下的值都可以通過上下文自動計算出來。
舉個例子:我們修改一下第七章中的顏色漸變的動畫,用顯式的`CABasicAnimation`來取代之前的隱式動畫,代碼見清單8.1。
清單8.1 通過`CABasicAnimation`來設置圖層背景色
```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
```
運行程序,結果有點差強人意,點擊按鈕,的確可以使圖層動畫過渡到一個新的顏色,然動畫結束之後又立刻變回原始值。
這是因為動畫並沒有改變圖層的*模型*,而只是*呈現*(第七章)。一旦動畫結束並從圖層上移除之後,圖層就立刻恢復到之前定義的外觀狀態。我們從沒改變過`backgroundColor`屬性,所以圖層就返回到原始的顏色。
當之前在使用隱式動畫的時候,實際上它就是用例子中`CABasicAnimation`來實現的(回憶第七章,我們在`-actionForLayer:forKey:`委托方法打印出來的結果就是`CABasicAnimation`)。但是在那個例子中,我們通過設置屬性來打開動畫。在這裡我們做了相同的動畫,但是並沒有設置任何屬性的值(這就是為什麼會立刻變回初始狀態的原因)。
把動畫設置成一個圖層的行為(然後通過改變屬性值來啟動動畫)是到目前為止同步屬性值和動畫狀態最簡單的方式了,假設由於某些原因我們不能這麼做(通常因為`UIView`關聯的圖層不能這麼做動畫),那麼有兩種可以更新屬性值的方式:在動畫開始之前或者動畫結束之後。
動畫之前改變屬性的值是最簡單的辦法,但這意味著我們不能使用`fromValue`這麼好的特性了,而且要手動將`fromValue`設置成圖層當前的值。
於是在動畫創建之前插入如下代碼,就可以解決問題了
animation.fromValue = (__bridge id)self.colorLayer.backgroundColor;
self.colorLayer.backgroundColor = color.CGColor;
這的確是可行的,但還是有些問題,如果這裡已經正在進行一段動畫,我們需要從*呈現*圖層那裡去獲得`fromValue`,而不是模型圖層。另外,由於這裡的圖層並不是`UIView`關聯的圖層,我們需要用`CATransaction`來禁用隱式動畫行為,否則默認的圖層行為會干擾我們的顯式動畫(實際上,顯式動畫通常會覆蓋隱式動畫,但在文章中並沒有提到,所以為了安全最好這麼做)。
更新之後的代碼如下:
```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];
```
如果給每個動畫都添加這些,代碼會顯得特別臃腫。幸運的是,我們可以從`CABasicAnimation`去自動設置這些。於是可以創建一個可復用的代碼。清單8.2修改了之前的示例,通過使用`CABasicAnimation`的一個函數來避免在每次動畫時候都重復那些臃腫的代碼。
清單8.2 修改動畫立刻恢復到原始狀態的一個可復用函數
```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];
}
```
這種簡單的實現方式通過`toValue`而不是`byValue`來處理動畫,不過這已經是朝更好的解決方案邁出一大步了。你可以把它添加給`CALayer`作為一個分類,以方便更好地使用。
解決看起來如此簡單的一個問題都著實麻煩,但是別的方案會更加復雜。如果不在動畫開始之前去更新目標屬性,那麼就只能在動畫完全結束或者取消的時候更新它。這意味著我們需要精准地在動畫結束之後,圖層返回到原始值之前更新屬性。那麼該如何找到這個點呢?
###CAAnimationDelegate
在第七章使用隱式動畫的時候,我們可以在`CATransaction`完成塊中檢測到動畫的完成。但是這種方式並不適用於顯式動畫,因為這裡的動畫和事務並沒太多關聯。
那麼為了知道一個顯式動畫在何時結束,我們需要使用一個實現了`CAAnimationDelegate`協議的`delegate`。
`CAAnimationDelegate`在任何頭文件中都找不到,但是可以在`CAAnimation`頭文件或者蘋果開發者文檔中找到相關函數。在這個例子中,我們用`-animationDidStop:finished:`方法在動畫結束之後來更新圖層的`backgroundColor`。
當更新屬性的時候,我們需要設置一個新的事務,並且禁用圖層行為。否則動畫會發生兩次,一個是因為顯式的`CABasicAnimation`,另一次是因為隱式動畫,具體實現見訂單8.3。
清單8.3 動畫完成之後修改圖層的背景色
```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
```
對`CAAnimation`而言,使用委托模式而不是一個完成塊會帶來一個問題,就是當你有多個動畫的時候,無法在在回調方法中區分。在一個視圖控制器中創建動畫的時候,通常會用控制器本身作為一個委托(如清單8.3所示),但是所有的動畫都會調用同一個回調方法,所以你就需要判斷到底是那個圖層的調用。
考慮一下第三章的鬧鐘,“圖層幾何學”,我們通過簡單地每秒更新指針的角度來實現一個鐘,但如果指針動態地轉向新的位置會更加真實。
我們不能通過隱式動畫來實現因為這些指針都是`UIView`的實例,所以圖層的隱式動畫都被禁用了。我們可以簡單地通過`UIView`的動畫方法來實現。但如果想更好地控制動畫時間,使用顯式動畫會更好(更多內容見第十章)。使用`CABasicAnimation`來做動畫可能會更加復雜,因為我們需要在`-animationDidStop:finished:`中檢測指針狀態(用於設置結束的位置)。
動畫本身會作為一個參數傳入委托的方法,也許你會認為可以控制器中把動畫存儲為一個屬性,然後在回調用比較,但實際上並不起作用,因為委托傳入的動畫參數是原始值的一個深拷貝,從而不是同一個值。
當使用`-addAnimation:forKey:`把動畫添加到圖層,這裡有一個到目前為止我們都設置為`nil`的`key`參數。這裡的鍵是`-animationForKey:`方法找到對應動畫的唯一標識符,而當前動畫的所有鍵都可以用`animationKeys`獲取。如果我們對每個動畫都關聯一個唯一的鍵,就可以對每個圖層循環所有鍵,然後調用`-animationForKey:`來比對結果。盡管這不是一個優雅的實現。
幸運的是,還有一種更加簡單的方法。像所有的`NSObject`子類一樣,`CAAnimation`實現了KVC(鍵-值-編碼)協議,於是你可以用`-setValue:forKey:`和`-valueForKey:`方法來存取屬性。但是`CAAnimation`有一個不同的性能:它更像一個`NSDictionary`,可以讓你隨意設置鍵值對,即使和你使用的動畫類所聲明的屬性並不匹配。
這意味著你可以對動畫用任意類型打標簽。在這裡,我們給`UIView`類型的指針添加的動畫,所以可以簡單地判斷動畫到底屬於哪個視圖,然後在委托方法中用這個信息正確地更新鐘的指針(清單8.4)。
清單8.4 使用KVC對動畫打標簽
```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];
}
```
我們成功的識別出每個圖層停止動畫的時間,然後更新它的變換到一個新值,很好。
不幸的是,即使做了這些,還是有個問題,清單8.4在模擬器上運行的很好,但當真正跑在iOS設備上時,我們發現在`-animationDidStop:finished:`委托方法調用之前,指針會迅速返回到原始值,這個清單8.3圖層顏色發生的情況一樣。
問題在於回調方法在動畫完成之前已經被調用了,但不能保證這發生在屬性動畫返回初始狀態之前。這同時也很好地說明了為什麼要在真實的設備上測試動畫代碼,而不僅僅是模擬器。
我們可以用一個`fillMode`屬性來解決這個問題,下一章會詳細說明,這裡知道在動畫之前設置它比在動畫結束之後更新屬性更加方便。
###關鍵幀動畫
`CABasicAnimation`揭示了大多數隱式動畫背後依賴的機制,這的確很有趣,但是顯式地給圖層添加`CABasicAnimation`相較於隱式動畫而言,只能說費力不討好。
`CAKeyframeAnimation`是另一種UIKit沒有暴露出來但功能強大的類。和`CABasicAnimation`類似,`CAKeyframeAnimation`同樣是`CAPropertyAnimation`的一個子類,它依然作用於單一的一個屬性,但是和`CABasicAnimation`不一樣的是,它不限制於設置一個起始和結束的值,而是可以根據一連串隨意的值來做動畫。
*關鍵幀*起源於傳動動畫,意思是指主導的動畫在顯著改變發生時重繪當前幀(也就是*關鍵*幀),每幀之間剩下的繪制(可以通過關鍵幀推算出)將由熟練的藝術家來完成。`CAKeyframeAnimation`也是同樣的道理:你提供了顯著的幀,然後Core Animation在每幀之間進行插入。
我們可以用之前使用顏色圖層的例子來演示,設置一個顏色的數組,然後通過關鍵幀動畫播放出來(清單8.5)
清單8.5 使用`CAKeyframeAnimation`應用一系列顏色的變化
```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];
}
```
注意到序列中開始和結束的顏色都是藍色,這是因為`CAKeyframeAnimation`並不能自動把當前值作為第一幀(就像`CABasicAnimation`那樣把`fromValue`設為`nil`)。動畫會在開始的時候突然跳轉到第一幀的值,然後在動畫結束的時候突然恢復到原始的值。所以為了動畫的平滑特性,我們需要開始和結束的關鍵幀來匹配當前屬性的值。
當然可以創建一個結束和開始值不同的動畫,那樣的話就需要在動畫啟動之前手動更新屬性和最後一幀的值保持一致,就和之前討論的一樣。
我們用`duration`屬性把動畫時間從默認的0.25秒增加到2秒,以便於動畫做的不那麼快。運行它,你會發現動畫通過顏色不斷循環,但效果看起來有些*奇怪*。原因在於動畫以一個*恆定的步調*在運行。當在每個動畫之間過渡的時候並沒有減速,這就產生了一個略微奇怪的效果,為了讓動畫看起來更自然,我們需要調整一下*緩沖*,第十章將會詳細說明。
提供一個數組的值就可以按照顏色變化做動畫,但一般這不是直觀的方式去描述一段運用。`CAKeyframeAnimation`有另一種方式去指定動畫,就是使用`CGPath`。`path`屬性可以用一種直觀的方式,使用Core Graphics函數定義運動的序列來繪制動畫。
我們來用一個宇宙飛船沿著一個簡單曲線的實例演示一下。為了創建路徑,我們需要使用一個*三次貝塞爾曲線*,它是一種使用開始點,結束點和另外兩個*控制點*來定義形狀的曲線,可以通過使用一個基於C的Core Graphics繪圖指令來創建,不過用UIKit提供的`UIBezierPath`類會更簡單。
我們這次用`CAShapeLayer`來在屏幕上繪制曲線,盡管對動畫來說並不是必須的,但這會讓我們的動畫更加形象。繪制完`CGPath`之後,我們用它來創建一個`CAKeyframeAnimation`,然後用它來應用到我們的宇宙飛船。代碼見清單8.6,結果見圖8.1。
清單8.6 沿著一個貝塞爾曲線對圖層做動畫
```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 沿著一個貝塞爾曲線移動的宇宙飛船圖片
運行示例,你會發現飛船的動畫有些不太真實,這是因為當它運動的時候永遠指向右邊,而不是指向曲線切線的方向。你可以調整它的`affineTransform`來對運動方向做動畫,但很可能和其它的動畫沖突。
幸運的是,蘋果預見到了這點,並且給`CAKeyFrameAnimation`添加了一個`rotationMode`的屬性。設置它為常量`kCAAnimationRotateAuto`(清單8.7),圖層將會根據曲線的切線自動旋轉(圖8.2)。
清單8.7 通過`rotationMode`自動對齊圖層到曲線
```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 匹配曲線切線方向的飛船圖層
###虛擬屬性
之前提到過屬性動畫實際上是針對於關鍵*路徑*而不是一個鍵,這就意味著可以對子屬性甚至是*虛擬屬性*做動畫。但是*虛擬*屬性到底是什麼呢?
考慮一個旋轉的動畫:如果想要對一個物體做旋轉的動畫,那就需要作用於`transform`屬性,因為`CALayer`沒有顯式提供角度或者方向之類的屬性,代碼如清單8.8所示
清單8.8 用`transform`屬性對圖層做動畫
```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
```
這麼做是可行的,但看起來更因為是運氣而不是設計的原因,如果我們把旋轉的值從`M_PI`(180度)調整到`2 * M_PI`(360度),然後運行程序,會發現這時候飛船完全不動了。這是因為這裡的矩陣做了一次360度的旋轉,和做了0度是一樣的,所以最後的值根本沒變。
現在繼續使用`M_PI`,但這次用`byValue`而不是`toValue`。也許你會認為這和設置`toValue`結果一樣,因為0 + 90度 == 90度,但實際上飛船的圖片變大了,並沒有做任何旋轉,這是因為變換矩陣不能像角度值那樣疊加。
那麼如果需要獨立於角度之外單獨對平移或者縮放做動畫呢?由於都需要我們來修改`transform`屬性,實時地重新計算每個時間點的每個變換效果,然後根據這些創建一個復雜的關鍵幀動畫,這一切都是為了對圖層的一個獨立做一個簡單的動畫。
幸運的是,有一個更好的解決方案:為了旋轉圖層,我們可以對`transform.rotation`關鍵路徑應用動畫,而不是`transform`本身(清單8.9)。
清單8.9 對虛擬的`transform.rotation`屬性做動畫
```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
```
結果運行的特別好,用`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`的實現細節是私有的,所以目前不能通過繼承它來自定義。你可以通過使用蘋果目前已近提供的常量(目前都是和變換矩陣的虛擬屬性相關,所以沒太多使用場景了,因為這些屬性都有了默認的實現方式)。
##動畫組
`CABasicAnimation`和`CAKeyframeAnimation`僅僅作用於單獨的屬性,而`CAAnimationGroup`可以把這些動畫組合在一起。`CAAnimationGroup`是另一個繼承於`CAAnimation`的子類,它添加了一個`animations`數組的屬性,用來組合別的動畫。我們把清單8.6那種關鍵幀動畫和調整圖層背景色的基礎動畫組合起來(清單8.10),結果如圖8.3所示。
清單8.10 組合關鍵幀動畫和基礎動畫
```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 關鍵幀路徑和基礎動畫的組合
##過渡
有時候對於iOS應用程序來說,希望能通過屬性動畫來對比較難做動畫的布局進行一些改變。比如交換一段文本和圖片,或者用一段網格視圖來替換,等等。屬性動畫只對圖層的可動畫屬性起作用,所以如果要改變一個不能動畫的屬性(比如圖片),或者從層級關系中添加或者移除圖層,屬性動畫將不起作用。
於是就有了過渡的概念。過渡並不像屬性動畫那樣平滑地在兩個值之間做動畫,而是影響到整個圖層的變化。過渡動畫首先展示之前的圖層外觀,然後通過一個交換過渡到新的外觀。
為了創建一個過渡動畫,我們將使用`CATransition`,同樣是另一個`CAAnimation`的子類,和別的子類不同,`CAAnimation`有一個`type`和`subtype`來標識變換效果。`type`屬性是一個`NSString`類型,可以被設置成如下類型:
kCATransitionFade
kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal
到目前為止你只能使用上述四種類型,但你可以通過一些別的方法來自定義過渡效果,後續會詳細介紹。
默認的過渡類型是`kCATransitionFade`,當你在改變圖層屬性之後,就創建了一個平滑的淡入淡出效果。
我們在第七章的例子中就已經用到過`kCATransitionPush`,它創建了一個新的圖層,從邊緣的一側滑動進來,把舊圖層從另一側推出去的效果。
`kCATransitionMoveIn`和`kCATransitionReveal`與`kCATransitionPush`類似,都實現了一個定向滑動的動畫,但是有一些細微的不同,`kCATransitionMoveIn`從頂部滑動進入,但不像推送動畫那樣把老土層推走,然而`kCATransitionReveal`把原始的圖層滑動出去來顯示新的外觀,而不是把新的圖層滑動進入。
後面三種過渡類型都有一個默認的動畫方向,它們都從左側滑入,但是你可以通過`subtype`來控制它們的方向,提供了如下四種類型:
kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom
一個簡單的用`CATransition`來對非動畫屬性做動畫的例子如清單8.11所示,這裡我們對`UIImage`的`image`屬性做修改,但是隱式動畫或者`CAPropertyAnimation`都不能對它做動畫,因為Core Animation不知道如何在插圖圖片。通過對圖層應用一個淡入淡出的過渡,我們可以忽略它的內容來做平滑動畫(圖8.4),我們來嘗試修改過渡的`type`常量來觀察其它效果。
清單8.11 使用`CATransition`來對`UIImageView`做動畫
```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
```
你可以從代碼中看出,過渡動畫和之前的屬性動畫或者動畫組添加到圖層上的方式一致,都是通過`-addAnimation:forKey:`方法。但是和屬性動畫不同的是,對指定的圖層一次只能使用一次`CATransition`,因此,無論你對動畫的鍵設置什麼值,過渡動畫都會對它的鍵設置成“transition”,也就是常量`kCATransition`。
圖8.4 使用`CATransition`對圖像平滑淡入淡出
###隱式過渡
`CATransision`可以對圖層任何變化平滑過渡的事實使得它成為那些不好做動畫的屬性圖層行為的理想候選。蘋果當然意識到了這點,並且當設置了`CALayer`的`content`屬性的時候,`CATransition`的確是默認的行為。但是對於視圖關聯的圖層,或者是其他隱式動畫的行為,這個特性依然是被禁用的,但是對於你自己創建的圖層,這意味著對圖層`contents`圖片做的改動都會自動附上淡入淡出的動畫。
我們在第七章使用`CATransition`作為一個圖層行為來改變圖層的背景色,當然`backgroundColor`屬性可以通過正常的`CAPropertyAnimation`來實現,但這不是說不可以用`CATransition`來實行。
###對圖層樹的動畫
`CATransition`並不作用於指定的圖層屬性,這就是說你可以在即使不能准確得知改變了什麼的情況下對圖層做動畫,例如,在不知道`UITableView`哪一行被添加或者刪除的情況下,直接就可以平滑地刷新它,或者在不知道`UIViewController`內部的視圖層級的情況下對兩個不同的實例做過渡動畫。
這些例子和我們之前所討論的情況完全不同,因為它們不僅涉及到圖層的屬性,而且是整個*圖層樹*的改變--我們在這種動畫的過程中手動在層級關系中添加或者移除圖層。
這裡用到了一個小詭計,要確保`CATransition`添加到的圖層在過渡動畫發生時不會在樹狀結構中被移除,否則`CATransition`將會和圖層一起被移除。一般來說,你只需要將動畫添加到被影響圖層的`superlayer`。
在清單8.2中,我們展示了如何在`UITabBarController`切換標簽的時候添加淡入淡出的動畫。這裡我們建立了默認的標簽應用程序模板,然後用`UITabBarControllerDelegate`的`-tabBarController:didSelectViewController:`方法來應用過渡動畫。我們把動畫添加到`UITabBarController`的視圖圖層上,於是在標簽被替換的時候動畫不會被移除。
清單8.12 對`UITabBarController`做動畫
```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
```
###自定義動畫
我們證實了過渡是一種對那些不太好做平滑動畫屬性的強大工具,但是`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提供的方法來做過渡動畫
```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`來創建其它的過渡效果。然是直到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:`創建自定義過渡效果
```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:`創建自定義過渡效果
這裡有個警告:`-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 開始和停止一個動畫
```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 通過開始和停止按鈕控制的旋轉動畫
##總結
這一章中,我們涉及了屬性動畫(你可以對單獨的圖層屬性動畫有更加具體的控制),動畫組(把多個屬性動畫組合成一個獨立單元)以及過度(影響整個圖層,可以用來對圖層的任何內容做任何類型的動畫,包括子圖層的添加和移除)。
在第九章中,我們繼續學習`CAMediaTiming`協議,來看一看Core Animation是怎樣處理逝去的時間。