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