原文鏈接
How To Create an Uber Splash Screen
近日Uber與滴滴合并了,作為開發(fā)者表示真心喜歡Uber的這個啟動動畫!

因為新聞上看到Uber的消息,更新過后, 真心喜歡這個動畫效果, 之前學習iOS Core Animation時也做了一些筆記,最近在梳理知識點時看到一篇文章介紹Uber動畫的, 借此機會再對** CALayers **和 CAAnimations,溫習一下.
先放個重點,以防沒耐心的童鞋看不完導致收獲不到任何東西
貝塞爾曲線與CAShapeLayer的關(guān)系
1. CAShapeLayer中shape代表形狀的意思,所以需要形狀才能生效
2. 貝塞爾曲線可以創(chuàng)建基于矢量的路徑
3. 貝塞爾曲線給CAShapeLayer提供路徑,CAShapeLayer在提供的路徑中進行渲染。路徑會閉環(huán),所以繪制出了Shape
4. 用于CAShapeLayer的貝塞爾曲線作為Path,其path是一個首尾相接的閉環(huán)的曲線,即使該貝塞爾曲線不是一個閉環(huán)的曲線
整體分析
從UIViewController的角度出發(fā),APP的炫酷動畫(啟動動畫)應該是父視圖,由此來轉(zhuǎn)換到用戶的使用界面---地圖頁面, 直到APP請求完必要的API與加載完成必要的數(shù)據(jù)從而結(jié)束循環(huán)動畫.
RootContainerViewController中有兩個方法:showSplashViewController() 和 showSplashViewControllerNoPing(). 像大多數(shù)教程一樣, 你會調(diào)用showSplashViewControllerNoPing()循環(huán)展示動畫, 所以你可以將動畫集中到子類--SplashViewController中, 最后你可以通過調(diào)用showSplashViewController()來模擬延遲實現(xiàn)轉(zhuǎn)場到主視圖.
層級分析
SplashViewController包含兩個視圖, 一個主要負責顯示文字, 另外一個負責動畫AnimatedULogoView, 如下圖, 圖片來源:

主要的功能在AnimatedULogoView包含了四個CAShapeLayers:
1. circleLayer: 描繪背景**U**的循環(huán);
2. lineLayer: 這個直線負責顯示循環(huán)最后留有的邊緣;
3. squareLayer: 是circleLayer中間的那個縮小時的方塊'
4. maskLayer: 遮蓋其他的View, 使其他layer層實現(xiàn)波浪效果.
總的來說CAShaperLayers實現(xiàn)的效果的原型如下圖示:

第一部分
對于貝塞爾曲線和CA動畫的實現(xiàn), 如果需要工程初期項目---Download the starter project here.
第一步: 畫圓
在實現(xiàn)特效動畫時,應該拋開特效,分步驟的來分析實現(xiàn). 接下來在AnimatedULogoView.swift 中一步一步實現(xiàn)圓的效果:

//畫圓的前期配置
func generateCircleLayer() -> CAShapeLayer {
//初始化layer層
let layer = CAShapeLayer()
let width:CGFloat = 40
//在此使用半徑為寬度
layer.lineWidth = width//4
/**
* center:弧線中心點的坐標 radius:弧線所在圓的半徑 startAngle:弧線開始的角度值 endAngle:弧線結(jié)束的角度值 clockwise:是否順時針畫弧線
*/
//開始和結(jié)束的角度之和需要達到360°
layer.path = UIBezierPath(arcCenter: CGPointZero, radius: width/2, startAngle: CGFloat(-2*M_PI_2), endAngle: CGFloat(2*M_PI_2), clockwise: true).CGPath
//填充的顏色
layer.strokeColor = UIColor.redColor().CGColor
//如果不將此設置為指定顏色,將顯示黑色
layer.fillColor = UIColor.clearColor().CGColor
return layer
}
第二步
圓形動畫還需要三個CAAnimations:
1. CAKeyframeAnimation: 關(guān)鍵幀動畫;
2. CABasicAnimation: 基本動畫實現(xiàn)轉(zhuǎn)換;
3. CAAnimationGroup: 包含并實現(xiàn)以上兩種動畫.
1.CAKeyframeAnimation: 關(guān)鍵幀動畫;
//執(zhí)行動畫, 畫圓
func animationCircleLayer(){
// 關(guān)鍵幀(keyframe)使我們能夠定義動畫中任意的一個點,然后讓 Core Animation 填充所謂的中間幀。
let strokeEndAnimation = CAKeyframeAnimation(keyPath: "strokeEnd")
//設定動畫的速度變化 x - y - x - y
strokeEndAnimation.timingFunction = CAMediaTimingFunction(controlPoints: 1.00, 0.0, 0.35, 1.00)
strokeEndAnimation.duration = kAnimationDuration - kAnimationDurationDelay
//確保是一個完整的圓
strokeEndAnimation.values = [0.0, 1.0]
//keyTimes是確保開始到結(jié)束的時從0.0-1.0分別設置,避免其中的動畫產(chǎn)生跳轉(zhuǎn)。
strokeEndAnimation.keyTimes = [0.0, 1.0]
}
2.CABasicAnimation: 基本動畫實現(xiàn)轉(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)
基本動畫在Z軸上執(zhí)行縮放和轉(zhuǎn)動兩種動畫, 轉(zhuǎn)動45°后恢復至原始半徑.
3.CAAnimationGroup: 包含并實現(xiàn)以上兩種動畫.可以試著只添加一種動畫, 看看效果.
let groupAnimation = CAAnimationGroup()
//添加動畫, 可以在此刪除任一種動畫, 會看到另類效果
groupAnimation.animations = [strokeEndAnimation, transform]
//重復次數(shù)
groupAnimation.repeatCount = Float.infinity
groupAnimation.duration = kAnimationDuration
groupAnimation.beginTime = beginTime
groupAnimation.timeOffset = 0.7 * kAnimationDuration
circleLayer.addAnimation(groupAnimation, forKey: "looping")
第三步: 劃線
畫好了圓, 把接下來的那個留白的線整了, 做了這一塊, 你會發(fā)現(xiàn)動畫重在分析分步. 眼見不一定為實

