鍍金池/ 教程/ iOS/ 顯式動畫
圖層的樹狀結(jié)構
視覺效果
圖像IO
寄宿圖
緩沖
隱式動畫
圖層幾何學
圖層時間
顯式動畫
變換
性能調(diào)優(yōu)
專用圖層
高效繪圖
圖層性能
基于定時器的動畫

顯式動畫

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

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

屬性動畫

首先我們來探討一下屬性動畫。屬性動畫作用于圖層的某個單一屬性,并指定了它的一個目標值,或者一連串將要做動畫的值。屬性動畫分為兩種:基礎關鍵幀

基礎動畫

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

CAPropertyAnimation通過指定動畫的keyPath作用于一個單一屬性,CAAnimation通常應用于一個指定的CALayer,于是這里指的也就是一個圖層的keyPath了。實際上它是一個關鍵路徑(一些用點表示法可以在層級關系中指向任意嵌套的對象),而不僅僅是一個屬性的名稱,因為這意味著動畫不僅可以作用于圖層本身的屬性,而且還包含了它的子成員的屬性,甚至是一些虛擬的屬性(后面會詳細解釋)。

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

id fromValue 
id toValue 
id byValue

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

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

id類型可以包含任意由NSObject派生的對象,但有時候你會希望對一些不直接從NSObject繼承的屬性類型做動畫,這意味著你需要把這些值用一個對象來封裝,或者強轉(zhuǎn)成一個對象,就像某些功能和Objective-C對象類似的Core Foundation類型。但是如何從一個具體的數(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,toValuebyValue屬性可以用很多種方式來組合,但為了防止沖突,不能一次性同時指定這三個值。例如,如果指定了fromValue等于2,toValue等于4,byValue等于3,那么Core Animation就不知道結(jié)果到底是4(toValue)還是5(fromValue + byValue)了。他們的用法在CABasicAnimation頭文件中已經(jīng)描述的很清楚了,所以在這里就不重復了??偟恼f來,就是只需要指定toValue或者byValue,剩下的值都可以通過上下文自動計算出來。

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

清單8.1 通過CABasicAnimation來設置圖層背景色

@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

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

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

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

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

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

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

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

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

更新之后的代碼如下:

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去自動設置這些。于是可以創(chuàng)建一個可復用的代碼。清單8.2修改了之前的示例,通過使用CABasicAnimation的一個函數(shù)來避免在每次動畫時候都重復那些臃腫的代碼。

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

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

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

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

CAAnimationDelegate

在第七章使用隱式動畫的時候,我們可以在CATransaction完成塊中檢測到動畫的完成。但是這種方式并不適用于顯式動畫,因為這里的動畫和事務并沒太多關聯(lián)。

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

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

當更新屬性的時候,我們需要設置一個新的事務,并且禁用圖層行為。否則動畫會發(fā)生兩次,一個是因為顯式的CABasicAnimation,另一次是因為隱式動畫,具體實現(xiàn)見訂單8.3。

清單8.3 動畫完成之后修改圖層的背景色

@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而言,使用委托模式而不是一個完成塊會帶來一個問題,就是當你有多個動畫的時候,無法在在回調(diào)方法中區(qū)分。在一個視圖控制器中創(chuàng)建動畫的時候,通常會用控制器本身作為一個委托(如清單8.3所示),但是所有的動畫都會調(diào)用同一個回調(diào)方法,所以你就需要判斷到底是那個圖層的調(diào)用。

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

我們不能通過隱式動畫來實現(xiàn)因為這些指針都是UIView的實例,所以圖層的隱式動畫都被禁用了。我們可以簡單地通過UIView的動畫方法來實現(xiàn)。但如果想更好地控制動畫時間,使用顯式動畫會更好(更多內(nèi)容見第十章)。使用CABasicAnimation來做動畫可能會更加復雜,因為我們需要在-animationDidStop:finished:中檢測指針狀態(tài)(用于設置結(jié)束的位置)。

動畫本身會作為一個參數(shù)傳入委托的方法,也許你會認為可以控制器中把動畫存儲為一個屬性,然后在回調(diào)用比較,但實際上并不起作用,因為委托傳入的動畫參數(shù)是原始值的一個深拷貝,從而不是同一個值。

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

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

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

清單8.4 使用KVC對動畫打標簽

@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設備上時,我們發(fā)現(xiàn)在-animationDidStop:finished:委托方法調(diào)用之前,指針會迅速返回到原始值,這個清單8.3圖層顏色發(fā)生的情況一樣。

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

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

關鍵幀動畫

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

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

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

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

清單8.5 使用CAKeyframeAnimation應用一系列顏色的變化

- (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é)束的顏色都是藍色,這是因為CAKeyframeAnimation并不能自動把當前值作為第一幀(就像CABasicAnimation那樣把fromValue設為nil)。動畫會在開始的時候突然跳轉(zhuǎn)到第一幀的值,然后在動畫結(jié)束的時候突然恢復到原始的值。所以為了動畫的平滑特性,我們需要開始和結(jié)束的關鍵幀來匹配當前屬性的值。

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

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

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

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

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

清單8.6 沿著一個貝塞爾曲線對圖層做動畫

@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
http://wiki.jikexueyuan.com/project/ios-core-animation/images/8.1.jpeg" alt="圖8.1" title="圖8.1" width="700"/>

圖8.1 沿著一個貝塞爾曲線移動的宇宙飛船圖片

運行示例,你會發(fā)現(xiàn)飛船的動畫有些不太真實,這是因為當它運動的時候永遠指向右邊,而不是指向曲線切線的方向。你可以調(diào)整它的affineTransform來對運動方向做動畫,但很可能和其它的動畫沖突。

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

清單8.7 通過rotationMode自動對齊圖層到曲線

- (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];
}
http://wiki.jikexueyuan.com/project/ios-core-animation/images/8.2.jpeg" alt="圖8.2" title="圖8.2" width="700"/>

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

