此篇為譯文逸绎,若存在紕漏惹恃,請見諒。
原文:How To Create an Uber Splash Screen
一個完美的啟動動畫—通過有趣的動畫讓開發(fā)者不會再為app啟動時依賴API返回核心數(shù)據(jù)而產(chǎn)生的延時問題抓狂棺牧。有趣的啟動動畫(啟動畫面不再是靜態(tài)的巫糙,無動畫的啟動畫面)會在app中起到十分重要的作用:讓用戶有耐心等待app的啟動。
盡管我們能在很多app中見到啟動動畫颊乘,但是你很難找到一個比Uber漂亮的参淹。在2016年的第一個季度,Uber推出了新版major rebranding strare gy led by its CEO乏悄。其中的一個變化就是帶來了一個十分酷炫的啟動畫面浙值。
此篇教程的目的是盡可能地還原Uber的啟動動畫。其中重度使用 CALayer 及 CAAnimation 類檩小,包括他們的子類开呐。除了介紹這些這些類的概念,本教程會更注重如何使用這些類來構(gòu)造高質(zhì)量的動畫。想要深入學習這些動畫筐付,看這里:Marin Todorov’s Intermediate iOS Animation video series
開始
因為在教程中需要實現(xiàn)大量的動畫卵惦,所以你將從一個初始工程開始學習,我們已經(jīng)在這個工程中創(chuàng)建了所有與動畫相關(guān)的CALayer類的實例瓦戚。
下載工程
初始項目為一個名為Fuber的app沮尿。(譯者注:接下來這段話是原文作者賣萌)Fuber提供呼叫Segway(一種獨輪電動自行車)司機來接載乘客到城市中的任意一個角落。Fuber發(fā)展迅速较解,現(xiàn)在已經(jīng)在60多個國家為Segway乘客服務(wù)畜疾,但是遭到了許多國家政府的反對就像Segway工會反對用戶使用Fuber聯(lián)系Segway司機。:]
教程結(jié)束的時候印衔,你會創(chuàng)建出一個如下圖的啟動動畫:
打開并運行Fuber項目庸疾,看一看。
從UIViewController角度当编,app啟動 SplashViewController 從父視圖控制器-RootContainerViewController:負責控制它的子視圖控制器届慈。SplashViewController負責循環(huán)啟動動畫直到app準備好加載。一般這段時間內(nèi)會去請求API以獲取app啟動所必須的數(shù)據(jù)忿偷。值得一提的是金顿,在這個簡單的示例項目中,啟動動畫擁有自己的模塊鲤桥。
這里有兩個方法在 RootContainerViewController: showSplashViewController() 和 ShowSplashViewControllerNoPing()揍拆。此教程的大部分時間,你只需要調(diào)用 ShowSplashViewControllerNoPing()茶凳,它只會循環(huán)啟動動畫嫂拴,這樣你可以專注于在 SplashViewController中的動畫,之后你再會調(diào)用 showSplashViewController() 用來模擬請求API的延遲并轉(zhuǎn)場進入主視圖控制器贮喧。
啟動動畫的Views與Layers組成
SplashViewController視圖中包含兩個subview筒狠,第一個subview是 TileGridView,它有一個名為“ripple grid”的背景圖,它包含了一個格子布局的子視圖實例 TileView箱沦。另外一個subview由動畫視圖 ‘U’ icon組成辩恼,名為 AnimatedULogoView
AnimatedULogoView包含了4個CAShapeLayer:
- circleLayer 表示“U”的圓形白色背景。
- lineLayer 是一條直線從 circleLayer 的中心延伸到它的邊緣谓形。
- squareLayer 是 circleLayer 中心的正方形灶伊。
- maskLayer 當其他圖層的邊界改變時,在一個簡單的動畫中它被用來統(tǒng)一控制這些圖層寒跳。
組合起來聘萨,這些 CAShaperLayer 創(chuàng)建了Fuber的“U”。
現(xiàn)在你知道了這些圖層是怎么組合起來的童太,是時候去寫動畫代碼讓 AnimatedULogoView動起來米辐。
白色圓形背景動畫
在實現(xiàn)這些動畫的過程中碾牌,最好排除外界干擾專注于正在實現(xiàn)的動畫,點開 AnimatedULogoView.swift儡循。在 init(frame:) 中舶吗,注釋掉添加這些 sublayer 除了 circleLayer 的代碼。當完成所有動畫之后择膝,便會取消這些注釋誓琼。代碼現(xiàn)在應(yīng)該是這個樣子:
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)
}
找到 generateCircleLayer() 去看看這個圓是怎么創(chuàng)建的。它是用 UIBezierPath 創(chuàng)建出來的 CAShapeLayer 圖層肴捉。注意這一行代碼:
layer.path = UIBezierPath(arcCenter: CGPointZero, radius: radius/2, startAngle: -CGFloat(M_PI_2), endAngle: CGFloat(3*M_PI_2), clockwise: true).CGPath
默認情況下腹侣,也就是 startAngle 參數(shù)為0,貝爾塞曲線(bezier)的路徑會從右邊開始(3點鐘的位置)齿穗。當設(shè)置為 -M_PI_2 也就是-90°傲隶,這個曲線會從圓的正上方開始繪制,因為 endAngle 參數(shù)設(shè)置為270°及 3M_PI_2*窃页,曲線會在圓的正上方結(jié)束繪制跺株。因為你要動畫展示這個繪制圓的過程,所以圓的半徑 radius 與曲線的線寬 lineWidth 相同脖卖。
circleLayer 的動畫需要3個 CAAnimation組合起來:一個關(guān)鍵幀動畫 CAkeyframeAnimation 繪制圓鍵值為 strokeEnd乒省,一個轉(zhuǎn)換基礎(chǔ)關(guān)鍵幀動畫 CABasicAnimation 使圓的形態(tài)轉(zhuǎn)換,最后一個為動畫組 CAAnimationGroup 用來將前面兩個動畫組合起來畦木。接下來讓我們創(chuàng)建它們袖扛。
找到 animateCirleLayer() 添加以下代碼:
// strokeEnd
let strokeEndAnimation = CAKeyframeAnimation(keyPath: "strokeEnd")
strokeEndAnimation.timingFunction = strokeEndTimingFunction
strokeEndAnimation.duration = kAnimationDuration - kAnimationDurationDelay
strokeEndAnimation.values = [0.0, 1.0]
strokeEndAnimation.keyTimes = [0.0, 1.0]
通過設(shè)置這個動畫的 values 為 [0.0,1.0],你會看到一個很cool的類似時鐘的動畫十籍。當 strokeEnd 的值增加的時候蛆封,貝塞爾曲線的長度也跟著圓的周長增加,最后這個圓就被“填滿”了勾栗。舉個特定的例子惨篱,假如你將 values 的值設(shè)置為 [0.0,0.5],這個動畫將只會繪制到一個半圓便結(jié)束了,因為 strokeEnd 停止在圓的周長一半的位置械姻。
(譯者注:想要看到這一個小動畫的效果妒蛇,可以將這個動畫加入 circleLayer 中,添加這一行代碼:circleLayer.addAnimation(strokeEndAnimation, forKey: "looping") 后運行工程楷拳。)
現(xiàn)在來添加形態(tài)轉(zhuǎn)換動畫:
// transform
let transformAnimation = CABasicAnimation(keyPath: "transform")
transformAnimation.timingFunction = strokeEndTimingFunction
transformAnimation.duration = kAnimationDuration - kAnimationDurationDelay
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)
這個動畫包含兩個部分,一部分是比例(scale)變化吏奸,另一部分為z軸上旋轉(zhuǎn)變化欢揖。這樣 circleLayer 再繪制圓的過程中還會順時針旋轉(zhuǎn)45°。旋轉(zhuǎn)動畫十分重要奋蔚,它需要配合 lineLayer 圖層動畫的位置與速度她混。
最后烈钞,添加一個動畫組 CAAnimationGroup,這個動畫組包含了之前的兩個動畫坤按,所以你只需要將這個動畫組加入到 circleLayer圖層即可毯欣。
// Group
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 有兩個值得關(guān)注的屬性被設(shè)置:beginTime 和 timeOffset。如果你對它們都不熟悉臭脓,這里有一篇很贊的文章介紹它們以及它們的用途酗钞。
這個動畫組 groupAnimation的 beginTime 設(shè)置參照于它的父視圖。
timeOffset是必須要設(shè)置的来累,因為這個動畫不是從動畫循環(huán)的起點開始的砚作。當你完成了更多的動畫之后,嘗試去修改 startTimeOffset的值并觀察動畫發(fā)生了什么變化嘹锁。(譯者注:關(guān)于timeOffset可以這么理解葫录,假如一段動畫是一個環(huán),持續(xù)時間為5秒领猾,設(shè)置timeOffset的值為2秒米同,那么這個動畫循環(huán)將從2秒開始到5秒,然后再從0秒到2秒摔竿,這樣的一個流程)
將這個動畫組加到 circleLayer 圖層后窍霞,運行工程,動畫的效果應(yīng)該如圖:
注意:嘗試從 groupAnimation.animations 數(shù)組中移除 strokeEndAnimation 或者 transformAnimation拯坟,來看看每個動畫究竟是什么樣子的但金。盡量在本教程中對每一個你創(chuàng)建的動畫采用這個方式來預覽,你會驚訝于這些動畫組合出了你意想不到的效果郁季。
直線動畫
已經(jīng)完成了 circleLayer 動畫冷溃,接下來我們來解決 lineLayer動畫。還是在 AnimatedULogoView.swift,找到 startAnimating() 注釋掉調(diào)用動畫的代碼除了 animateLineLayer()梦裂。代碼看起來應(yīng)該是如下的樣子:
public func startAnimating() {
beginTime = CACurrentMediaTime()
layer.anchorPoint = CGPointZero
// animateMaskLayer()
// animateCircleLayer()
animateLineLayer()
// animateSquareLayer()
}
除此之外似枕,改變 init(frame:) 中的內(nèi)容,這樣我們只添加了 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() 在實現(xiàn)中添加下一組動畫:
// lineWidth
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]
這個動畫用來控制直線線寬由細到粗再到細的過程年柠。
下一個轉(zhuǎn)換動畫凿歼,添加:
// transform
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 轉(zhuǎn)換動畫很像,在這里你定義一個繞著z軸順時針旋轉(zhuǎn)冗恨。對直線而言答憔,首先執(zhí)行25%的比例變換耻警,緊接著變換成15%(百分比相對于直線原始尺寸而言)验烧。
將上面的兩個動畫使用一個 CAAnimationGroup 組合起來,并將這個組合動畫添加到 lineLayer:
// Group
let groupAnimation = CAAnimationGroup()
groupAnimation.repeatCount = Float.infinity
groupAnimation.removedOnCompletion = false
groupAnimation.duration = kAnimationDuration
groupAnimation.beginTime = beginTime
groupAnimation.animations = [lineWidthAnimation, transformAnimation]
groupAnimation.timeOffset = startTimeOffset
lineLayer.addAnimation(groupAnimation, forKey: "looping")
運行工程,看到這個prettiness(可愛单绑?0廖洹)的動畫:
請注意你使用 -M_PI_4 初始轉(zhuǎn)換值與畫圓動畫配合起來蓉驹。你還需要設(shè)置 keyTimes 為 [0.0, 1.0 -kAnimationDurationDelay/kAnimationDuration, 1.0]城榛。這個數(shù)組的第一個和最后一個元素的含義很明顯:0表示開始,1.0表示結(jié)束态兴,中間的元素需要去計算畫圓完成的時間緊接著開始縮小動畫狠持。用 kAnimationDurationDelay 除 kAnimationDuration 獲得準確的百分比,因為這是個延時動畫瞻润,所以需要用1.0減去這個百分比才是延時時間喘垂。
你現(xiàn)在已經(jīng)完成了 circleLayer 和 lineLayer 動畫,是時候?qū)崿F(xiàn)圓中心的方塊動畫敢订。
方塊動畫
接下來你應(yīng)該很熟悉了王污,找到 startAnimating() 注釋掉調(diào)用動畫的方法除了 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() 然后開始實現(xiàn)下一個動畫:
// bounds
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)
let 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 的邊界(bound)。讓這個方形的邊長從3分之2的長度開始變化到原長最后變?yōu)?矾柜。
接下來阱驾,改變背景顏色的動畫:
// backgroundColor
let backgroundColorAnimation = CABasicAnimation(keyPath: "backgroundColor")
backgroundColorAnimation.fromValue = UIColor.whiteColor().CGColor
backgroundColorAnimation.toValue = UIColor.fuberBlue().CGColor
backgroundColorAnimation.timingFunction = squareLayerTimingFunction
backgroundColorAnimation.fillMode = kCAFillModeBoth
backgroundColorAnimation.beginTime = kAnimationDurationDelay * 2.0 / kAnimationDuration
backgroundColorAnimation.duration = kAnimationDuration / (kAnimationDuration - kAnimationDurationDelay)
注意 fillMode 屬性,因為 beginTime 不為0怪蔑,這個動畫會固定住開始與結(jié)束的顏色里覆,這樣添加這個動畫進入動畫組的時候就不會出現(xiàn)閃爍。
說到這缆瓣,是時候?qū)崿F(xiàn)這個動畫組了:
// Group
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [boundsAnimation, backgroundColorAnimation]
groupAnimation.repeatCount = Float.infinity
groupAnimation.duration = kAnimationDuration
groupAnimation.removedOnCompletion = false
groupAnimation.beginTime = beginTime
groupAnimation.timeOffset = startTimeOffset
squareLayer.addAnimation(groupAnimation, forKey: "looping")
運行工程喧枷,你現(xiàn)在可以看到如下方塊動畫的效果:
是時候組合以上實現(xiàn)的動畫,看看這些動畫組合起來的效果吧弓坞!
注意:這些動畫在模擬器上顯示可能會出現(xiàn)鋸齒狀邊緣隧甚,因為是電腦模擬iOS設(shè)備的GPU。如果電腦無法實現(xiàn)這些動畫效果渡冻,嘗試切換到一個更小屏幕尺寸的模擬器或者在真機上運行程序戚扳。
MaskLayer
首先,取消 init(frame:) 以及 starAnimating() 中被注釋的代碼族吻。
所有的動畫都被添加之后帽借,運行工程:
看起來還是差一點,是吧超歌?有一個突然的閃爍當 circleLayer 的邊界(bounds)縮小的時候砍艾。幸運的是,mask動畫可以去掉這個閃爍握础,讓邊界的收縮更加平滑辐董。
找到 animateMaskLayer() 添加以下代碼:
// bounds
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
這個是改變邊界的動畫。請記住當 maskLayer 的邊界改變的時候禀综,整個 AnimateULogoView 都會消失简烘,因為 maskLayer 是最底層的圖層。
現(xiàn)在來實現(xiàn)一個 cornerRadius 動畫定枷,來保持 maskLayer 邊界是一個圓:
// cornerRadius
let cornerRadiusAnimation = CABasicAnimation(keyPath: "cornerRadius")
cornerRadiusAnimation.beginTime = kAnimationDuration - kAnimationDurationDelay
cornerRadiusAnimation.duration = kAnimationDurationDelay
cornerRadiusAnimation.fromValue = radius
cornerRadiusAnimation.toValue = 2
cornerRadiusAnimation.timingFunction = circleLayerTimingFunction
將這兩個動畫加入動畫組中孤澎,并將動畫組添加到這個圖層:
// Group
let groupAnimation = CAAnimationGroup()
groupAnimation.removedOnCompletion = false
groupAnimation.fillMode = kCAFillModeBoth
groupAnimation.beginTime = beginTime
groupAnimation.repeatCount = Float.infinity
groupAnimation.duration = kAnimationDuration
groupAnimation.animations = [boundsAnimation, cornerRadiusAnimation]
groupAnimation.timeOffset = startTimeOffset
maskLayer.addAnimation(groupAnimation, forKey: "looping")
運行工程:
看起來非常好!
網(wǎng)格
一個虛擬邊界欠窒,想象一連串的 UIView 快速穿過 TileGridView 的畫面覆旭,好了...是時候停止引用Tron,接著往下看岖妄。(譯者注:此處可去看看原文型将。)
這個背景網(wǎng)格包含這一系列的 TileView 并貼在它的父視圖 TileGridView 上。想要更直接的理解這句話荐虐,打開 TileView.swift 找到 init(frame:) 七兜。加入以下代碼:
layer.borderWidth = 2.0
運行工程:
就像你所看到的,TileView們在網(wǎng)格中排列很整齊福扬。實現(xiàn)他們的邏輯在 TileGridView.swift 的 renderTileViews() 中腕铸。接下來你需要做的就是讓它們動起來。
TileView的動畫
TileGridView 只有一個子視圖 containerView铛碑。它添加了所有子視圖 TileView狠裹。除此之外,它有一個屬性 tileViewRows 汽烦,它是個二維數(shù)組包含了所有的被加入到 container View 的 tileView涛菠。
找到 TileView 的 init(frame:) 方法。刪除那行用來顯示邊界的代碼并取消注釋添加 chimeSplashImage 到圖層的代碼撇吞。這個方法現(xiàn)在看起來是這樣:
override init(frame: CGRect) {
super.init(frame: frame)
layer.contents = TileView.chimesSplashImage.CGImage
layer.shouldRasterize = true
}
運行程序:
Coooooool...We're getting there!
無論如何俗冻,TileGridView(包括所有的 TileView) 需要一些動畫。點開 TileView.swift梢夯,找到 startAnimatingWithDuration(_:beginTime:rippleDelay:rippleOffset:) 添加下一個動畫:
let timingFunction = CAMediaTimingFunction(controlPoints: 0.25, 0, 0.2, 1)
let linearFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
let easeOutFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
let easeInOutTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let zeroPointValue = NSValue(CGPoint: CGPointZero)
var animations = [CAAnimation]()
這一段代碼聲明了幾個你即將用到的 TimingFunction變量言疗。添加以下代碼:
if shouldEnableRipple {
// Transform.scale
let scaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
scaleAnimation.values = [1, 1, 1.05, 1, 1]
scaleAnimation.keyTimes = TileView.rippleAnimationKeyTimes
scaleAnimation.timingFunctions = [linearFunction, timingFunction, timingFunction, linearFunction]
scaleAnimation.beginTime = 0.0
scaleAnimation.duration = duration
animations.append(scaleAnimation)
// Position
let positionAnimation = CAKeyframeAnimation(keyPath: "position")
positionAnimation.duration = duration
positionAnimation.timingFunctions = [linearFunction, timingFunction, timingFunction, linearFunction]
positionAnimation.keyTimes = TileView.rippleAnimationKeyTimes
positionAnimation.values = [zeroPointValue, zeroPointValue, NSValue(CGPoint:rippleOffset), zeroPointValue, zeroPointValue]
positionAnimation.additive = true
animations.append(positionAnimation)
}
shouldEnableRipple 是一個布爾值,它決定著什么時候添加轉(zhuǎn)換與位移動畫進入你剛剛創(chuàng)建的 animations 數(shù)組中颂砸。當 TileView 在 TileGridView 邊界以內(nèi)它的值為 true(譯者注:具體邏輯可以看 renderTileViews())噪奄。這個邏輯已經(jīng)被實現(xiàn),在 TileGridView.swift 的 renderTileViews() 方法中人乓。
接著添加一個 opacity(透明) 動畫:
// Opacity
let opacityAnimation = CAKeyframeAnimation(keyPath: "opacity")
opacityAnimation.duration = duration
opacityAnimation.timingFunctions = [easeInOutTimingFunction, timingFunction, timingFunction, easeOutFunction, linearFunction]
opacityAnimation.keyTimes = [0.0, 0.61, 0.7, 0.767, 0.95, 1.0]
opacityAnimation.values = [0.0, 1.0, 0.45, 0.6, 0.0, 0.0]
animations.append(opacityAnimation)
通過 keyTimes 可以很明確的知道這個動畫是如何變換透明度的勤篮。
現(xiàn)在將上面的動畫加入動畫組中:
// Group
let groupAnimation = CAAnimationGroup()
groupAnimation.repeatCount = Float.infinity
groupAnimation.fillMode = kCAFillModeBackwards
groupAnimation.duration = duration
groupAnimation.beginTime = beginTime + rippleDelay
groupAnimation.removedOnCompletion = false
groupAnimation.animations = animations
groupAnimation.timeOffset = kAnimationTimeOffset
layer.addAnimation(groupAnimation, forKey: "ripple")
這里將這個動畫組加入到 TileView 中,注意到這個動畫組可能有一個或三個動畫組成色罚,這取決于 shouldEnableRipple 的值碰缔。
現(xiàn)在你已經(jīng)實現(xiàn)了每一個 TileView 的動畫,是時候在 TileGridView 中調(diào)用它戳护〗鹇眨回過頭來看 TileGridView.swift 把下面的代碼加入到 startAnimatingWithBeginTime(_:) :
private func startAnimatingWithBeginTime(beginTime: NSTimeInterval) {
for tileRows in tileViewRows {
for view in tileRows {
view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: 0, rippleOffset: CGPointZero)
}
}
}
運行工程:
Hum...現(xiàn)在看起來更贊了瀑焦,但是缺一點東西,那就是 AnimatedULogView 放大的時候 TileGridView 要有一種波紋往外闊的效果(譯者注:就像扔一個石頭進入水中激起的波紋效果)梗肝。這意味著每一個 TileView 的動畫需要有個延遲榛瓮,延遲的大小由它與屏幕中心的距離決定。(譯者注:這里是譯者關(guān)于采用延時策略實現(xiàn)簡單波浪效果的文章:])
找到 startAnimatingWithBeginTime(_:) 巫击,加入下面這個方法:
private func distanceFromCenterViewWithView(view: UIView)->CGFloat {
guard let centerTileView = centerTileView else { return 0.0 }
let normalizedX = (view.center.x - centerTileView.center.x)
let normalizedY = (view.center.y - centerTileView.center.y)
return sqrt(normalizedX * normalizedX + normalizedY * normalizedY)
}
這個工具方法用來獲取 TileView 與中心那個 TileView 中心的距離禀晓。
回到 startAnimatingWithBeginTime(_:) ,用以下代碼替換掉原來的代碼:
for tileRows in tileViewRows {
for view in tileRows {
let distance = self.distanceFromCenterViewWithView(view)
view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: kRippleDelayMultiplier * NSTimeInterval(distance), rippleOffset: CGPointZero)
}
}
這里使用 distanceFromCenterViewWithView(_:) 方法來計算每個小動畫的延時時間坝锰。
運行工程:
更贊了粹懒!這個啟動動畫看起有那么一回事了,但是還是存在一些瑕疵顷级。這個波浪動畫看起來不是那么波浪凫乖,很僵硬不夠自然。
現(xiàn)在最好重新拿起你的高中數(shù)學(不用擔心愕把,很簡單的內(nèi)容)拣凹,用向量來表示 TileView 與中心的位置關(guān)系。
在 distanceFromCenterViewWithView(_:) 加入另外一個方法:
private func normalizedVectorFromCenterViewToView(view: UIView)->CGPoint {
let length = self.distanceFromCenterViewWithView(view)
guard let centerTileView = centerTileView where length != 0 else { return CGPointZero }
let deltaX = view.center.x - centerTileView.center.x
let deltaY = view.center.y - centerTileView.center.y
return CGPoint(x: deltaX / length, y: deltaY / length)
}
回到 startAnimatingWithBeginTime(_:)恨豁,修改代碼如下:
private func startAnimatingWithBeginTime(beginTime: NSTimeInterval) {
for tileRows in tileViewRows {
for view in tileRows {
let distance = self.distanceFromCenterViewWithView(view)
var vector = self.normalizedVectorFromCenterViewToView(view)
vector = CGPoint(x: vector.x * kRippleMagnitudeMultiplier * distance, y: vector.y * kRippleMagnitudeMultiplier * distance)
view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: kRippleDelayMultiplier * NSTimeInterval(distance), rippleOffset: vector)
}
}
}
這里計算每一個 TileView 與中心的向量嚣镜,并賦值給 rippleOffset 參數(shù)。
運行工程:
Very cool!現(xiàn)在只剩最后一步啦:實現(xiàn)背景似乎要沖出屏幕的動感畫面(如下圖)橘蜜,一個放大動畫需要在 mask 的邊界發(fā)生變化之前啟動菊匿。
在 startAnimatingWithBeginTime(_:) 最上方插入以下代碼:
let linearTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
let keyframe = CAKeyframeAnimation(keyPath: "transform.scale")
keyframe.timingFunctions = [linearTimingFunction, CAMediaTimingFunction(controlPoints: 0.6, 0.0, 0.15, 1.0), linearTimingFunction]
keyframe.repeatCount = Float.infinity;
keyframe.duration = kAnimationDuration
keyframe.removedOnCompletion = false
keyframe.keyTimes = [0.0, 0.45, 0.887, 1.0]
keyframe.values = [0.75, 0.75, 1.0, 1.0]
keyframe.beginTime = beginTime
keyframe.timeOffset = kAnimationTimeOffset
containerView.layer.addAnimation(keyframe, forKey: "scale")
運行程序:
Beautiful! You have now created a production-quality animation that many Fuber users will complain about on Twitter. Great job! :](翻譯略.....)
提示:去嘗試修改 kRippleMagnitudeMultiplier 和 kRippleDelayMultiplier 的值并看看會有哪些變化。
為了完成整個啟動流程计福,點開 RootContainerViewController.swift跌捆。在 viewDidLoad() 中,將最后一行代碼 showSplashViewControllerNoPing() 改為 showSplashViewController()象颖。
再次運行工程佩厚,欣賞下你的成果吧:
是不是很cool...一個完美的啟動動畫!
后話
你可以在這里下載完整的工程说订。
如果你想要學習更多關(guān)于動畫的知識抄瓦,看iOS Animations by Tutorials
譯者注:整個教程還是比較清晰易懂的,有什么紕漏及疑惑的地方可以撩下我哈陶冷!