iOS啟動動畫--Uber啟動動畫

原文鏈接
How To Create an Uber Splash Screen

近日Uber與滴滴合并了,作為開發(fā)者表示真心喜歡Uber的這個啟動動畫!
Paste_Image.png

因為新聞上看到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, 如下圖, 圖片來源:

RiderIconView.gif

主要的功能在AnimatedULogoView包含了四個CAShapeLayers:

1. circleLayer: 描繪背景**U**的循環(huán);
2. lineLayer: 這個直線負責顯示循環(huán)最后留有的邊緣;
3. squareLayer: 是circleLayer中間的那個縮小時的方塊'
4. maskLayer: 遮蓋其他的View, 使其他layer層實現(xiàn)波浪效果.

總的來說CAShaperLayers實現(xiàn)的效果的原型如下圖示:

Fuber-Animation (1).gif

第一部分

對于貝塞爾曲線和CA動畫的實現(xiàn), 如果需要工程初期項目---Download the starter project here.

第一步: 畫圓

在實現(xiàn)特效動畫時,應該拋開特效,分步驟的來分析實現(xiàn). 接下來在AnimatedULogoView.swift 中一步一步實現(xiàn)圓的效果:

1.gif
//畫圓的前期配置
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)動畫重在分析分步. 眼見不一定為實

2.gif
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")
    
    }



如下圖所示

3.gif

第二部分: 背景網(wǎng)格

第一步:創(chuàng)建網(wǎng)格

試著想象集群的ui視圖通過TileGridView實例。他們看起來像什么?嗯…時間停止然后產(chǎn)生隆隆聲, 接著看一看效果吧!

背景網(wǎng)格包含一系列的TileViews組成的TileGridView.打開TileView.swift找到init(frame:), 添加如下方法:

layer.borderWidth = 2.0

運行后的效果:

Paste_Image.png

正如你所看到的, 這個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
}

如圖所示:

Grid-Starting.gif

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

如圖所示:

Grid-1.gif

這下好多了, 但是在這個網(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)用延遲方法, 運行如下圖:

Grid-2.gif

更好了!現(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)
    }
  }
}

運行如下圖:

Grid-3.gif

非常酷!已經(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")

運行如下如:

FuberFinal.gif

漂亮!


注釋: 試著去改變`kRippleMagnitudeMultiplier`和`kRippleDelayMultiplier`的值, 看看會有什么樣的效果

完成了所有的事情后, 回到RootContainerViewController.swift. 在viewDidLoad()中注釋showSplashViewControllerNoPing(), 添加showSplashViewController().

最后的效果如下

Fuber-Animation (1).gif

參考
1.CAShapeLayer和貝塞爾曲線
2.Stackoverflow

更多精彩內(nèi)容請關(guān)注“IT實戰(zhàn)聯(lián)盟”哦~~~


IT實戰(zhàn)聯(lián)盟.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,423評論 4 61
  • 1.帶娃就診聞罵聲 前兩天娃突發(fā)高燒,帶娃去某知名兒童醫(yī)院看病,等待就診期間,看見有位患兒媽媽在樓道里聲嘶力竭地大...
    小海貍媽媽閱讀 271評論 0 0
  • 洛陽古城巷子里的青石板 躺在地上,任車輪碾過 馬蹄踐踏,千萬人踩來踩去 身上沾滿唾沫,塵土乃至屎尿 像一個流浪兒,...
    肖建東閱讀 226評論 10 6
  • 0又是一年中秋節(jié),家人團聚把酒言歡的時候。秋季養(yǎng)生,大家要對飲食多有留心哦,不要出現(xiàn)尷尬場面哦。中秋節(jié)是僅次于春節(jié)...
    創(chuàng)客生活匯閱讀 1,285評論 0 1
  • 有些人用心感悟生活,情到深處會流淚。 一句珍重,竟掀起我一心的不舍也凄涼。 每個人都走了一些些彎彎曲曲的線。 所以...
    各自珍重閱讀 354評論 0 0

友情鏈接更多精彩內(nèi)容