虛擬屬性

之前提到過屬性動畫實際上是針對于關鍵路徑而不是一個鍵,這就意味著可以對子屬性甚至是虛擬屬性做動畫。但是虛擬屬性到底是什么呢?

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

清單8.8 用transform屬性對圖層做動畫

@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

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

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

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

幸運的是,有一個更好的解決方案:為了旋轉(zhuǎn)圖層,我們可以對transform.rotation關鍵路徑應用動畫,而不是transform本身(清單8.9)。

清單8.9 對虛擬的transform.rotation屬性做動畫

@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é)果運行的特別好,用transform.rotation而不是transform做動畫的好處如下:

  • 我們可以不通過關鍵幀一步旋轉(zhuǎn)多于180度的動畫。
  • 可以用相對值而不是絕對值旋轉(zhuǎn)(設置byValue而不是toValue)。
  • 可以不用創(chuàng)建CATransform3D,而是使用一個簡單的數(shù)值來指定角度。
  • 不會和transform.position或者transform.scale沖突(同樣是使用關鍵路徑來做獨立的動畫屬性)。

transform.rotation屬性有一個奇怪的問題是它其實并不存在。這是因為CATransform3D并不是一個對象,它實際上是一個結(jié)構體,也沒有符合KVC相關屬性,transform.rotation實際上是一個CALayer用于處理動畫變換的虛擬屬性。

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

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

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

動畫組

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

清單8.10 組合關鍵幀動畫和基礎動畫

- (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];
}
http://wiki.jikexueyuan.com/project/ios-core-animation/images/8.3.jpeg" alt="圖8.3" title="圖8.3" width="700"/>

圖8.3 關鍵幀路徑和基礎動畫的組合

過渡

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

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

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

kCATransitionFade 
kCATransitionMoveIn 
kCATransitionPush 
kCATransitionReveal

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

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

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

kCATransitionMoveInkCATransitionRevealkCATransitionPush類似,都實現(xiàn)了一個定向滑動的動畫,但是有一些細微的不同,kCATransitionMoveIn從頂部滑動進入,但不像推送動畫那樣把老土層推走,然而kCATransitionReveal把原始的圖層滑動出去來顯示新的外觀,而不是把新的圖層滑動進入。

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

kCATransitionFromRight 
kCATransitionFromLeft 
kCATransitionFromTop 
kCATransitionFromBottom

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

清單8.11 使用CATransition來對UIImageView做動畫

@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。

http://wiki.jikexueyuan.com/project/ios-core-animation/images/8.4.jpeg" alt="圖8.4" title="圖8.4" width="700" />

圖8.4 使用CATransition對圖像平滑淡入淡出

隱式過渡

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

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

對圖層樹的動畫

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

這些例子和我們之前所討論的情況完全不同,因為它們不僅涉及到圖層的屬性,而且是整個圖層樹的改變--我們在這種動畫的過程中手動在層級關系中添加或者移除圖層。

這里用到了一個小詭計,要確保CATransition添加到的圖層在過渡動畫發(fā)生時不會在樹狀結(jié)構中被移除,否則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的過渡特性。但是這里的可用的過渡選項和CATransitiontype屬性提供的常量完全不同UIView過渡方法中options參數(shù)可以由如下常量指定:

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框架)之后,可以通過CATransitionfilter屬性,用CIFilter來創(chuàng)建其它的過渡效果。然是直到iOS6都做不到這點。試圖對CATransition使用Core Image的濾鏡完全沒效果(但是在Mac OS中是可行的,也許文檔是想表達這個意思)。

因此,根據(jù)要實現(xiàn)的效果,你只用關心是用CATransition還是用UIView的過渡方法就可以了。希望下個版本的iOS系統(tǒng)可以通過CATransition很好的支持Core Image的過渡濾鏡效果(或許甚至會有新的方法)。

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

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

清單8.14演示了一個基本的實現(xiàn)。我們對當前視圖狀態(tài)截圖,然后在我們改變原始視圖的背景色的時候?qū)貓D快速轉(zhuǎn)動并且淡出,圖8.5展