Derek Selander 將 Uber 動畫一步步拆解,利用遮罩,向量計算,組合等多種方式, 重新將動畫建立起來。
2016.09.26更新:此教程已使用Xcode8 和Swift3進行更新.
Oh,啟動動畫—-當應用忙碌呼叫API埠獲取功能所需的重要資料時,開發者們可以抓住這個時機瘋狂展示有趣的動畫。啟動動畫(不要與靜態的,非動畫應用開始畫面混淆)在應用中承擔重要的角色:在應用等待啟動時刻保持對使用者的吸引力。
雖然有很多啟動動畫的例子,但你很難找到一個像Uber如此漂亮的啟動動畫。在2016年第一季度,Uber釋出了由其CEO領導的品牌再造戰略。該戰略成果之一就是一個炫酷的啟動畫面。
這個教程目的在於使用十分近似的方法複製Uber的啟動畫面。該方法大量使用CALayer和CAAnimation以及它們的子類集。相比介紹在這些類中發現的設計思想方法,此教程更集中於這些類在高質量動畫上的使用。你可以檢視Marin Todorov’s Intermediate iOS Animation video series去學習這些動畫背後的設計思想。
開始
因為在這個教程中有很多有意義的動畫需要實現完成,你將會以一個專案開始,這個專案已經包含了接下來製作優美動畫所需的CALayer包。在這下載開始專案。
“開始專案”為一個名叫Fuber的應用。Fuber是一個請求式交通分享服務軟體,允許乘客請求Segway司機將他們載往城市不同的地方。Fuber增長十分迅速,已經在60多個國家為乘客們服務,但同時它也面臨著多個國家政府以及Segway聯盟的反對。
在此教程結尾時,你將會製作出一個像下面的啟動動畫:
開啟並執行Fuber啟動專案並瀏覽
以UIViewControler的角度來看,應用通過其父檢視控制類RootContainerViewController來啟動SplashViewController,該父檢視的控制可以改變子檢視的控制(UIViewController)。它迴圈播放啟動動畫直到應用啟動。當應用和API端點資訊交換或者應用需要必要的資料繼續傳送時,啟動畫面就會迴圈播放。 值得一提的是,在這個示例工程中啟動畫面在它本身的元件內. 在RootContainerViewController內showSplashViewController()和showSplashViewontrollerNoPing() 兩個函式可以用來執行畫面控制。教程中,大多數情況下,showSplashViewControllerNoPing()函式只會在動畫迴圈時呼叫,因此只需要專注SplashViewController內的子檢視動畫,接著先使用showSplashViewController()去模擬API延時,然後轉到動畫主控制器上。
啟動畫面檢視圖層的合成
SplashViewController檢視包含兩個子檢視。第一個子檢視由“波紋網格”為背景,作為TileGridView(貼片網格檢視),其包括一個網格狀佈局的TileView(貼片狀檢視)子檢視實體。另一個子檢視由‘U’圖示動畫組成,作為AnimatedULogoView(該應用logo中U動畫檢視)。
AnimatedULogoView包含四個CAShapeLayers:
- CircleLayer代表‘U’的白色圓形背景
- lineLayer是從circlelayer佈局的中心出發到其邊界的直線
- squareLayer是在circleLayer層中心的方形
- maskLayer被用於檢視的遮罩。當邊界跟隨動畫變化時,被用於在一個簡單動畫中摺疊其他的檢視層。
通過組合,這些CAShaperLayer合成Fuber應用中的‘U’。
現在已經知道這些層是如何組成的,那接下來應該去生成動畫使AnimateULogoView動起來。
為圓新增動畫
當製作動畫時,最好先評估視覺噪聲並關注動畫當前執行情況。 轉到AimatedULogoView.swift檔案,在init(frame:)內,把除了circleLayer層的其他所有被新增到檢視的子檢視層註釋掉。當完成該動畫時,可以將它們一個一個從新新增回去。程式碼應該如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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()函式去理解圓形是如何製作出來的。它是使用UIBexierPath 繪畫出的一個簡單的CAShapeLayer。仔細看這一行程式碼:
1 |
layer.path = UIBezierPath(arcCenter: CGPointZero, radius: radius/2, startAngle: -CGFloat(M_PI_2), endAngle: CGFloat(3*M_PI_2), clockwise: true).CGPath |
在預設情況下,將0作為起始角度(startAngle),貝塞爾曲線路徑從順時針3點鐘方向開始。 設定-M_PI_2的值為-90度,從圓的頂部開始到270度角處結束或者在3*M_PI_2角度處重新回到圓的頂部。因為需要實現整個圓形形成動畫,所以需要特別關注那些(引數的設定),同時將圓的半徑大小作為線的寬度(lineWidth)。
轉到annimateCircleLayer()函式佔位符處並新增如下程式碼:circleLayer動畫需要包含三個CAAnimaiton:一個CAKeyframeAnimation作為動畫行程的終點,一個CABasicAnimation作為動畫轉換,還有一個CAAnimationGroup用於將這兩者組合在一起。你將會同時建立這些動畫。
1 2 3 4 5 6 |
// 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] |
通過設定CAKeyframeAnimation動畫起始和結束值為 0.0和1.0,就告訴動畫核心框架從起始角開始畫圓到結束角處結束,就像時鐘轉動動畫。隨著strokeEnd的值增加,線的長度會沿著圓周增加,圓逐漸被充滿 。如果將引數值設定為[0.0,0.5],那麼只會畫出半個圓,因為strokeEnd將只會到達圓的半周。
現在增加轉換動畫:
1 2 3 4 5 6 7 8 9 |
// 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) |
1 2 3 4 5 6 7 8 9 |
// Group let groupAnimation = CAAnimationGroup() groupAnimation.animations = [strokeEndAnimation, transformAnimation] groupAnimation.repeatCount = Float.infinity groupAnimation.duration = kAnimationDuration groupAnimation.beginTime = beginTime groupAnimation.timeOffset = startTimeOffset circleLayer.add(groupAnimation, forKey: "looping") |
CAAnimationGroup有兩個顯著的特性被更改了:beginTime和timeOffset。如果你對它們中的任意一個都不是很熟悉,你可以在這找到這些特性的描述及用法說明.
groupAnimation的beginTime參考它的父檢視進行設定。
時間補償(timeOffset)是需要的,因為動畫第一次執行時實際上會從動畫的一半處開始。如果有更多完整的動畫,可以試著改變startTimeOffset的值並觀察視覺上的不同。
新增groupAnimation到circleLayer,然後構建並執行應用去看看動畫是什麼樣子。
提示:試一試在groupAnimation動畫組中移除strokeEndAnimation或者transformAnimation 去弄明白每個動畫的作用。 把在這個教程中你所製作的動畫都像這樣試一試,你將會驚奇於動畫組合產生的讓人意想不到的特殊視覺效果。
讓線動起來
已經完成了circleLayer動畫,接下來處理lineLayer的動畫。同樣還在AnimatedULogoView.swif檔案中,轉到startAnimating()函式並將除animateLineLayer()外的其它函式都註釋掉。結果如以下程式碼:
1 2 3 4 5 6 7 8 9 |
public func startAnimating() { beginTime = CACurrentMediaTime() layer.anchorPoint = CGPointZero // animateMaskLayer() // animateCircleLayer() animateLineLayer() // animateSquareLayer() } |
另外,更改init(frame:)中的內容,使circleLayer和lineLayer是唯一正在被使用的CALayers層:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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) } |
正確註釋掉CALayers 動畫,轉到animateLineLayer()函式並執行下一組動畫:
1 2 3 4 5 6 |
// 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)) as NSNumber, 1.0] |
這個動畫實現lineLayer寬度先增加後變小的變換
對下個動畫,新增下列程式碼:
1 2 3 4 5 6 7 8 9 10 11 |
// transform let transformAnimation = CAKeyframeAnimation(keyPath: "transform") transformAnimation.timingFunctions = [strokeEndTimingFunction, circleLayerTimingFunction] transformAnimation.duration = kAnimationDuration transformAnimation.keyTimes = [0.0, 1.0 - (kAnimationDurationDelay/kAnimationDuration) as NSNumber, 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))] |
用CAAnimationGroup將動畫組合並新增到lineLayer:同circleLayer變換動畫類似,定義一個繞Z軸順時針的旋轉。對於線條,同時執行一個25%的縮放變換,然後恢復,最後收縮到15%。
1 2 3 4 5 6 7 8 9 10 |
// Group let groupAnimation = CAAnimationGroup() groupAnimation.repeatCount = Float.infinity groupAnimation.isRemovedOnCompletion = false groupAnimation.duration = kAnimationDuration groupAnimation.beginTime = beginTime groupAnimation.animations = [lineWidthAnimation, transformAnimation] groupAnimation.timeOffset = startTimeOffset lineLayer.add(groupAnimation, forKey: "looping") |
構建並執行程式,你將看到下面漂亮的畫面
注意使用相同的命名方法,用-M_PI_4初始化排列線條和圓周行程的轉換引數。同樣用 [0.0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]來設定keyTimes。陣列中第一個和最後一個元素的意義很明顯:0代表開始,1.0代表結束,因此也可以得到開始到圓的動畫結束,第二部分(線條收縮)動畫出現之間任意想要計算的值。
因為需要在動畫播放結束時再回到播放延遲的時間點(為迴圈播放),因此用1減去kAnimationDurationDelay和 kAnimationDuration的比值(獲取延遲的時間點)
現在已經完成circleLayer和lineLayer動畫,接下來轉到中心的方形動畫。
讓方形動起來
現在應該很熟練了,和之前一樣。轉到startAnimating()函式並註釋掉除animateSquareLayer()外的函式。像下面這樣更改init(frame:)函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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()函式並實現下面動畫:
1 2 3 4 5 6 7 8 9 10 |
// 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: CGRect.zero) let boundsAnimation = CAKeyframeAnimation(keyPath: "bounds") boundsAnimation.values = [b1, b2, b3] boundsAnimation.timingFunctions = [fadeInSquareTimingFunction, squareLayerTimingFunction] boundsAnimation.duration = kAnimationDuration boundsAnimation.keyTimes = [0, 1.0-(kAnimationDurationDelay/kAnimationDuration) as NSNumber, 1.0] |
這個動畫改變CALayer的大小,是一個實現將方形邊長縮小到三分之二,然後恢復,最後變成0的關鍵幀動畫.
繼續為背景色新增動畫效果:
1 2 3 4 5 6 7 8 |
// backgroundColor let backgroundColorAnimation = CABasicAnimation(keyPath: "backgroundColor") backgroundColorAnimation.fromValue = UIColor.white.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,背景色也會在動畫開始前保持動畫開始時的CGColor,在動畫結束後保持動畫結束時的CGColor。這會使得當將這些動畫被新增到父CAAnimationGroup時不產生閃爍。
說到這裡,接下來就來實現它吧:
1 2 3 4 5 6 7 8 9 |
// Group let groupAnimation = CAAnimationGroup() groupAnimation.animations = [boundsAnimation, backgroundColorAnimation] groupAnimation.repeatCount = Float.infinity groupAnimation.duration = kAnimationDuration groupAnimation.isRemovedOnCompletion = false groupAnimation.beginTime = beginTime groupAnimation.timeOffset = startTimeOffset squareLayer.add(groupAnimation, forKey: "looping") |
構建並執行。注意方形的動畫效果。
現在可以結合前面的動畫看看整個效果。
提示:模擬器中的動畫可能會出現參差不齊不完整,這因為模擬只是在GPU上模仿IOS裝置的虛擬環境中進行的。如果你的電腦帶不動動畫,可以將模擬器顯示視窗調小或直接執行在ios裝置上。
遮罩
首先,取消所有在init(frame:)中增加的和動畫startAnimating()函式中的註釋。
在所有動畫組合下,構建並執行Fuber
動畫還有一點不足,當圓消失時,圓的尺寸會產生突變的感覺。幸運的是,遮罩動畫可以修復這個缺陷,使各子層動畫收縮平滑。
轉到animateMaskLineLayer()函式並新增下列程式碼:
1 2 3 4 5 6 7 |
// 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 |
這是平滑邊界變化的動畫。記住當邊界改變時,整個AnimatedULogoView將會消失直到該層的遮罩作用於所有的子動畫層。
現在執行一個圓角動畫 使遮罩保值圓形:
1 2 3 4 5 6 7 |
// cornerRadius let cornerRadiusAnimation = CABasicAnimation(keyPath: "cornerRadius") cornerRadiusAnimation.beginTime = kAnimationDuration - kAnimationDurationDelay cornerRadiusAnimation.duration = kAnimationDurationDelay cornerRadiusAnimation.fromValue = radius cornerRadiusAnimation.toValue = 2 cornerRadiusAnimation.timingFunction = circleLayerTimingFunction |
把這兩個動畫新增到一個CAAnimationGroup去完成這個動畫層:
1 2 3 4 5 6 7 8 9 10 |
// Group let groupAnimation = CAAnimationGroup() groupAnimation.isRemovedOnCompletion = false groupAnimation.fillMode = kCAFillModeBoth groupAnimation.beginTime = beginTime groupAnimation.repeatCount = Float.infinity groupAnimation.duration = kAnimationDuration groupAnimation.animations = [boundsAnimation, cornerRadiusAnimation] groupAnimation.timeOffset = startTimeOffset maskLayer.add(groupAnimation, forKey: "looping") |
構建並執行
看起來不錯哦!
網格
數字邊界。試著想象成串的UIViews穿過TileGridView的情景。那會是什麼樣子呢?
好…可以參考Tron這部電影去看看是什麼樣子!
背景網格由一系列新增到父類TileGridView的TileView構成。為了能更快了理解這一點,開啟TileView.swift檔案並找到init(frame:)函式。在最底部新增如下屬性:
1 |
layer.borderWidth = 2.0 |
構建並執行該應用
正如所見,TileView 被排在一個網格里。
所有這些邏輯被建立在TileGridView.swift中renderTileView()函式內。 所幸的是,方格佈局的這些邏輯在開始專案中實現了。因此你所要做的就是讓它動起來。
給TileView新增動畫
TileGridView有一個唯一的子檢視containerView。它會新增所有的子 TileView。另外,它還有一個 tileViewRows的屬性,該屬性是一個包含所有新增到containerView的 TileView二維矩陣。返回到TileView的init(frame:)函式。刪除為了顯示邊界而增加的邊框,並將chimeSplashImage前的註釋去掉,將它新增到圖層。函式如下:
1 2 3 4 5 |
override init(frame: CGRect) { super.init(frame: frame) layer.contents = TileView.chimesSplashImage.cgImage layer.shouldRasterize = true } |
構建並執行
然而, TileGridView (和所有的TileView)也需要新增一些動畫。開啟TileView.swift,轉到startAnimatingWithDuration(_:beginTime:rippleDelay:rippleOffset:)並完成下一塊的動畫:
1 2 3 4 5 6 7 |
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: CGPoint.zero) var animations = [CAAnimation]() |
這段程式碼設定了一系列的定時函式,這些函式不久將會被用到。新增如下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
if shouldEnableRipple { // Transform.scale let scaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale") scaleAnimation.values = [1, 1, 1.05, 1, 1] scaleAnimation.keyTimes = TileView.rippleAnimationKeyTimes as [NSNumber]? 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 as [NSNumber]? positionAnimation.values = [zeroPointValue, zeroPointValue, NSValue(cgPoint:rippleOffset), zeroPointValue, zeroPointValue] positionAnimation.isAdditive = true animations.append(positionAnimation) } |
shouldEnableRipple 是一個布林型別變數,它決定變換和方位動畫什麼時候會被新增到動畫序列中。對所有不在網格邊界的TileView,shouldEnableRipple的值被設為True,。這一邏輯已經隨TileGridView在renderTileView()中被建立時實現了。
新增透明動畫:
1 2 3 4 5 6 7 |
// 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。
現在把所有的動畫新增到到一個組:
1 2 3 4 5 6 7 8 9 10 11 |
// Group let groupAnimation = CAAnimationGroup() groupAnimation.repeatCount = Float.infinity groupAnimation.fillMode = kCAFillModeBackwards groupAnimation.duration = duration groupAnimation.beginTime = beginTime + rippleDelay groupAnimation.isRemovedOnCompletion = false groupAnimation.animations = animations groupAnimation.timeOffset = kAnimationTimeOffset layer.add(groupAnimation, forKey: "ripple") |
這將會把groupAnimation新增到TileView的實體中。注意動畫組中可以是一個或者三個動畫在一個組,這取決於shouldEnableRipple的值。
現在已經完成了每一個讓TileView動起來的函式,接下來在TileGridView中呼叫它們。轉到TileGridView.swift並在startAnimatingWithBeginTime()函式中增加如下程式碼:
1 2 3 4 5 6 7 |
fileprivate func startAnimatingWithBeginTime(beginTime: NSTimeInterval) { for tileRows in tileViewRows { for view in tileRows { view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: 0, rippleOffset: CGPoint.zero) } } } |
構建並執行
Hmm……這確實看起來好多了,但是AnimatedULogoView的徑向膨脹應該對所有方格中的TileView產生震盪波的動畫效果。那也就意味著,需要根據外圍檢視與大綱(基準)檢視中心之間的距離設定一個延時補償乘於適當常數的參量。
在startAnimatingWithBeginTime(:)函式底部,增加如下程式碼:
1 2 3 4 5 6 7 |
fileprivate 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) } |
上述程式碼僅僅是獲取了從centerTileView中心到檢視中心的相對距離。
轉到startAnimatingWithBeginTime(_:)並用下列程式碼進行替換:
1 2 3 4 5 6 7 |
for tileRows in tileViewRows { for view in tileRows { let distance = self.distanceFromCenterViewWithView(view: view) view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: kRippleDelayMultiplier * TimeInterval(distance), rippleOffset: CGPoint.zero) } } |
使用distanceFromCenterViewWithView(_:)函式確定動畫所需要的延時。
構建並執行
好多了!動畫開始看起來得體了,但還是有些缺陷。拼貼塊(TileView)應該隨著震盪波的方向和起伏符合物理運動。
最好的方法是拿出高中時學的數學(不要畏縮——它將會超越你對它的認識)並根據拼貼塊到中心的距離確定拼貼塊的移動向量。
1 2 3 4 5 6 7 8 |
fileprivate func normalizedVectorFromCenterViewToView(view: UIView)->CGPoint { let length = self.distanceFromCenterViewWithView(view: view) guard let centerTileView = centerTileView, length != 0 else { return CGPoint.zero } 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(_:),對程式碼進行如下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fileprivate func startAnimatingWithBeginTime(beginTime: NSTimeInterval) { for tileRows in tileViewRows { for view in tileRows { let distance = self.distanceFromCenterViewWithView(view: view) var vector = self.normalizedVectorFromCenterViewToView(view: 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的值)。
構建並執行
太酷了!現在來個錦上添花:實現放大效果,但這個動畫需要正好在遮罩邊界改變之前出現。
在startAnimatingWithBeginTime(_:)函式頂部增加如下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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.isRemovedOnCompletion = 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.add(keyframe, forKey: "scale") |
再次構建和執行
漂亮!現在你已經制作了產品級質量的動畫,很多Fuber使用者將會在Twitter上為此點讚的。
提示:試一試改變 kRippleMagnitudeMultiplier和 kRippleDelayMultiplier的值,會有很有趣的結果的。
最後,轉到RootContainerViewController.swift.。在viewDidLoad()函式中最後一行,把showSplashViewControllerNoPing()改成 showSplashViewController()。
最後一次構建和執行程式,欣賞一下自己的工作成果吧。
給自己一點讚美……真是一個酷斃了的啟動動畫!
從這出發可以去幹什麼?
你可以在這下載Fuber最終完整的工程檔案。
如果你想學習更多與動畫相關的知識,可以瀏覽iOS系列動畫教程。