你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 模仿Uber的啟動畫面(上)

模仿Uber的啟動畫面(上)

編輯:IOS開發基礎

啟動畫面(Splash Screen)——不但給開發者們提供了一個盡情發揮、創建有趣動畫的機會,也填補了App啟動時從終端慢吞吞地下載數據的時間。啟動畫面(動態的)對於App至關重要:它可以讓用戶不失興趣地耐心等待應用完成加載。

盡管現在的啟動畫面多種多樣,但很少有像Uber這般精美的。2016年第一季度,Uber的CEO發表了關於重塑品牌的策略,其中之一就是現在這個超酷的啟動畫面。

這篇教程的目的是盡可能真實地再現Uber的動畫。我們會大量地使用到CALayersCAAnimations,以及它們的子類。我不會從頭介紹這些類的基本概念,而是把重點放在如何應用這些類,創建高質量的動畫。如果你想要了解動畫背後的基本原理,可以參考Marin Todorove的iOS動畫中級教程。

入門

由於有非常多的動畫要實現,我們不妨在這個初始項目的基礎上進行修改。初始項目裡已經為你創建好了所有需要的CALayer,我們給它們添加動畫即可。

譯者注:為了保持教程簡潔,刪除了原文裡一些與教學無關的文字。如有興趣可通過文章最後的鏈接閱讀原文相關內容。

先來看一眼最終效果:

Fuber-Animation.gif

打開初始項目看看裡面的文件。

從控制器的角度分析,項目中的SplashViewController通過它的父視圖控制器RootContainerViewController生成。SplashViewController會不停循環播放啟動動畫,直到App完全加載完成,即與終端API握手成功並獲取了必要的數據。值得一提的是,在這個示例項目裡,啟動動畫抽象成了一個單獨的模塊(譯者注:可以直接集成到其他項目裡)。

RootContainerViewController裡有兩個方法:showSplashViewController()showSplashViewControllerNoPing()。我們主要使用第二個方法,它只會不停循環播放動畫(不會進入主界面),便於我們把精力集中在SplashViewController的子視圖上。當然,最後我們還是會切換回第一個方法,模擬API延遲並過渡到主界面。

啟動畫面的視圖和層

SplashViewController包含了兩個子視圖。其一是“波紋格子”背景,我們把它叫做TileGridView,    由一系列TileView組成。另一個是帶有動畫效果的“U”字Logo,我們把它叫做AnimatedULogoView

Fuber-View-Hierarchy-1.png

AnimatedULogoView裡有4個CAShapeLayer:

  • circleLayer:“U”型Logo的圓形白色背景

  • lineLayercircleLayer中心到邊界的一條直線

  • squareLayercircleLayer中心位置的正方形

  • maskLayer:遮罩層,當它的邊界隨著動畫改變的時會遮蓋其他層

這些CAShapeLayer組合在一起,構成了Fuber標志性的“U”。

既然已經知道這些層的組合方式,那我們可以開始創建動畫,讓AnimatedULogoView動起來了。

RiderIconView.gif

圓形的動畫

制作動畫的時候,最好過濾掉其他視覺“噪音”,只關注當前實現的動畫。打開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設定了兩個重要的屬性:beginTimetimeOffset,如果你對它們不熟悉的話可以參考這篇文章,裡面有這兩個屬性的描述以及用法。

這裡的groupAnimationbeginTime屬性是根據父視圖的時間設定的。
timeOffset在這裡也需要設定,因為這個動畫在第一次運行的時候,實際上是從中途開始的。當我們完成更多動畫時,你可以回到這裡,嘗試改變startTimeOffset的值並觀察效果的差別。

groupAnimation添加給circleLayer,編譯運行一下看看目前的效果:

CircleIn-Animation.gif

提示:試著刪除strokeEndAnimation或者transformAnimation,看看單獨的每一個動畫是什麼樣的。在這篇教程裡,你可以嘗試不同動畫的效果。你可能會驚奇地發現,不同是動畫組合竟能創建出如此意想不到的獨特視覺效果。

直線的動畫

現在我們已經完成circleLayer的動畫了,該開始說說lineLayer了。還是在AnimatedULogoView.swift裡,定位到startAnimating()把除了animateLineLayer()以外的方法全部注釋掉。注釋完的代碼應該如下面所示:

public func startAnimating() {
  beginTime = CACurrentMediaTime()
  layer.anchorPoint = CGPointZero

  //  animateMaskLayer()
  //  animateCircleLayer()
  animateLineLayer()  //  animateSquareLayer()}

此外還需要調整一下init(frame:),只顯示circleLayerlineLayer

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")

編譯運行,觀察一下效果。

Knockoutline-Animation.gif

注意,在這裡我們把線條的初始位置設置為了-M_PI_4,同時把keyTimes設置為了[0.0, 1.0 - kAnimationDurationDelay/kAnimationDuration, 1.0]。數組的第一個和最後一個元素顯而易見:0.0代表起始,1.0代表終止。為了得到中間時間點,我們需要計算出圓形動畫完成、後半部分動畫開始(縮小的動畫)的時間。用kAnimationDurationDelay除以kAnimationDuration可以得到我們需要的結果。但因為它是個延遲動畫,所以我們應該用1.0減去它,我們需要從末尾往前倒,減去延遲時間。

現在我們已經完成了circleLayerlineLayer的動畫了,接下來該處理中間的方形了。

方形的動畫

現在你應該已經輕車熟路了。定位到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")

編譯運行,檢查一下我們的進度,嗯看來方形動畫已經順利完成了。

KnockoutSquare-Animation.gif

是時候把之前的動畫合並到一起看看效果了!

提示:在iOS模擬器裡的動畫可能會有些卡頓,因為我們需要在Mac上模擬平時由iOS的GPU完成的工作。如果你的電腦不能流暢運行動畫,試著縮小模擬器的屏幕大小,或者在真機上測試。

遮罩層

首先取消init(frame:)startAnimating()裡所有注釋。

把所有動畫組合到一起,我們重新編譯運行一下Fuber。

PreMask-Animation.gif

看起來好像還是差點意思?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")

編譯運行。

RiderIconView-Animation.gif

看起來不錯!

教程的上半部分至此結束,關於背景網格的水波效果會在下一篇教程中介紹。

原文鏈接:How To Create an Uber Splash Screen

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved