你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS動畫教你編寫Slack的Loading動畫進階篇

iOS動畫教你編寫Slack的Loading動畫進階篇

編輯:IOS開發綜合

前幾天看了一篇關於動畫的博客叫手摸手教你寫 Slack 的 Loading 動畫,看著挺炫,但是是安卓版的,尋思的著仿造著寫一篇iOS版的,下面是我寫這個動畫的分解~ 

老規矩先上圖和demo地址:

這裡寫圖片描述

剛看到這個動畫的時候,腦海裡出現了兩個方案,一種是通過drawRect畫出來,然後配合CADisplayLink不停的繪制線的樣式;第二種是通過CAShapeLayer配合CAAnimation來實現動畫效果。再三考慮覺得使用後者,因為前者需要計算很多,比較復雜,而且經過測試前者相比於後者消耗更多的CPU,下面將我的思路寫下來:

相關配置和初始化方法

在寫這個動畫之前,我們把先需要的屬性寫好,比如線條的粗細,動畫的時間等等,下面是相關的配置和初識化方法:

  //線的寬度
  var lineWidth:CGFloat = 0
  //線的長度
  var lineLength:CGFloat = 0
  //邊距
  var margin:CGFloat = 0
  //動畫時間
  var duration:Double = 2
  //動畫的間隔時間
  var interval:Double = 1
  //四條線的顏色
  var colors:[UIColor] = [UIColor.init(rgba: "#9DD4E9") , UIColor.init(rgba: "#F5BD58"), UIColor.init(rgba: "#FF317E") , UIColor.init(rgba: "#6FC9B5")]
  //動畫的狀態
  private(set) var status:AnimationStatus = .Normal
  //四條線
  private var lines:[CAShapeLayer] = []

  enum AnimationStatus {
    //普通狀態
    case Normal
    //動畫中
    case Animating
    //暫停
    case pause
  }

   //MARK: Initial Methods
  convenience init(fram: CGRect , colors: [UIColor]) {
    self.init()
    self.frame = frame
    self.colors = colors
    config()
  }

  override init(frame: CGRect) {
    super.init(frame: frame)
    config()
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    config()
  }

  private func config() {
    lineLength = max(frame.width, frame.height)
    lineWidth = lineLength/6.0
    margin   = lineLength/4.5 + lineWidth/2
    drawLineShapeLayer()
    transform = CGAffineTransformRotate(CGAffineTransformIdentity, angle(-30))
  }

通過CAShapeLayer繪制線條

看到這個線條我就想到了用CAShapeLayer來處理,因為CAShapeLayer完全可以實現這種效果,而且它的strokeEnd的屬性可以用來實現線條的長度變化的動畫,下面上繪制四根線條的代碼:

這裡寫圖片描述

//MARK: 繪制線
  /**
   繪制四條線
   */
  private func drawLineShapeLayer() {
    //開始點
    let startPoint = [point(lineWidth/2, y: margin),
             point(lineLength - margin, y: lineWidth/2),
             point(lineLength - lineWidth/2, y: lineLength - margin),
             point(margin, y: lineLength - lineWidth/2)]
    //結束點
    let endPoint  = [point(lineLength - lineWidth/2, y: margin) ,
             point(lineLength - margin, y: lineLength - lineWidth/2) ,
             point(lineWidth/2, y: lineLength - margin) ,
             point(margin, y: lineWidth/2)]
    for i in 0...3 {
      let line:CAShapeLayer = CAShapeLayer()
      line.lineWidth  = lineWidth
      line.lineCap   = kCALineCapRound
      line.opacity   = 0.8
      line.strokeColor = colors[i].CGColor
      line.path    = getLinePath(startPoint[i], endPoint: endPoint[i]).CGPath
      layer.addSublayer(line)
      lines.append(line)
    }

  }

  /**
   獲取線的路徑

   - parameter startPoint: 開始點
   - parameter endPoint:  結束點

   - returns: 線的路徑
   */
  private func getLinePath(startPoint: CGPoint, endPoint: CGPoint) -> UIBezierPath {
    let path = UIBezierPath()
    path.moveToPoint(startPoint)
    path.addLineToPoint(endPoint)
    return path
  }

  private func point(x:CGFloat , y:CGFloat) -> CGPoint {
    return CGPointMake(x, y)
  }

  private func angle(angle: Double) -> CGFloat {
    return CGFloat(angle * (M_PI/180))
  }

