隱式動畫
數組
按照個人意思去作,而不是我說的。 -- 埃德娜,辛普森安全
咱們在第一部分討論了Core Animation除了動畫以外能夠作到的任何事情。可是動畫師Core Animation庫一個很是顯著的特性。這一章咱們來看看它是怎麼作到的。具體來講,咱們先來討論框架自動完成的隱式動畫(除非你明確禁用了這個功能)。網絡
事務app
Core Animation基於一個假設,說屏幕上的任何東西均可以(或者可能)作動畫。動畫並不須要你在Core Animation中手動打開,相反須要明確地關閉,不然他會一直存在。框架
當你改變CALayer的一個可作動畫的屬性,它並不能馬上在屏幕上體現出來。相反,它是從先前的值平滑過渡到新的值。這一切都是默認的行爲,你不須要作額外的操做。dom
這看起來這太棒了,彷佛不太真實,咱們來用一個demo解釋一下:首先和第一章「圖層樹」同樣建立一個藍色的方塊,而後添加一個按鈕,隨機改變它的顏色。代碼見清單7.1。點擊按鈕,你會發現圖層的顏色平滑過渡到一個新值,而不是跳變(圖7.1)。ide
清單7.1 隨機改變圖層顏色函數
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView; @property (nonatomic, weak) 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 { //randomize the layer background color CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; ? } @end
圖7.1 添加一個按鈕來控制圖層顏色工具
這其實就是所謂的隱式動畫。之因此叫隱式是由於咱們並無指定任何動畫的類型。咱們僅僅改變了一個屬性,而後Core Animation來決定如何而且什麼時候去作動畫。Core Animaiton一樣支持顯式動畫,下章詳細說明。oop
但當你改變一個屬性,Core Animation是如何判斷動畫類型和持續時間的呢?實際上動畫執行的時間取決於當前事務的設置,動畫類型取決於圖層行爲。
事務其實是Core Animation用來包含一系列屬性動畫集合的機制,任何用指定事務去改變能夠作動畫的圖層屬性都不會馬上發生變化,而是當事務一旦提交的時候開始用一個動畫過渡到新值。
事務是經過CATransaction類來作管理,這個類的設計有些奇怪,不像你從它的命名預期的那樣去管理一個簡單的事務,而是管理了一疊你不能訪問的事務。CATransaction沒有屬性或者實例方法,而且也不能用+alloc和-init方法建立它。可是能夠用+begin和+commit分別來入棧或者出棧。
任何能夠作動畫的圖層屬性都會被添加到棧頂的事務,你能夠經過+setAnimationDuration:方法設置當前事務的動畫時間,或者經過+animationDuration方法來獲取值(默認0.25秒)。
Core Animation在每一個run loop週期中自動開始一次新的事務(run loop是iOS負責收集用戶輸入,處理定時器或者網絡事件而且從新繪製屏幕的東西),即便你不顯式的用[CATransaction begin]開始一次事務,任何在一次run loop循環中屬性的改變都會被集中起來,而後作一次0.25秒的動畫。
明白這些以後,咱們就能夠輕鬆修改變色動畫的時間了。咱們固然能夠用當前事務的+setAnimationDuration:方法來修改動畫時間,但在這裏咱們首先起一個新的事務,因而修改時間就不會有別的反作用。由於修改當前事務的時間可能會致使同一時刻別的動畫(如屏幕旋轉),因此最好仍是在調整動畫以前壓入一個新的事務。
修改後的代碼見清單7.2。運行程序,你會發現色塊顏色比以前變得更慢了。
清單7.2 使用CATransaction控制動畫時間
- (IBAction)changeColor { //begin a new transaction [CATransaction begin]; //set the animation duration to 1 second [CATransaction setAnimationDuration:1.0]; //randomize the layer background color CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; ?//commit the transaction [CATransaction commit]; }
若是你用過UIView的動畫方法作過一些動畫效果,那麼應該對這個模式不陌生。UIView有兩個方法,+beginAnimations:context:和+commitAnimations,和CATransaction的+begin和+commit方法相似。實際上在+beginAnimations:context:和+commitAnimations之間全部視圖或者圖層屬性的改變而作的動畫都是因爲設置了CATransaction的緣由。
在iOS4中,蘋果對UIView添加了一種基於block的動畫方法:+animateWithDuration:animations:。這樣寫對作一堆的屬性動畫在語法上會更加簡單,但實質上它們都是在作一樣的事情。
CATransaction的+begin和+commit方法在+animateWithDuration:animations:內部自動調用,這樣block中全部屬性的改變都會被事務所包含。這樣也能夠避免開發者因爲對+begin和+commit匹配的失誤形成的風險。
完成塊
基於UIView的block的動畫容許你在動畫結束的時候提供一個完成的動做。CATranscation接口提供的+setCompletionBlock:方法也有一樣的功能。咱們來調整上個例子,在顏色變化結束以後執行一些操做。咱們來添加一個完成以後的block,用來在每次顏色變化結束以後切換到另外一個旋轉90的動畫。代碼見清單7.3,運行結果見圖7.2。
清單7.3 在顏色動畫完成以後添加一個回調
- (IBAction)changeColor { //begin a new transaction [CATransaction begin]; //set the animation duration to 1 second [CATransaction setAnimationDuration:1.0]; //add the spin animation on completion [CATransaction setCompletionBlock:^{ //rotate the layer 90 degrees CGAffineTransform transform = self.colorLayer.affineTransform; transform = CGAffineTransformRotate(transform, M_PI_2); self.colorLayer.affineTransform = transform; }]; //randomize the layer background color CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; //commit the transaction [CATransaction commit]; }
圖7.2 顏色漸變之完成以後再作一次旋轉
注意旋轉動畫要比顏色漸變快得多,這是由於完成塊是在顏色漸變的事務提交併出棧以後才被執行,因而,用默認的事務作變換,默認的時間也就變成了0.25秒。
圖層行爲
如今來作個實驗,試着直接對UIView關聯的圖層作動畫而不是一個單獨的圖層。清單7.4是對清單7.2代碼的一點修改,移除了colorLayer,而且直接設置layerView關聯圖層的背景色。
清單7.4 直接設置圖層的屬性
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //set the color of our layerView backing layer directly self.layerView.layer.backgroundColor = [UIColor blueColor].CGColor; } - (IBAction)changeColor { //begin a new transaction [CATransaction begin]; //set the animation duration to 1 second [CATransaction setAnimationDuration:1.0]; //randomize the layer background color CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; self.layerView.layer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; //commit the transaction [CATransaction commit]; }
運行程序,你會發現當按下按鈕,圖層顏色瞬間切換到新的值,而不是以前平滑過渡的動畫。發生了什麼呢?隱式動畫好像被UIView關聯圖層給禁用了。
試想一下,若是UIView的屬性都有動畫特性的話,那麼不管在何時修改它,咱們都應該能注意到的。因此,若是說UIKit創建在Core Animation(默認對全部東西都作動畫)之上,那麼隱式動畫是如何被UIKit禁用掉呢?
咱們知道Core Animation一般對CALayer的全部屬性(可動畫的屬性)作動畫,可是UIView把它關聯的圖層的這個特性關閉了。爲了更好說明這一點,咱們須要知道隱式動畫是如何實現的。
咱們把改變屬性時CALayer自動應用的動畫稱做行爲,當CALayer的屬性被修改時候,它會調用-actionForKey:方法,傳遞屬性的名稱。剩下的操做都在CALayer的頭文件中有詳細的說明,實質上是以下幾步:
圖層首先檢測它是否有委託,而且是否實現CALayerDelegate協議指定的-actionForLayer:forKey方法。若是有,直接調用並返回結果。
若是沒有委託,或者委託沒有實現-actionForLayer:forKey方法,圖層接着檢查包含屬性名稱對應行爲映射的actions字典。
若是actions字典沒有包含對應的屬性,那麼圖層接着在它的style字典接着搜索屬性名。
最後,若是在style裏面也找不到對應的行爲,那麼圖層將會直接調用定義了每一個屬性的標準行爲的-defaultActionForKey:方法。
因此一輪完整的搜索結束以後,-actionForKey:要麼返回空(這種狀況下將不會有動畫發生),要麼是CAAction協議對應的對象,最後CALayer拿這個結果去對先前和當前的值作動畫。
因而這就解釋了UIKit是如何禁用隱式動畫的:每一個UIView對它關聯的圖層都扮演了一個委託,而且提供了-actionForLayer:forKey的實現方法。當不在一個動畫塊的實現中,UIView對全部圖層行爲返回nil,可是在動畫block範圍以內,它就返回了一個非空值。咱們能夠用一個demo作個簡單的實驗(清單7.5)
清單7.5 測試UIView的actionForLayer:forKey:實現
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //test layer action when outside of animation block NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]); //begin animation block [UIView beginAnimations:nil context:nil]; //test layer action when inside of animation block NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]); //end animation block [UIView commitAnimations]; } @end
運行程序,控制檯顯示結果以下:
$ LayerTest[21215:c07] Outside: $ LayerTest[21215:c07] Inside:
因而咱們能夠預言,當屬性在動畫塊以外發生改變,UIView直接經過返回nil來禁用隱式動畫。但若是在動畫塊範圍以內,根據動畫具體類型返回相應的屬性,在這個例子就是CABasicAnimation(第八章「顯式動畫」將會提到)。
固然返回nil並非禁用隱式動畫惟一的辦法,CATransacition有個方法叫作+setDisableActions:,能夠用來對全部屬性打開或者關閉隱式動畫。若是在清單7.2的[CATransaction begin]以後添加下面的代碼,一樣也會阻止動畫的發生:
[CATransaction setDisableActions:YES];
總結一下,咱們知道了以下幾點
UIView關聯的圖層禁用了隱式動畫,對這種圖層作動畫的惟一辦法就是使用UIView的動畫函數(而不是依賴CATransaction),或者繼承UIView,並覆蓋-actionForLayer:forKey:方法,或者直接建立一個顯式動畫(具體細節見第八章)。
對於單獨存在的圖層,咱們能夠經過實現圖層的-actionForLayer:forKey:委託方法,或者提供一個actions字典來控制隱式動畫。
咱們來對顏色漸變的例子使用一個不一樣的行爲,經過給colorLayer設置一個自定義的actions字典。咱們也可使用委託來實現,可是actions字典能夠寫更少的代碼。那麼到底改如何建立一個合適的行爲對象呢?
行爲一般是一個被Core Animation隱式調用的顯式動畫對象。這裏咱們使用的是一個實現了CATransaction的實例,叫作推動過渡。
第八章中將會詳細解釋過渡,不過對於如今,知道CATransition響應CAAction協議,而且能夠當作一個圖層行爲就足夠了。結果很贊,不論在何時改變背景顏色,新的色塊都是從左側滑入,而不是默認的漸變效果。
清單7.6 實現自定義行爲
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView; @property (nonatomic, weak) 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 a custom action CATransition *transition = [CATransition animation]; transition.type = kCATransitionPush; transition.subtype = kCATransitionFromLeft; self.colorLayer.actions = @{@"backgroundColor": transition}; //add it to our view [self.layerView.layer addSublayer:self.colorLayer]; } - (IBAction)changeColor { //randomize the layer background color CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; } @end
圖7.3 使用推動過渡的色值動畫
呈現與模型
CALayer的屬性行爲其實很不正常,由於改變一個圖層的屬性並無馬上生效,而是經過一段時間漸變動新。這是怎麼作到的呢?
當你改變一個圖層的屬性,屬性值的確是馬上更新的(若是你讀取它的數據,你會發現它的值在你設置它的那一刻就已經生效了),可是屏幕上並無立刻發生改變。這是由於你設置的屬性並無直接調整圖層的外觀,相反,他只是定義了圖層動畫結束以後將要變化的外觀。
當設置CALayer的屬性,其實是在定義當前事務結束以後圖層如何顯示的模型。Core Animation扮演了一個控制器的角色,而且負責根據圖層行爲和事務設置去不斷更新視圖的這些屬性在屏幕上的狀態。
咱們討論的就是一個典型的微型MVC模式。CALayer是一個鏈接用戶界面(就是MVC中的view)虛構的類,可是在界面自己這個場景下,CALayer的行爲更像是存儲了視圖如何顯示和動畫的數據模型。實際上,在蘋果本身的文檔中,圖層樹一般都是值的圖層樹模型。
在iOS中,屏幕每秒鐘重繪60次。若是動畫時長比60分之一秒要長,Core Animation就須要在設置一次新值和新值生效之間,對屏幕上的圖層進行從新組織。這意味着CALayer除了「真實」值(就是你設置的值)以外,必需要知道當前顯示在屏幕上的屬性值的記錄。
每一個圖層屬性的顯示值都被存儲在一個叫作呈現圖層的獨立圖層當中,他能夠經過-presentationLayer方法來訪問。這個呈現圖層其實是模型圖層的複製,可是它的屬性值表明了在任何指定時刻當前外觀效果。換句話說,你能夠經過呈現圖層的值來獲取當前屏幕上真正顯示出來的值(圖7.4)。
咱們在第一章中提到除了圖層樹,另外還有呈現樹。呈現樹經過圖層樹中全部圖層的呈現圖層所造成。注意呈現圖層僅僅當圖層首次被提交(就是首次第一次在屏幕上顯示)的時候建立,因此在那以前調用-presentationLayer將會返回nil。
你可能注意到有一個叫作–modelLayer的方法。在呈現圖層上調用–modelLayer將會返回它正在呈現所依賴的CALayer。一般在一個圖層上調用-modelLayer會返回–self(實際上咱們已經建立的原始圖層就是一種數據模型)。
圖7.4 一個移動的圖層是如何經過數據模型呈現的
大多數狀況下,你不須要直接訪問呈現圖層,你能夠經過和模型圖層的交互,來讓Core Animation更新顯示。兩種狀況下呈現圖層會變得頗有用,一個是同步動畫,一個是處理用戶交互。
若是你在實現一個基於定時器的動畫(見第11章「基於定時器的動畫」),而不只僅是基於事務的動畫,這個時候準確地知道在某一時刻圖層顯示在什麼位置就會對正確擺放圖層頗有用了。
若是你想讓你作動畫的圖層響應用戶輸入,你可使用-hitTest:方法(見第三章「圖層幾何學」)來判斷指定圖層是否被觸摸,這時候對呈現圖層而不是模型圖層調用-hitTest:會顯得更有意義,由於呈現圖層表明了用戶當前看到的圖層位置,而不是當前動畫結束以後的位置。
咱們能夠用一個簡單的案例來證實後者(見清單7.7)。在這個例子中,點擊屏幕上的任意位置將會讓圖層平移到那裏。點擊圖層自己能夠隨機改變它的顏色。咱們經過對呈現圖層調用-hitTest:來判斷是否被點擊。
若是修改代碼讓-hitTest:直接做用於colorLayer而不是呈現圖層,你會發現當圖層移動的時候它並不能正確顯示。這時候你就須要點擊圖層將要移動到的位置而不是圖層自己來響應點擊(這就是爲何用呈現圖層來響應交互的緣由)。
清單7.7 使用presentationLayer圖層來判斷當前圖層位置
@interface ViewController () @property (nonatomic, strong) CALayer *colorLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //create a red layer self.colorLayer = [CALayer layer]; self.colorLayer.frame = CGRectMake(0, 0, 100, 100); self.colorLayer.position = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2); self.colorLayer.backgroundColor = [UIColor redColor].CGColor; [self.view.layer addSublayer:self.colorLayer]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { //get the touch point CGPoint point = [[touches anyObject] locationInView:self.view]; //check if we've tapped the moving layer if ([self.colorLayer.presentationLayer hitTest:point]) { //randomize the layer background color CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; } else { //otherwise (slowly) move the layer to new position [CATransaction begin]; [CATransaction setAnimationDuration:4.0]; self.colorLayer.position = point; [CATransaction commit]; } } @end
總結
這一章討論了隱式動畫,還有Core Animation對指定屬性選擇合適的動畫行爲的機制。同時你知道了UIKit是如何充分利用Core Animation的隱式動畫機制來強化它的顯式系統,以及動畫是如何被默認禁用而且當須要的時候啓用的。最後,你瞭解了呈現和模型圖層,以及Core Animation是如何經過它們來判斷出圖層當前位置以及將要到達的位置。
在下一章中,咱們將研究Core Animation提供的顯式動畫類型,既能夠直接對圖層屬性作動畫,也能夠覆蓋默認的圖層行爲。
--------------------------------------------------------------------------------------------------------------------------------------------------------顯式動畫
若是想讓事情變得順利,只有靠本身 -- 夏爾·紀堯姆
上一章介紹了隱式動畫的概念。隱式動畫是在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的一些類型轉換
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來設置圖層背景色
@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來禁用隱式動畫行爲,不然默認的圖層行爲會干擾咱們的顯式動畫(實際上,顯式動畫一般會覆蓋隱式動畫,但在文章中並無提到,因此爲了安全最好這麼作)。
更新以後的代碼以下:
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 修改動畫馬上恢復到原始狀態的一個可複用函數
- (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來處理動畫,不過這已是朝更好的解決方案邁出一大步了。你能夠把它添加給CALaye做爲一個分類,以方便更好地使用。
解決看起來如此簡單的一個問題都着實麻煩,可是別的方案會更加複雜。若是不在動畫開始以前去更新目標屬性,那麼就只能在動畫徹底結束或者取消的時候更新它。這意味着咱們須要精準地在動畫結束以後,圖層返回到原始值以前更新屬性。那麼該如何找到這個點呢?
CAAnimationDelegate
在第七章使用隱式動畫的時候,咱們能夠在CATransaction完成塊中檢測到動畫的完成。可是這種方式並不適用於顯式動畫,由於這裏的動畫和事務並沒太多關聯。
那麼爲了知道一個顯式動畫在什麼時候結束,咱們須要使用一個實現了CAAnimationDelegate協議的delegate。
CAAnimationDelegate在任何頭文件中都找不到,可是能夠在CAAnimation頭文件或者蘋果開發者文檔中找到相關函數。在這個例子中,咱們用-animationDidStop:finished:方法在動畫結束以後來更新圖層的backgroundColor。
當更新屬性的時候,咱們須要設置一個新的事務,而且禁用圖層行爲。不然動畫會發生兩次,一個是由於顯式的CABasicAnimation,另外一次是由於隱式動畫,具體實現見訂單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而言,使用委託模式而不是一個完成塊會帶來一個問題,就是當你有多個動畫的時候,沒法在在回調方法中區分。在一個視圖控制器中建立動畫的時候,一般會用控制器自己做爲一個委託(如清單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對動畫打標籤
@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應用一系列顏色的變化
- (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 沿着一個貝塞爾曲線對圖層作動畫
@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自動對齊圖層到曲線
- (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屬性對圖層作動畫
@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屬性作動畫
@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 組合關鍵幀動畫和基礎動畫
- (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作動畫
@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作動畫
#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提供的方法來作過渡動畫
@interface ViewController () @property (nonatomic, weak) IBOutlet UIImageView *imageView; @property (nonatomic, copy) NSArray *images; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //set up images self.images = @[[UIImage imageNamed:@"Anchor.png"], [UIImage imageNamed:@"Cone.png"], [UIImage imageNamed:@"Igloo.png"], [UIImage imageNamed:@"Spaceship.png"]]; - (IBAction)switchImage { [UIView transitionWithView:self.imageView duration:1.0 options:UIViewAnimationOptionTransitionFlipFromLeft animations:^{ //cycle to next image UIImage *currentImage = self.imageView.image; NSUInteger index = [self.images indexOfObject:currentImage]; index = (index + 1) % [self.images count]; self.imageView.image = self.images[index]; } completion:NULL]; } @end
文檔暗示過在iOS5(帶來了Core Image框架)以後,能夠經過CATransition的filter屬性,用CIFilter來建立其它的過渡效果。然是直到iOS6都作不到這點。試圖對CATransition使用Core Image的濾鏡徹底沒效果(可是在Mac OS中是可行的,也許文檔是想表達這個意思)。
所以,根據要實現的效果,你只用關心是用CATransition仍是用UIView的過渡方法就能夠了。但願下個版本的iOS系統能夠經過CATransition很好的支持Core Image的過渡濾鏡效果(或許甚至會有新的方法)。
但這並不意味着在iOS上就不能實現自定義的過渡效果了。這只是意味着你須要作一些額外的工做。就像以前提到的那樣,過渡動畫作基礎的原則就是對原始的圖層外觀截圖,而後添加一段動畫,平滑過渡到圖層改變以後那個截圖的效果。若是咱們知道如何對圖層截圖,咱們就可使用屬性動畫來代替CATransition或者是UIKit的過渡方法來實現動畫。
事實證實,對圖層作截圖仍是很簡單的。CALayer有一個-renderInContext:方法,能夠經過把它繪製到Core Graphics的上下文中捕獲當前內容的圖片,而後在另外的視圖中顯示出來。若是咱們把這個截屏視圖置於原始視圖之上,就能夠遮住真實視圖的全部變化,因而從新建立了一個簡單的過渡效果。
清單8.14演示了一個基本的實現。咱們對當前視圖狀態截圖,而後在咱們改變原始視圖的背景色的時候對截圖快速轉動而且淡出,圖8.5展現了咱們自定義的過渡效果。
爲了讓事情更簡單,咱們用UIView -animateWithDuration:completion:方法來實現。雖然用CABasicAnimation能夠達到一樣的效果,可是那樣的話咱們就須要對圖層的變換和不透明屬性建立單獨的動畫,而後當動畫結束的是哦戶在CAAnimationDelegate中把coverView從屏幕中移除。
清單8.14 用renderInContext:建立自定義過渡效果
@implementation ViewController - (IBAction)performTransition { //preserve the current view snapshot UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0); [self.view.layer renderInContext:UIGraphicsGetCurrentContext()]; UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext(); //insert snapshot view in front of this one UIView *coverView = [[UIImageView alloc] initWithImage:coverImage]; coverView.frame = self.view.bounds; [self.view addSubview:coverView]; //update the view (we'll simply randomize the layer background color) CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; self.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0]; //perform animation (anything you like) [UIView animateWithDuration:1.0 animations:^{ //scale, rotate and fade the view CGAffineTransform transform = CGAffineTransformMakeScale(0.01, 0.01); transform = CGAffineTransformRotate(transform, M_PI_2); coverView.transform = transform; coverView.alpha = 0.0; } completion:^(BOOL finished) { //remove the cover view now we're finished with it [coverView removeFromSuperview]; }]; } @end
圖8.5 使用renderInContext:建立自定義過渡效果
這裏有個警告:-renderInContext:捕獲了圖層的圖片和子圖層,可是不能對子圖層正確地處理變換效果,並且對視頻和OpenGL內容也不起做用。可是用CATransition,或者用私有的截屏方式就沒有這個限制了。
在動畫過程當中取消動畫
以前提到過,你能夠用-addAnimation:forKey:方法中的key參數來在添加動畫以後檢索一個動畫,使用以下方法:
- (CAAnimation *)animationForKey:(NSString *)key;
但並不支持在動畫運行過程當中修改動畫,因此這個方法主要用來檢測動畫的屬性,或者判斷它是否被添加到當前圖層中。
爲了終止一個指定的動畫,你能夠用以下方法把它從圖層移除掉:
- (void)removeAnimationForKey:(NSString *)key;
或者移除全部動畫:
- (void)removeAllAnimations;
動畫一旦被移除,圖層的外觀就馬上更新到當前的模型圖層的值。通常說來,動畫在結束以後被自動移除,除非設置removedOnCompletion爲NO,若是你設置動畫在結束以後不被自動移除,那麼當它不須要的時候你要手動移除它;不然它會一直存在於內存中,直到圖層被銷燬。
咱們來擴展以前旋轉飛船的示例,這裏添加一個按鈕來中止或者啓動動畫。這一次咱們用一個非nil的值做爲動畫的鍵,以便以後能夠移除它。-animationDidStop:finished:方法中的flag參數代表了動畫是天然結束仍是被打斷,咱們能夠在控制檯打印出來。若是你用中止按鈕來終止動畫,它會打印NO,若是容許它完成,它會打印YES。
清單8.15是更新後的示例代碼,圖8.6顯示告終果。
清單8.15 開始和中止一個動畫
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, strong) CALayer *shipLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //add the ship self.shipLayer = [CALayer layer]; self.shipLayer.frame = CGRectMake(0, 0, 128, 128); self.shipLayer.position = CGPointMake(150, 150); self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage; [self.containerView.layer addSublayer:self.shipLayer]; } - (IBAction)start { //animate the ship rotation CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"transform.rotation"; animation.duration = 2.0; animation.byValue = @(M_PI * 2); animation.delegate = self; [self.shipLayer addAnimation:animation forKey:@"rotateAnimation"]; } - (IBAction)stop { [self.shipLayer removeAnimationForKey:@"rotateAnimation"]; } - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { //log that the animation stopped NSLog(@"The animation stopped (finished: %@)", flag? @"YES": @"NO"); } @end
圖8.6 經過開始和中止按鈕控制的旋轉動畫
總結
這一章中,咱們涉及了屬性動畫(你能夠對單獨的圖層屬性動畫有更加具體的控制),動畫組(把多個屬性動畫組合成一個獨立單元)以及過分(影響整個圖層,能夠用來對圖層的任何內容作任何類型的動畫,包括子圖層的添加和移除)。
在第九章中,咱們繼續學習CAMediaTiming協議,來看一看Core Animation是怎樣處理逝去的時間。