啟動畫面(Splash Screen)——不但給開發者們提供了一個盡情發揮、創建有趣動畫的機會,也填補了App啟動時從終端慢吞吞地下載數據的時間。啟動畫面(動態的)對于App至關重要:它可以讓用戶不失興趣地耐心等待應用完成加載。
盡管現在的啟動畫面多種多樣,但很少有像Uber這般精美的。2016年第一季度,Uber的CEO發表了關于重塑品牌的策略,其中之一就是現在這個超酷的啟動畫面。
這篇教程的目的是盡可能真實地再現Uber的動畫。我們會大量地使用到CALayers、CAAnimations,以及它們的子類。我不會從頭介紹這些類的基本概念,而是把重點放在如何應用這些類,創建高質量的動畫。如果你想要了解動畫背后的基本原理,可以參考Marin Todorove的iOS動畫中級教程。
入門
由于有非常多的動畫要實現,我們不妨在這個初始項目的基礎上進行修改。初始項目里已經為你創建好了所有需要的CALayer,我們給它們添加動畫即可。譯者注:為了保持教程簡潔,刪除了原文里一些與教學無關的文字。如有興趣可通過文章最后的鏈接閱讀原文相關內容。
先來看一眼最終效果:
打開初始項目看看里面的文件。
從控制器的角度分析,項目中的SplashViewController通過它的父視圖控制器RootContainerViewController生成。SplashViewController會不停循環播放啟動動畫,直到App完全加載完成,即與終端API握手成功并獲取了必要的數據。值得一提的是,在這個示例項目里,啟動動畫抽象成了一個單獨的模塊(譯者注:可以直接集成到其他項目里)。
RootContainerViewController里有兩個方法:showSplashViewController()和showSplashViewControllerNoPing()。我們主要使用第二個方法,它只會不停循環播放動畫(不會進入主界面),便于我們把精力集中在SplashViewController的子視圖上。當然,最后我們還是會切換回第一個方法,模擬API延遲并過渡到主界面。
啟動畫面的視圖和層
SplashViewController包含了兩個子視圖。其一是“波紋格子”背景,我們把它叫做TileGridView, ? ?由一系列TileView組成。另一個是帶有動畫效果的“U”字Logo,我們把它叫做AnimatedULogoView。
AnimatedULogoView里有4個CAShapeLayer:circleLayer:“U”型Logo的圓形白色背景
lineLayer:circleLayer中心到邊界的一條直線
squareLayer:circleLayer中心位置的正方形
maskLayer:遮罩層,當它的邊界隨著動畫改變的時會遮蓋其他層
這些CAShapeLayer組合在一起,構成了Fuber標志性的“U”。
既然已經知道這些層的組合方式,那我們可以開始創建動畫,讓AnimatedULogoView動起來了。
圓形的動畫
制作動畫的時候,最好過濾掉其他視覺“噪音”,只關注當前實現的動畫。打開AnimatedULogoView.swift,在init(frame:)方法里,把除了cricleLayer的其他層全都注釋掉,實現完動畫后我們會重新添加回來。注釋后的代碼應該像下面一樣:override?init(frame:?CGRect)?{??super.init(frame:?frame)
circleLayer?=?generateCircleLayer()
lineLayer?=?generateLineLayer()
squareLayer?=?generateSquareLayer()
maskLayer?=?generateMaskLayer()??//??layer.mask?=?maskLayer
layer.addSublayer(circleLayer)??//??layer.addSublayer(lineLayer)
//??layer.addSublayer(squareLayer)}
定位到generateCricleLayer()方法,試著理解一下這里的圓是怎么繪制的。其實它只不過是用UIBezierPath繪制的一個CAShapeLayer。注意這一行:layer.path?=?UIBezierPath(arcCenter:?CGPointZero,?radius:?radius/2,?startAngle:?-CGFloat(M_PI_2),?endAngle:?CGFloat(3*M_PI_2),?clockwise:?true).CGPath
默認情況下,如果你把startAngle設置為0,圓弧會從右側開始繪制(3點鐘方向)。如果設置為-M_PI_2,也就是-90°的話則會從上方開始繪制,endAngle最終是270°,或者說3*M_PI_2,同樣也是圓的正上方。另外要注意的是,在這里我們把弧線的寬度lineWidth設置為圓的半徑radius,因為我們想讓它動起來(畫圓的過程)。
circleLayer的動畫是由三個CAAnimation組成的:一個描繪筆端動畫的CAKeyframeAnimation,一個進行圖形變換的CABasicAnimation,以及一個CAAnimationGroup把它們合成在一起。
定位到animateCricleLayer(),添加下面代碼://?筆畫變化的動畫let?strokeEndAnimation?=?CAKeyframeAnimation(keyPath:?"strokeEnd")
strokeEndAnimation.timingFunction?=?strokeEndTimingFunction
strokeEndAnimation.duration?=?kAnimationDuration?-?kAnimationDurationDelay
strokeEndAnimation.values?=?[0.0,?1.0]
strokeEndAnimation.keyTimes?=?[0.0,?1.0]
通過把動畫的values設置為0.0和1.0,我們告訴Core Animation,從startAngle開始,到endAngle結束,創建像時鐘一樣的動畫。隨社storkeEnd的值變大,沿著周長的弧線長度也逐漸增加,整個圓逐漸被填滿。對于這個例子,如果你把values改為[0.0, 0.5],那么整個動畫只會填滿半個圓。
接著添加形變動畫:let?transformAnimation?=?CABasicAnimation(keyPath:?"transform")
transformAnimation.timingFunction?=?strokeEndTimingFunction
transformAnimation.duration?=?kAnimationDuration?-?kAnimationDurationDelay//?旋轉放大的動畫//?起始時:逆時針旋轉45°,x、y為正常大小的0.25倍var?startingTransform?=?CATransform3DMakeRotation(-CGFloat(M_PI_4),?0,?0,?1)
startingTransform?=?CATransform3DScale(startingTransform,?0.25,?0.25,?1)
transformAnimation.fromValue?=?NSValue(CATransform3D:?startingTransform)
transformAnimation.toValue?=?NSValue(CATransform3D:?CATransform3DIdentity)
這個動畫既包括了圖形的縮放也包括了沿z軸的旋轉。其結果是circleLayer在順時針旋轉45°的同時逐漸放大。這里旋轉的參數設置非常重要,因為和其它層的動畫組合的時候,它需要和lineLayer的位置及速度相匹配。
最后,在方法的末尾添加一個CAAnimationGroup,它負責把前面兩個動畫合成在一起,這樣你只需給cricleLayer添加一個動畫即可。//?把兩個動畫合成let?groupAnimation?=?CAAnimationGroup()
groupAnimation.animations?=?[strokeEndAnimation,?transformAnimation]
groupAnimation.repeatCount?=?Float.infinity?//?無限重復動畫groupAnimation.duration?=?kAnimationDuration
groupAnimation.beginTime?=?beginTime
groupAnimation.timeOffset?=?startTimeOffset
circleLayer.addAnimation(groupAnimation,?forKey:?"looping")
CAAnimationGroup設定了兩個重要的屬性:beginTime和timeOffset,如果你對它們不熟悉的話可以參考這篇文章,里面有這兩個屬性的描述以及用法。
這里的groupAnimation的beginTime屬性是根據父視圖的時間設定的。
timeOffset在這里也需要設定,因為這個動畫在第一次運行的時候,實際上是從中途開始的。當我們完成更多動畫時,你可以回到這里,嘗試改變startTimeOffset的值并觀察效果的差別。
把groupAnimation添加給circleLayer,編譯運行一下看看目前的效果:
提示:試著刪除strokeEndAnimation或者transformAnimation,看看單獨的每一個動畫是什么樣的。在這篇教程里,你可以嘗試不同動畫的效果。你可能會驚奇地發現,不同是動畫組合竟能創建出如此意想不到的獨特視覺效果。
直線的動畫
現在我們已經完成circleLayer的動畫了,該開始說說lineLayer了。還是在AnimatedULogoView.swift里,定位到startAnimating()把除了animateLineLayer()以外的方法全部注釋掉。注釋完的代碼應該如下面所示:public?func?startAnimating()?{
beginTime?=?CACurrentMediaTime()
layer.anchorPoint?=?CGPointZero
//??animateMaskLayer()
//??animateCircleLayer()
animateLineLayer()??//??animateSquareLayer()}
此外還需要調整一下init(frame:),只顯示circleLayer和lineLayer:override?init(frame:?CGRect)?{??super.init(frame:?frame)
circleLayer?=?generateCircleLayer()
lineLayer?=?generateLineLayer()
squareLayer?=?generateSquareLayer()
maskLayer?=?generateMaskLayer()??//??layer.mask?=?maskLayer
layer.addSublayer(circleLayer)
layer.addSublayer(lineLayer)??//??layer.addSublayer(squareLayer)}
注釋完畢,定位到animateLineLayer()方法,實現下一組動畫效果://?線段寬度動畫let?lineWidthAnimation?=?CAKeyframeAnimation(keyPath:?"lineWidth")
lineWidthAnimation.values?=?[0.0,?5.0,?0.0]
lineWidthAnimation.timingFunctions?=?[strokeEndTimingFunction,?circleLayerTimingFunction]
lineWidthAnimation.duration?=?kAnimationDuration
lineWidthAnimation.keyTimes?=?[0.0,?1.0?-?kAnimationDurationDelay/kAnimationDuration,?1.0]
這個動畫會先增加lineLayer的寬度,隨后變回來。
添加下面代碼實現下一個動畫://?變形let?transformAnimation?=?CAKeyframeAnimation(keyPath:?"transform")
transformAnimation.timingFunctions?=?[strokeEndTimingFunction,?circleLayerTimingFunction]
transformAnimation.duration?=?kAnimationDuration
transformAnimation.keyTimes?=?[0.0,?1.0?-?kAnimationDurationDelay/kAnimationDuration,?1.0]//?和之前一樣的旋轉放大動畫var?transform?=?CATransform3DMakeRotation(-CGFloat(M_PI_4),?0.0,?0.0,?1.0)
transform?=?CATransform3DScale(transform,?0.25,?0.25,?1.0)//?先放大再縮小transformAnimation.values?=?[NSValue(CATransform3D:?transform),?????????????????????????????NSValue(CATransform3D:?CATransform3DIdentity),?????????????????????????????NSValue(CATransform3D:?CATransform3DMakeScale(0.15,?0.15,?1.0))]
和circleLayer的動畫非常相似,我們在這里也定義了一個沿z軸順時針旋轉的動畫。在這里,我們同樣對線條定義了一個縮放動畫:從原始大小的25%開始,先變為原始大小,緊接著變為原始大小的15%。
用CAAnimationGroup把它們合成到一起,添加到lineLayer里://?合成動畫let?groupAnimation?=?CAAnimationGroup()
groupAnimation.repeatCount?=?Float.infinity
groupAnimation.removedOnCompletion?=?falsegroupAnimation.duration?=?kAnimationDuration
groupAnimation.beginTime?=?beginTime
groupAnimation.animations?=?[lineWidthAnimation,?transformAnimation]
groupAnimation.timeOffset?=?startTimeOffset
lineLayer.addAnimation(groupAnimation,?forKey:?"looping")
編譯運行,觀察一下效果。
注意,在這里我們把線條的初始位置設置為了-M_PI_4,同時把keyTimes設置為了[0.0, 1.0 - kAnimationDurationDelay/kAnimationDuration, 1.0]。數組的第一個和最后一個元素顯而易見:0.0代表起始,1.0代表終止。為了得到中間時間點,我們需要計算出圓形動畫完成、后半部分動畫開始(縮小的動畫)的時間。用kAnimationDurationDelay除以kAnimationDuration可以得到我們需要的結果。但因為它是個延遲動畫,所以我們應該用1.0減去它,我們需要從末尾往前倒,減去延遲時間。
現在我們已經完成了circleLayer和lineLayer的動畫了,接下來該處理中間的方形了。
方形的動畫
現在你應該已經輕車熟路了。定位到startAnimation()方法,注釋掉animateSquareLayer()以外的方法。并把init(frame:)方法修改成下面這樣:override?init(frame:?CGRect)?{??super.init(frame:?frame)
circleLayer?=?generateCircleLayer()
lineLayer?=?generateLineLayer()
squareLayer?=?generateSquareLayer()
maskLayer?=?generateMaskLayer()??//??layer.mask?=?maskLayer
layer.addSublayer(circleLayer)??//??layer.addSublayer(lineLayer)
layer.addSublayer(squareLayer)
}
修改完前往animateSquareLayer(),開始解決下一個動畫://?邊框let?b1?=?NSValue(CGRect:?CGRect(x:?0.0,?y:?0.0,?width:?2.0/3.0?*?squareLayerLength,?height:?2.0/3.0??*?squareLayerLength))let?b2?=?NSValue(CGRect:?CGRect(x:?0.0,?y:?0.0,?width:?squareLayerLength,?height:?squareLayerLength))let?b3?=?NSValue(CGRect:?CGRectZero)//?邊框從原始長度的2/3開始放大,到原始大小后再逐漸縮小到0let?boundsAnimation?=?CAKeyframeAnimation(keyPath:?"bounds")
boundsAnimation.values?=?[b1,?b2,?b3]
boundsAnimation.timingFunctions?=?[fadeInSquareTimingFunction,?squareLayerTimingFunction]
boundsAnimation.duration?=?kAnimationDuration
boundsAnimation.keyTimes?=?[0,?1.0-kAnimationDurationDelay/kAnimationDuration,?1.0]
上面的動畫改變了CALayer的邊框。我們創建了一個關鍵幀動畫,從邊長的2/3開始,放大到完整尺寸,再縮小到0。
接下來是背景顏色的動畫://?背景顏色的變化let?backgroundColorAnimation?=?CABasicAnimation(keyPath:?"backgroundColor")
backgroundColorAnimation.fromValue?=?UIColor.whiteColor().CGColorbackgroundColorAnimation.toValue?=?UIColor.fuberBlue().CGColorbackgroundColorAnimation.timingFunction?=?squareLayerTimingFunction
backgroundColorAnimation.fillMode?=?kCAFillModeBoth
backgroundColorAnimation.beginTime?=?kAnimationDurationDelay?*?2.0?/?kAnimationDuration
backgroundColorAnimation.duration?=?kAnimationDuration?/?(kAnimationDuration?-?kAnimationDurationDelay)
注意這里的fillMode屬性。由于beginTime不是零,動畫會把開始和結束時的CGColor包含進去。因此,當我們把動畫添加到父CAAnimationGroup里的時候不會閃現不同顏色。
說到CAAnimationGroup,該實現它了://?合成動畫let?groupAnimation?=?CAAnimationGroup()
groupAnimation.animations?=?[boundsAnimation,?backgroundColorAnimation]
groupAnimation.repeatCount?=?Float.infinity
groupAnimation.duration?=?kAnimationDuration
groupAnimation.removedOnCompletion?=?falsegroupAnimation.beginTime?=?beginTime
groupAnimation.timeOffset?=?startTimeOffset
squareLayer.addAnimation(groupAnimation,?forKey:?"looping")
編譯運行,檢查一下我們的進度,嗯看來方形動畫已經順利完成了。
是時候把之前的動畫合并到一起看看效果了!提示:在iOS模擬器里的動畫可能會有些卡頓,因為我們需要在Mac上模擬平時由iOS的GPU完成的工作。如果你的電腦不能流暢運行動畫,試著縮小模擬器的屏幕大小,或者在真機上測試。
遮罩層
首先取消init(frame:)和startAnimating()里所有注釋。
把所有動畫組合到一起,我們重新編譯運行一下Fuber。
看起來好像還是差點意思?cricleLayer的縮小消失太過突然。幸運的是,遮罩層動畫可以修正這個問題,讓它平滑地縮小。
定位到animateMaskLayer()添加下面的代碼://?邊框縮小let?boundsAnimation?=?CABasicAnimation(keyPath:?"bounds")
boundsAnimation.fromValue?=?NSValue(CGRect:?CGRect(x:?0.0,?y:?0.0,?width:?radius?*?2.0,?height:?radius?*?2))
boundsAnimation.toValue?=?NSValue(CGRect:?CGRect(x:?0.0,?y:?0.0,?width:?2.0/3.0?*?squareLayerLength,?height:?2.0/3.0?*?squareLayerLength))
boundsAnimation.duration?=?kAnimationDurationDelay
boundsAnimation.beginTime?=?kAnimationDuration?-?kAnimationDurationDelay
boundsAnimation.timingFunction?=?circleLayerTimingFunction
上面的代碼用于設定遮罩層邊界動畫。別忘了,當遮罩層的邊界改變時,整個AnimatedULogoView都會被遮擋,因為它作用于所有子層。
現在我們來實現圓角動畫,保持遮罩是圓形的://?邊角弧度let?cornerRadiusAnimation?=?CABasicAnimation(keyPath:?"cornerRadius")
cornerRadiusAnimation.beginTime?=?kAnimationDuration?-?kAnimationDurationDelay
cornerRadiusAnimation.duration?=?kAnimationDurationDelay
cornerRadiusAnimation.fromValue?=?radius
cornerRadiusAnimation.toValue?=?2cornerRadiusAnimation.timingFunction?=?circleLayerTimingFunction
把這兩個動畫合成為一個CAAnimationGroup,這個層就完成了://?合成動畫let?groupAnimation?=?CAAnimationGroup()
groupAnimation.removedOnCompletion?=?falsegroupAnimation.fillMode?=?kCAFillModeBoth
groupAnimation.beginTime?=?beginTime
groupAnimation.repeatCount?=?Float.infinity
groupAnimation.duration?=?kAnimationDuration
groupAnimation.animations?=?[boundsAnimation,?cornerRadiusAnimation]
groupAnimation.timeOffset?=?startTimeOffset
maskLayer.addAnimation(groupAnimation,?forKey:?"looping")
編譯運行。
看起來不錯!
教程的上半部分至此結束,關于背景網格的水波效果會在下一篇教程中介紹。