執行完後就跟上圖一樣的效果了~~~

動畫分解

經過分析,可以將動畫分為四個步驟:
 •畫布的旋轉動畫,旋轉兩圈
 •線條由長變短的動畫,更畫布選擇的動畫一起執行,旋轉一圈的時候結束
 •線條的位移動畫,線條逐漸向中間靠攏,再畫筆旋轉完一圈的時候執行,兩圈的時候結束
 •線條由短變長的動畫,畫布旋轉完兩圈的時候執行 

第一步畫布旋轉動畫

這裡我們使用CABasicAnimation基礎動畫,keyPath作用於畫布的transform.rotation.z,以z軸為目標進行旋轉,下面是效果圖和代碼:

這裡寫圖片描述

//MARK: 動畫步驟
  /**
   旋轉的動畫,旋轉兩圈
   */
  private func angleAnimation() {
    let angleAnimation         = CABasicAnimation.init(keyPath: "transform.rotation.z")
    angleAnimation.fromValue      = angle(-30)
    angleAnimation.toValue       = angle(690)
    angleAnimation.fillMode      = kCAFillModeForwards
    angleAnimation.removedOnCompletion = false
    angleAnimation.duration      = duration
    angleAnimation.delegate      = self
    layer.addAnimation(angleAnimation, forKey: "angleAnimation")
  }

第二步線條由長變短的動畫

這裡我們還是使用CABasicAnimation基礎動畫,keyPath作用於線條的strokeEnd屬性,讓strokeEnd從1到0來實現線條長短的動畫,下面是效果圖和代碼:

這裡寫圖片描述

/**
   線的第一步動畫,線長從長變短
   */
  private func lineAnimationOne() {
    let lineAnimationOne         = CABasicAnimation.init(keyPath: "strokeEnd")
    lineAnimationOne.duration      = duration/2
    lineAnimationOne.fillMode      = kCAFillModeForwards
    lineAnimationOne.removedOnCompletion = false
    lineAnimationOne.fromValue      = 1
    lineAnimationOne.toValue       = 0
    for i in 0...3 {
      let lineLayer = lines[i]
      lineLayer.addAnimation(lineAnimationOne, forKey: "lineAnimationOne")
    }
  }

第三步線條的位移動畫

這裡我們也是使用CABasicAnimation基礎動畫,keyPath作用於線條的transform.translation.x和transform.translation.y屬性,來實現向中間聚攏的效果,下面是效果圖和代碼:

這裡寫圖片描述

/**
   線的第二步動畫,線向中間平移
   */
  private func lineAnimationTwo() {
    for i in 0...3 {
      var keypath = "transform.translation.x"
      if i%2 == 1 {
        keypath = "transform.translation.y"
      }
      let lineAnimationTwo = CABasicAnimation.init(keyPath: keypath)
      lineAnimationTwo.beginTime = CACurrentMediaTime() + duration/2
      lineAnimationTwo.duration = duration/4
      lineAnimationTwo.fillMode = kCAFillModeForwards
      lineAnimationTwo.removedOnCompletion = false
      lineAnimationTwo.autoreverses = true
      lineAnimationTwo.fromValue = 0
      if i < 2 {
        lineAnimationTwo.toValue = lineLength/4
      }else {
        lineAnimationTwo.toValue = -lineLength/4
      }
      let lineLayer = lines[i]
      lineLayer.addAnimation(lineAnimationTwo, forKey: "lineAnimationTwo")
    }

    //三角形兩邊的比例
    let scale = (lineLength - 2*margin)/(lineLength - lineWidth)
    for i in 0...3 {
      var keypath = "transform.translation.y"
      if i%2 == 1 {
        keypath = "transform.translation.x"
      }
      let lineAnimationTwo = CABasicAnimation.init(keyPath: keypath)
      lineAnimationTwo.beginTime = CACurrentMediaTime() + duration/2
      lineAnimationTwo.duration = duration/4
      lineAnimationTwo.fillMode = kCAFillModeForwards
      lineAnimationTwo.removedOnCompletion = false
      lineAnimationTwo.autoreverses = true
      lineAnimationTwo.fromValue = 0
      if i == 0 || i == 3 {
        lineAnimationTwo.toValue = lineLength/4 * scale
      }else {
        lineAnimationTwo.toValue = -lineLength/4 * scale
      }
      let lineLayer = lines[i]
      lineLayer.addAnimation(lineAnimationTwo, forKey: "lineAnimationThree")
    }
  }