func generateLineLayer() -> CAShapeLayer {
let layer = CAShapeLayer()
layer.position = CGPointZero
layer.frame = CGRectZero
layer.allowsGroupOpacity = true
layer.lineWidth = 5.0
layer.strokeColor = UIColor(red: 15/255, green: 78/255, blue: 101/255, alpha: 1).CGColor
let bezierPath = UIBezierPath()
//設置起點
bezierPath.moveToPoint(CGPointZero)
//劃線
bezierPath.addLineToPoint(CGPointMake(-40, 0.0))
layer.path = bezierPath.CGPath
return layer
}
// 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]
// 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))]
// 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")
第四步: 添加方形
添加animateSquareLayer()函數(shù)
// 2/3
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))
//0
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]
==============
// 背景色
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)
// 放在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")
以上幾個步驟拆分整個icon的動畫,接下來在進行修飾一下
//整體修飾
private func 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
// 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)格
第一步:創(chuàng)建網(wǎng)格
試著想象集群的ui視圖通過TileGridView實例。他們看起來像什么?嗯…時間停止然后產(chǎn)生隆隆聲, 接著看一看效果吧!
背景網(wǎng)格包含一系列的TileViews組成的TileGridView.打開TileView.swift找到init(frame:), 添加如下方法:
layer.borderWidth = 2.0
運行后的效果:

正如你所看到的, 這個TileViews已經(jīng)被安排為網(wǎng)格狀, 這樣被創(chuàng)建其實是調(diào)用了TileGridView.swift中的renderTileViews()方法, 這個邏輯前期已經(jīng)準備好了.你只需要實現(xiàn)它! 當然, 還是建議學學的
第二步:動態(tài)實現(xiàn)TileView
TileGridView有個containerView子類, 它負責添加所有的TileViews. 這個類中有個二維數(shù)組tileViewRows, 包含所有的添加當前的視圖上的TileViews.
回到TileView‘s init(frame:).刪除之前添加的代碼, 添加如下代碼, 將TileView中的圖片填充layer層上.
override init(frame: CGRect) {
super.init(frame: frame)
layer.contents = TileView.chimesSplashImage.CGImage
layer.shouldRasterize = true
}
如圖所示:

coooooooooool!!
然而, TileGridView和它的子視圖需要一些動態(tài)效果, 打開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]()
以上代碼定義了一系列的timing函數(shù), 接下來開始使用, 添加如下代碼:
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是個布爾類型,來控制變換/位置動畫是否添加到動畫剛剛創(chuàng)建數(shù)組中,它的默認值是true, 所有的TileViews不在TileGridView視圖上, 這個邏輯之前已經(jīng)在TileGridView中的renderTileViews()實現(xiàn)了.
添加蒙版的動畫:
// 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)
然后將這些動畫放進動畫組中:
// 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")
這個groupAnimation將添加在TileView的實例中, 現(xiàn)在還沒有包含一個動畫, 它的內(nèi)容取決于shouldEnableRipple之前那個數(shù)組.
是時候調(diào)用TileGridView中的方法了, 找到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)
}
}
}
如圖所示:

這下好多了, 但是在這個網(wǎng)狀中的沖擊波還是不夠火候, 這意味著延遲補償需要創(chuàng)建并基于視圖的中心距離乘以一個常數(shù)來創(chuàng)建.
在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)
}
回到startAnimatingWithBeginTime(_:), 用下面的代碼替換原來的代碼:
for view in tileRows {
let distance = self.distanceFromCenterViewWithView(view)
view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: kRippleDelayMultiplier * NSTimeInterval(distance), rippleOffset: CGPointZero)
}
}
在此調(diào)用延遲方法, 運行如下圖:

更好了!現(xiàn)在這個動畫開始看起來更加體面了,但仍有一些缺失。TileViews移動應該基于沖擊波的方向和大小的.
更好的方法是使用高中時學習的知識, 基于TileView的中心距離的標準化向量。在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)
}
}
}
運行如下圖:

非常酷!已經(jīng)有“放大”的感覺了,但規(guī)模動畫發(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")
運行如下如:

漂亮!
注釋: 試著去改變`kRippleMagnitudeMultiplier`和`kRippleDelayMultiplier`的值, 看看會有什么樣的效果
完成了所有的事情后, 回到RootContainerViewController.swift. 在viewDidLoad()中注釋showSplashViewControllerNoPing(), 添加showSplashViewController().
最后的效果如下

參考
1.CAShapeLayer和貝塞爾曲線
2.Stackoverflow
更多精彩內(nèi)容請關(guān)注“IT實戰(zhàn)聯(lián)盟”哦~~~
