我們寫的應用程序往往都不是靜態(tài)的,因為它們需要適應用戶的需求以及為執(zhí)行各種任務而改變狀態(tài)。
在這些狀態(tài)之間轉(zhuǎn)換時,清晰的揭示正在發(fā)生什么是非常重要的。而不是在頁面之間跳躍,動畫幫助我們解釋用戶從哪里來,要到哪里去。
鍵盤在 view 中滑進滑出給了我們一個錯覺,讓我們以為它是簡單的被隱藏在屏幕下方的,并且是手機很自然的一個部分。View controller 轉(zhuǎn)場加強了我們的應用程序的導航結構,并且給了用戶正在移向哪個方向的提示。微妙的反彈和碰撞使界面栩栩如生,并且激發(fā)出了物理的質(zhì)感。要是沒有這些的話,我們就只有一個沒有視覺修飾的干巴巴環(huán)境了。
動畫是敘述你的應用的故事的絕佳方式,在了解動畫背后的基本原理之后,設計它們會輕松很多。
在這篇文章 (以及這個話題中其余大多數(shù)文章) 中,我們將特別地針對 Core Animation 進行探討。雖然你將看到的很多東西也可以用更高層級的 UIKit 的方法來完成,但是 Core Animation 將會讓你更好的理解正在發(fā)生什么。它以一種更明確的方式來描述動畫,這對這篇文章讀者以及你自己的代碼的讀者來說都非常有用。
在看動畫如何與我們在屏幕上看到的內(nèi)容交互之前,我們需要快速瀏覽一下 Core Animation 的 CALayer,這是動畫產(chǎn)生作用的地方。
你大概知道 UIView 實例,以及 layer-backed 的 NSView,修改它們的 layer 來委托強大的 Core Graphics 框架來進行渲染。然而,你務必要理解,當把動畫添加到一個 layer 時,是不直接修改它的屬性的。
取而代之,Core Animation 維護了兩個平行 layer 層次結構: model layer tree(模型層樹) 和 presentation layer tree(表示層樹)。前者中的 layers 反映了我們能直接看到的 layers 的狀態(tài),而后者的 layers 則是動畫正在表現(xiàn)的值的近似。
實際上還有所謂的第三個 layer 樹,叫做 rendering tree(渲染樹)。因為它對 Core Animation 而言是私有的,所以我們在這里不討論它。
考慮在 view 上增加一個漸出動畫。如果在動畫中的任意時刻,查看 layer 的 opacity 值,你是得不到與屏幕內(nèi)容對應的透明度的。取而代之,你需要查看 presentation layer 以獲得正確的結果。
雖然你可能不會去直接設置 presentation layer 的屬性,但是使用它的當前值來創(chuàng)建新的動畫或者在動畫發(fā)生時與 layers 交互是非常有用的。
通過使用 -[CALayer presentationLayer] 和 -[CALayer modelLayer],你可以在兩個 layer 之間輕松切換。
可能最常見的情況是將一個 view 的屬性從一個值改變?yōu)榱硪粋€值,考慮下面這個例子:
http://wiki.jikexueyuan.com/project/objc/images/12-1.gif" alt="" />
在這里,我們讓紅色小火箭的 x-position 從 77.0 變?yōu)?455.0,剛好超過它的 parent view 的邊。為了填充所有路徑,我們需要確定我們的火箭在任意時刻所到達的位置。這通常使用線性插值法來完成:
http://wiki.jikexueyuan.com/project/objc/images/12-15.png" alt="" />
也就是說,對于動畫給定的一個分數(shù) t,火箭的 x 坐標就是起始點的 x 坐標 77,加上一個到終點的距離 ?x = 378 乘以該分數(shù)的值。
使用 CABasicAnimation,我們可以如下實現(xiàn)這個動畫:
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @77;
animation.toValue = @455;
animation.duration = 1;
[rocket.layer addAnimation:animation forKey:@"basic"];
請注意我們要動畫的鍵路徑,也就是 position.x,實際上包含一個存儲在 position 屬性中的 CGPoint 結構體成員。這是 Core Animation 一個非常方便的特性。請務必查看支持的鍵路徑的完整列表。
然而,當我們運行該代碼時,我們意識到火箭在完成動畫后馬上回到了初始位置。這是因為在默認情況下,動畫不會在超出其持續(xù)時間后還修改 presentation layer。實際上,在結束時它甚至會被徹底移除。
一旦動畫被移除,presentation layer 將回到 model layer 的值,并且因為我們從未修改該 layer 的 position 屬性,所以我們的飛船將重新出現(xiàn)在它開始的地方。
這里有兩種解決這個問題的方法:
第一種方法是直接在 model layer 上更新屬性。這是推薦的的做法,因為它使得動畫完全可選。
一旦動畫完成并且從 layer 中移除,presentation layer 將回到 model layer 設置的值,而這個值恰好與動畫最后一個步驟相匹配。
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @77;
animation.toValue = @455;
animation.duration = 1;
[rocket.layer addAnimation:animation forKey:@"basic"];
rocket.layer.position = CGPointMake(455, 61);
或者,你可以通過設置動畫的 fillMode 屬性為 kCAFillModeForward 以留在最終狀態(tài),并設置removedOnCompletion 為 NO 以防止它被自動移除:
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @77;
animation.toValue = @455;
animation.duration = 1;
animation.fillMode = kCAFillModeForward;
animation.removedOnCompletion = NO;
[rectangle.layer addAnimation:animation forKey:@"basic"];
Andy Matuschak 指出了,如果將已完成的動畫保持在 layer 上時,會造成額外的開銷,因為渲染器會去進行額外的繪畫工作。
值得指出的是,實際上我們創(chuàng)建的動畫對象在被添加到 layer 時立刻就復制了一份。這個特性在多個 view 中重用動畫時這非常有用。比方說我們想要第二個火箭在第一個火箭起飛不久后起飛:
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.byValue = @378;
animation.duration = 1;
[rocket1.layer addAnimation:animation forKey:@"basic"];
rocket1.layer.position = CGPointMake(455, 61);
animation.beginTime = CACurrentMediaTime() + 0.5;
[rocket2.layer addAnimation:animation forKey:@"basic"];
rocket2.layer.position = CGPointMake(455, 111);
設置動畫的 beginTime 為未來 0.5 秒將只會影響 rocket2,因為動畫在執(zhí)行語句 [rocket1.layer addAnimation:animation forKey:@"basic"]; 時已經(jīng)被復制了,并且之后 rocket1 也不會考慮對動畫對象的改變。
不妨看一看 David 的 關于動畫時間的一篇很棒的文章,通過它可以學習如何更精確的控制你的動畫。
我決定再使用 CABasicAnimation 的 byValue 屬性創(chuàng)建一個動畫,這個動畫從 presentation layer 的當前值開始,加上 byValue 的值后結束。這使得動畫更易于重用,因為你不需要精確的指定可能無法提前知道的 from- 和 toValue 的值。
fromValue, byValue 和 toValue 的不同組合可以用來實現(xiàn)不同的效果,如果你需要創(chuàng)建一個可以在你的不同應用中重用的動畫,你可以查看文檔。
這很容易想到一個場景,你想要為你的動畫定義超過兩個步驟,我們可以使用更通用的 CAKeyframeAnimation,而不是去鏈接多個 CABasicAnimation 實例。
關鍵幀(keyframe)使我們能夠定義動畫中任意的一個點,然后讓 Core Animation 填充所謂的中間幀。
比方說我們正在制作我們下一個 iPhone 應用程序上的登陸表單,我們希望當用戶輸入錯誤的密碼時表單會晃動。使用關鍵幀動畫,看起來大概像下面這樣:
http://wiki.jikexueyuan.com/project/objc/images/12-16.gif" alt="" />
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position.x";
animation.values = @[ @0, @10, @-10, @10, @0 ];
animation.keyTimes = @[ @0, @(1 / 6.0), @(3 / 6.0), @(5 / 6.0), @1 ];
animation.duration = 0.4;
animation.additive = YES;
[form.layer addAnimation:animation forKey:@"shake"];
values 數(shù)組定義了表單應該到哪些位置。
設置 keyTimes 屬性讓我們能夠指定關鍵幀動畫發(fā)生的時間。它們被指定為關鍵幀動畫總持續(xù)時間的一個分數(shù)。
請注意我是如何選擇不同的值從 0 到 10 和從 10 到 -10 轉(zhuǎn)換以維持恒定的速度的。
設置 additive 屬性為 YES 使 Core Animation 在更新 presentation layer 之前將動畫的值添加到 model layer 中去。這使我們能夠?qū)λ行问降男枰碌脑刂赜孟嗤膭赢嫞覠o需提前知道它們的位置。因為這個屬性從 CAPropertyAnimation 繼承,所以你也可以在使用 CABasicAnimation 時使用它。
雖然用代碼實現(xiàn)一個簡單的水平晃動并不難,但是沿著復雜路徑的動畫就需要我們在關鍵幀的 values 數(shù)組中存儲大量 box 化的 CGPoint。 值得慶幸的是,CAKeyframeAnimation 提供了更加便利的 path 屬性作為代替。
舉個例子,我們?nèi)绾巫屢粋€ view 做圓周運動:
http://wiki.jikexueyuan.com/project/objc/images/12-2.gif" alt="" />
CGRect boundingRect = CGRectMake(-150, -150, 300, 300);
CAKeyframeAnimation *orbit = [CAKeyframeAnimation animation];
orbit.keyPath = @"position";
orbit.path = CFAutorelease(CGPathCreateWithEllipseInRect(boundingRect, NULL));
orbit.duration = 4;
orbit.additive = YES;
orbit.repeatCount = HUGE_VALF;
orbit.calculationMode = kCAAnimationPaced;
orbit.rotationMode = kCAAnimationRotateAuto;
[satellite.layer addAnimation:orbit forKey:@"orbit"];
使用 CGPathCreateWithEllipseInRect(),我們創(chuàng)建一個圓形的 CGPath 作為我們的關鍵幀動畫的 path。
使用 calculationMode 是控制關鍵幀動畫時間的另一種方法。我們通過將其設置為 kCAAnimationPaced,讓 Core Animation 向被驅(qū)動的對象施加一個恒定速度,不管路徑的各個線段有多長。將其設置為 kCAAnimationPaced 將無視所有我們已經(jīng)設置的 keyTimes。
設置 rotationMode 屬性為 kCAAnimationRotateAuto 確保飛船沿著路徑旋轉(zhuǎn)。作為對比,如果我們將該屬性設置為 nil 那動畫會是什么樣的呢。
http://wiki.jikexueyuan.com/project/objc/images/12-3.gif" alt="" />
你可以使用帶路徑的動畫來實現(xiàn)幾個有趣的效果;資深 objc.io 作者 Ole Begemann 寫了一篇文章,闡述了如何將 CAShapeLayer 與基于路徑的動畫組合起來使用,并只用幾行代碼來創(chuàng)建酷炫的繪圖動畫。
讓我們再次來看看第一個例子:
http://wiki.jikexueyuan.com/project/objc/images/12-4.gif" alt="" />
你會發(fā)現(xiàn)我們的火箭的動畫有一些看起來非常不自然的地方。那是因為我們在現(xiàn)實世界中看到的大部分運動需要時間來加速或減速。對象瞬間達到最高速度,然后再立即停止往往看起來非常不自然。除非你在讓機器人跳舞,但這很少是想要的效果。
為了給我們的動畫一個存在慣性的感覺,我們可以使用我們上面提到的參數(shù)因子來進行插值。然而,如果我們接下來需要為每個需要加速或減速的行為創(chuàng)建一個新的插值函數(shù),這將是一個很難擴展的方法。
取而代之,常見的做法是把要進行動畫的屬性的插值從動畫的速度中解耦出來。這樣一來,給動畫提速會產(chǎn)生一種小火箭加速運動的效果,而不用改變我們的插值函數(shù)。
我們可以通過引入一個 時間函數(shù) (timing function) (有時也被稱為 easing 函數(shù))來實現(xiàn)這個目標。該函數(shù)通過修改持續(xù)時間的分數(shù)來控制動畫的速度。
http://wiki.jikexueyuan.com/project/objc/images/12-17.png" alt="" />
最簡單的 easing 函數(shù)是 linear。它在整個動畫上維持一個恒定的速度。在 Core Animation 中,這個功能由 CAMediaTimingFunction 類表示。
http://wiki.jikexueyuan.com/project/objc/images/12-5.gif" alt="" />
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @50;
animation.toValue = @150;
animation.duration = 1;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[rectangle.layer addAnimation:animation forKey:@"basic"];
rectangle.layer.position = CGPointMake(150, 0);
Core Animation 附帶了一些 linear 之外的內(nèi)置 easing 函數(shù),如:
kCAMediaTimingFunctionEaseIn):kCAMediaTimingFunctionEaseOut):kCAMediaTimingFunctionEaseInEaseOut):kCAMediaTimingFunctionDefault):在一定限度內(nèi),你也可以使用 +functionWithControlPoints:::: 創(chuàng)建自己的 easing 函數(shù)。通過傳遞 cubic Bézier 曲線的兩個控制點的 x 和 y 坐標,你可以輕松的創(chuàng)建自定義 easing 函數(shù),比如我為我們的紅色小火箭選擇的那個。
這個方法因為有三個無名參數(shù)而聲名狼藉,我們并不推薦在你的 API 中使用這種蛋疼的寫法。
http://wiki.jikexueyuan.com/project/objc/images/12-10.gif" alt="" />
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @77;
animation.toValue = @455;
animation.duration = 1;
animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.5:0:0.9:0.7];
[rocket.layer addAnimation:animation forKey:@"basic"];
rocket.layer.position = CGPointMake(150, 0);
我不打算講太多關于 Bézier 曲線的細節(jié),在計算機圖形學中,它們是創(chuàng)建平滑曲線的常用技術。你可能在基于矢量的繪圖工具,比如 Sketch 或 Adobe Illustrator 中見過它們。
http://wiki.jikexueyuan.com/project/objc/images/12-11.png" alt="" />
傳遞給 +functionWithControlPoints:::: 的值有效地控制了控制點的位置。所得到的定時函數(shù)將基于得到的路徑來調(diào)整動畫的速度。x 軸代表時間的分數(shù),而 y 軸是插值函數(shù)的輸入值。
遺憾的是,由于這些部分被鎖定在 [0–1] 的范圍內(nèi),我們不可能用它來創(chuàng)建一些像預期動作 (Anticipation,一種像目標進發(fā)前先回退一點,到達目標后還過沖一會兒,見下圖) 這樣的常見效果。
我寫了一個小型庫,叫做 RBBAnimation,它包含一個允許使用 更多復雜 easing 函數(shù) 的自定義子類 CAKeyframeAnimation,包括反彈和包含負分量的 cubic Bézier 函數(shù):
http://wiki.jikexueyuan.com/project/objc/images/12-12.gif" alt="" />
RBBTweenAnimation *animation = [RBBTweenAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @50;
animation.toValue = @150;
animation.duration = 1;
animation.easing = RBBCubicBezier(0.68, -0.55, 0.735, 1.55);
http://wiki.jikexueyuan.com/project/objc/images/12-13.gif" alt="" />
RBBTweenAnimation *animation = [RBBTweenAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @50;
animation.toValue = @150;
animation.duration = 1;
animation.easing = RBBEasingFunctionEaseOutBounce;
對于某些復雜的效果,可能需要同時為多個屬性進行動畫。想象一下,在一個媒體播放程序中,當切換到到隨機曲目時我們讓隨機動畫生效??雌饋砭拖裣旅孢@樣:
http://wiki.jikexueyuan.com/project/objc/images/12-14.gif" alt="" />
你可以看到,我們需要同時對上面的封面的 position,rotation 和 z-position 進行動畫。使用 CAAnimationGroup 來動畫其中一個封面的代碼大概如下:
CABasicAnimation *zPosition = [CABasicAnimation animation];
zPosition.keyPath = @"zPosition";
zPosition.fromValue = @-1;
zPosition.toValue = @1;
zPosition.duration = 1.2;
CAKeyframeAnimation *rotation = [CAKeyframeAnimation animation];
rotation.keyPath = @"transform.rotation";
rotation.values = @[ @0, @0.14, @0 ];
rotation.duration = 1.2;
rotation.timingFunctions = @[
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]
];
CAKeyframeAnimation *position = [CAKeyframeAnimation animation];
position.keyPath = @"position";
position.values = @[
[NSValue valueWithCGPoint:CGPointZero],
[NSValue valueWithCGPoint:CGPointMake(110, -20)],
[NSValue valueWithCGPoint:CGPointZero]
];
position.timingFunctions = @[
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]
];
position.additive = YES;
position.duration = 1.2;
CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
group.animations = @[ zPosition, rotation, position ];
group.duration = 1.2;
group.beginTime = 0.5;
[card.layer addAnimation:group forKey:@"shuffle"];
card.layer.zPosition = 1;
我們使用 CAAnimationGroup 得到的一個好處是可以將所有動畫作為一個對象暴露出去。如果你要在應用程序中的多個地方用工廠對象創(chuàng)建的重用的動畫的話,這將會非常有用。
你也可以使用動畫組同時控制所有動畫組成部分的時間。
都現(xiàn)在了,你應該已經(jīng)聽說過 UIKit Dynamics 了,這是 iOS 7 中引入的一個物理模擬框架,它允許你使用約束和力來為 views 做動畫。與 Core Animation 不同,它與你在屏幕上看到的內(nèi)容交互更為間接,但是它的動態(tài)特性讓你可以在事先不知道結果時創(chuàng)建動畫。
Facebook 最近開源了 Paper 背后的動畫引擎 Pop。從概念上講,它介于 Core Animation 和 UIKit Dynamics 之間。它完美的使用了彈簧(spring)動畫,并且能夠在動畫運行時操控目標值,而無需替換它。Pop 也可以在 OS X 上使用,并且允許我們在每個 NSObject 的子類中為任意屬性進行動畫。