第四步線條恢復的原來長度的動畫

這裡我們還是使用CABasicAnimation基礎動畫,keyPath作用於線條的strokeEnd屬性,讓strokeEnd從0到1來實現線條長短的動畫,下面是效果圖和代碼:

這裡寫圖片描述

/**
   線的第三步動畫,線由短變長
   */
  private func lineAnimationThree() {
    //線移動的動畫
    let lineAnimationFour         = CABasicAnimation.init(keyPath: "strokeEnd")
    lineAnimationFour.beginTime      = CACurrentMediaTime() + duration
    lineAnimationFour.duration      = duration/4
    lineAnimationFour.fillMode      = kCAFillModeForwards
    lineAnimationFour.removedOnCompletion = false
    lineAnimationFour.fromValue      = 0
    lineAnimationFour.toValue       = 1
    for i in 0...3 {
      if i == 3 {
        lineAnimationFour.delegate = self
      }
      let lineLayer = lines[i]
      lineLayer.addAnimation(lineAnimationFour, forKey: "lineAnimationFour")
    }
  }

最後一步需要將動畫組合起來

關於動畫組合我沒用到CAAnimationGroup,因為這些動畫並不是加到同一個layer上,再加上動畫類型有點多加起來也比較麻煩,我就通過動畫的beginTime屬性來控制動畫的執行順序,還加了動畫暫停和繼續的功能,效果和代碼見下圖:

這裡寫圖片描述

//MARK: Public Methods
  /**
   開始動畫
   */
  func startAnimation() {
    angleAnimation()
    lineAnimationOne()
    lineAnimationTwo()
    lineAnimationThree()
  }

  /**
   暫停動畫
   */
  func pauseAnimation() {
    layer.pauseAnimation()
    for lineLayer in lines {
      lineLayer.pauseAnimation()
    }
    status = .pause
  }

  /**
   繼續動畫
   */
  func resumeAnimation() {
    layer.resumeAnimation()
    for lineLayer in lines {
      lineLayer.resumeAnimation()
    }
    status = .Animating
  }

  extension CALayer {
  //暫停動畫
  func pauseAnimation() {
    // 將當前時間CACurrentMediaTime轉換為layer上的時間, 即將parent time轉換為localtime
    let pauseTime = convertTime(CACurrentMediaTime(), fromLayer: nil)
    // 設置layer的timeOffset, 在繼續操作也會使用到
    timeOffset  = pauseTime
    // localtime與parenttime的比例為0, 意味著localtime暫停了
    speed     = 0;
  }

  //繼續動畫
  func resumeAnimation() {
    let pausedTime = timeOffset
    speed     = 1
    timeOffset   = 0;
    beginTime   = 0
    // 計算暫停時間
    let sincePause = convertTime(CACurrentMediaTime(), fromLayer: nil) - pausedTime
    // local time相對於parent time時間的beginTime
    beginTime   = sincePause
  }
}

//MARK: Animation Delegate
  override func animationDidStart(anim: CAAnimation) {
    if let animation = anim as? CABasicAnimation {
      if animation.keyPath == "transform.rotation.z" {
        status = .Animating
      }
    }
  }

  override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
    if let animation = anim as? CABasicAnimation {
      if animation.keyPath == "strokeEnd" {
        if flag {
          status = .Normal
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(interval) * Int64(NSEC_PER_SEC)), dispatch_get_main_queue(), {
            if self.status != .Animating {
              self.startAnimation()
            }
          })
        }
      }
    }
  }

   //MARK: Override
  override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
    switch status {
    case .Animating:
      pauseAnimation()
    case .pause:
      resumeAnimation()
    case .Normal:
      startAnimation()
    }
  }

總結

動畫看起來挺復雜,但是細細劃分出來也就那麼回事,在寫動畫之前要先想好動畫的步驟,這個很關鍵,希望大家通過這篇博文章可以學到東西,有什麼好的建議可以隨時提出來,謝謝大家閱讀~~demo地址

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持本站。

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