lottie原理與案例

最近項目大量使用動畫效果,若是想用原生實現的話,無疑會大大增長研發人員的難度,即便最終實現了,可能仍是達不到UI要的效果!搜索了不少相關技術,找到了能夠友好解決項目需求的技術-lottie   - github.com/airbnb/lott…ios

Lottie簡介

lottie 是Airbnb開源的動畫庫,UI經過AE設計出動畫,使用Lottie提供的BodyMovin插件將設計好的動畫導出成JSON格式,就能夠直接在各個平臺上運用,無需其餘額外的操做。lottie 目前已經支持了iOS,macOS,以及Android和React Native。 對於iOS目前支持了Swift 4.2 ,也支持了CocoaPods 和 Carthage方式導入,對於導入和使用能夠參考上面github連接,裏面有相應的步驟,在這就不作講述。下面是lottie提供了一套完整的跨平臺動畫實現工做流:git

Lottie文件結構

UI給你們的.json文件大概以下:github

JSON文件結構json

第一層

第二層 assets

第二層 layers

Lottie應用

因爲本項目運用到了特別多的lottie動畫,特意抽取封裝到Assitant模塊中,以下:markdown

應用一

下面就以hccEngine.json爲主,看下如何使用的?前提要倒入Lottie第三方庫,咱們查看一下HCCEngineAnimationView代碼app

import Lottiepublic class HCCEngineAnimationView: UIView {
lazy var animateView: AnimationView = {
    let view = AnimationView()
    //json文件放入的位置,經過bundle取出
    let animation = Animation.named("hccEngine", bundle: Bundle(for: HCCEngineAnimationView.self))
    view.animation = animation
    ///填充方式
    view.contentMode = .scaleAspectFit
    ///執行一次
    view.loopMode = .playOnce
    /// 暫停動畫並在應到前臺時從新啓動它,在動畫完成時調用回調
    view.backgroundBehavior = .pauseAndRestore
    return view
}()

public override init(frame: CGRect) {
    super.init(frame: frame)
    self.setupSubviews()
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

private func setupSubviews() {
    ///動畫適配
    addSubview(animateView)
    animateView.snp.makeConstraints { (make) in
        make.edges.equalToSuperview()
    }
}

public func startAnimation(completion: (()-> Void)?) {
    ///開始播放動畫
    self.animateView.play { (_) in
        completion?()
    }
}

public func remove() {
    ///動畫中止並移除屏幕
    self.animateView.stop()
    self.removeFromSuperview()
}
}
複製代碼

而後在調用動畫的地方,開始初始化HCCEngineAnimationView動畫View框架

而後再適配屏幕ide

在合適的時機調用開始動畫oop

在合適的時機移除動畫字體

運行結果以下:

應用二

上面只是簡單的頁面用到了一處lottie.json文件,假如lottie加載和狀態有關係呢,那麼可能有枚舉類型的出現! 假如紅藍雙方PK,可能出現的PK結果爲紅方勝出的動畫,藍方勝出的動畫以及紅藍平局的動畫三種狀態,若是咱們寫三個封裝,顯然不合適,因此枚舉的出現解決了該問題!記住枚舉的原始值和lottie的動畫json的文件名同樣(能夠省去很多的麻煩)

public enum PKWinSideEnum: String {
    case red = "red"  //紅方勝出
    case blue = "blue"  //藍方勝出
    case draw = "draw" //雙方平局
}

public class HCCBrokerPKSuccessAnimation: UIView {

    public var type: PKWinSideEnum? {
        didSet {
            ///根據pk狀態,顯示不一樣Lottie動畫效果
            guard let side = type else { return }
            let animation = Animation.named(side.rawValue, bundle: Bundle(for: HCCBrokerPKSuccessAnimation.self))
            animateView.animation = animation
        }
    }

    lazy var animateView: AnimationView = {
        let view = AnimationView()
        view.contentMode = .scaleAspectFit
        view.loopMode = .loop
        view.backgroundBehavior = .pauseAndRestore
        return view
    }()

    public override init(frame: CGRect) {
        super.init(frame: frame)
        self.setupSubviews()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupSubviews() {
        addSubview(animateView)
        animateView.snp.makeConstraints { (make) in
            make.edges.equalToSuperview()
        }
    }

    public func startAnimation(completion: (()-> Void)?) {
        self.animateView.play { (_) in
            completion?()
        }
    }

    public func remove() {
        self.animateView.stop()
        self.removeFromSuperview()
    }
}
複製代碼

具體使用參考上面便可!

Lottie加載原理

代碼組織結構

代碼過程 

let animation = Animation.named("hccEngine", bundle: Bundle(for: HCCEngineAnimationView.self))
複製代碼

緊接着點進去查看name的實現代碼以下:

static func named(_ name: String,
                           bundle: Bundle = Bundle.main,
                           subdirectory: String? = nil,
                           animationCache: AnimationCacheProvider? = nil) -> Animation? {
    /// 建立一個cacheKey
    let cacheKey = bundle.bundlePath + (subdirectory ?? "") + "/" + name

    /// 檢查動畫key
    if let animationCache = animationCache,
      let animation = animationCache.animation(forKey: cacheKey) {
        ///若是找到了,就直接返回動畫
      return animation
    }
    /// 肯定提供的路徑文件
    guard let url = bundle.url(forResource: name, withExtension: "json", subdirectory: subdirectory) else {
      return nil
    }

    do {
      let json = try Data(contentsOf: url)
      let animation = try JSONDecoder().decode(Animation.self, from: json)
      animationCache?.setAnimation(animation, forKey: cacheKey)
      return animation
    } catch {
      print(error)
      return nil
    }
  }
複製代碼

 另外

view.animation = animation
複製代碼

裏面作了:

public var animation: Animation? {
    didSet {
      makeAnimationLayer()
    }
  }
//緊接着看makeAnimationLayer
fileprivate func makeAnimationLayer() {

    ///移除當前動畫
    removeCurrentAnimation()

    if let oldAnimation = self.animationLayer {
      oldAnimation.removeFromSuperlayer()
    }

    invalidateIntrinsicContentSize()

    guard let animation = animation else {
      return
    }
    ///經過AnimationContainer來構建animation和imageProvider等
    let animationLayer = AnimationContainer(animation: animation, imageProvider: imageProvider, textProvider: textProvider, fontProvider: fontProvider)
    animationLayer.renderScale = self.screenScale
    viewLayer?.addSublayer(animationLayer)
    self.animationLayer = animationLayer
    reloadImages()
    animationLayer.setNeedsDisplay()
    setNeedsLayout()
    currentFrame = CGFloat(animation.startFrame)
  }
複製代碼

而後查看核心類AnimationContainer初始化方法

final class AnimationContainer: CALayer {
    init(animation: Animation, imageProvider: AnimationImageProvider, textProvider: AnimationTextProvider, fontProvider: AnimationFontProvider) {
       /// 圖片layer的處理
      self.layerImageProvider = LayerImageProvider(imageProvider: imageProvider, assets: animation.assetLibrary?.imageAssets)
      /// 文字的處理
      self.layerTextProvider = LayerTextProvider(textProvider: textProvider)
      /// 字體的處理
      self.layerFontProvider = LayerFontProvider(fontProvider: fontProvider)
      self.animationLayers = []
      super.init()
      bounds = animation.bounds
      let layers = animation.layers.initializeCompositionLayers(assetLibrary: animation.assetLibrary, layerImageProvider: layerImageProvider, textProvider: textProvider, fontProvider: fontProvider, frameRate: CGFloat(animation.framerate))

      var imageLayers = [ImageCompositionLayer]()
      var textLayers = [TextCompositionLayer]()

      var mattedLayer: CompositionLayer? = nil

      //對layer圖層進行整合
      for layer in layers.reversed() {
        layer.bounds = bounds
        animationLayers.append(layer)
        if let imageLayer = layer as? ImageCompositionLayer {
          imageLayers.append(imageLayer)
        }
        if let textLayer = layer as? TextCompositionLayer {
          textLayers.append(textLayer)
        }
        if let matte = mattedLayer {
          /// The previous layer requires this layer to be its matte
          matte.matteLayer = layer
          mattedLayer = nil
          continue
        }
        if let matte = layer.matteType,
          (matte == .add || matte == .invert) {
          /// We have a layer that requires a matte.
          mattedLayer = layer
        }
        addSublayer(layer)
      }

      layerImageProvider.addImageLayers(imageLayers)
      layerImageProvider.reloadImages()
      layerTextProvider.addTextLayers(textLayers)
      layerTextProvider.reloadTexts()
      layerFontProvider.addTextLayers(textLayers)
      layerFontProvider.reloadTexts()
      setNeedsDisplay()
    }
}
複製代碼

而後咱們拿圖片layer的處理LayerImageProvider(imageProvider: imageProvider, assets: animation.assetLibrary?.imageAssets)方法

fileprivate(set) var imageLayers: [ImageCompositionLayer]
  let imageAssets: [String : ImageAsset]

  init(imageProvider: AnimationImageProvider, assets: [String : ImageAsset]?) {
    self.imageProvider = imageProvider
    self.imageLayers = [ImageCompositionLayer]()
    if let assets = assets {
      self.imageAssets = assets
    } else {
      self.imageAssets = [:]
    }
    reloadImages()
  }
複製代碼

而後查看一下assets和assetLibrary所在類的實體

若是認真查看會發現Lottie JSON文件結構與定義此實體相對應!

最後看下動畫的執行play過程以及內部作了什麼?

public func startAnimation(completion: (()-> Void)?) {
        ///開始播放動畫
        self.animateView.play { (_) in
            completion?()
        }
    }
複製代碼

點擊進去查看.play裏面

public func play(completion: LottieCompletionBlock? = nil) {
    guard let animation = animation else {
      return
    }
    ///爲動畫建立一個上下文
    let context = AnimationContext(playFrom: CGFloat(animation.startFrame),
                                   playTo: CGFloat(animation.endFrame),
                                   closure: completion)
    ///首先移除當前的動畫
    removeCurrentAnimation()
    ///添加新的動畫
    addNewAnimationForContext(context)
  }
複製代碼

而後查看如何添加新的動畫:addNewAnimationForContext(context)

/// Adds animation to animation layer and sets the delegate. If animation layer or animation are nil, exits.
  fileprivate func addNewAnimationForContext(_ animationContext: AnimationContext) {
    guard let animationlayer = animationLayer, let animation = animation else {
      return
    }

    self.animationContext = animationContext

    guard self.window != nil else { waitingToPlayAimation = true; return }

    animationID = animationID + 1
    activeAnimationName = AnimationView.animationName + String(animationID)

    let framerate = animation.framerate

    let playFrom = animationContext.playFrom.clamp(animation.startFrame, animation.endFrame)
    let playTo = animationContext.playTo.clamp(animation.startFrame, animation.endFrame)

    let duration = ((max(playFrom, playTo) - min(playFrom, playTo)) / CGFloat(framerate))

    let playingForward: Bool =
      ((animationSpeed > 0 && playFrom < playTo) ||
        (animationSpeed < 0 && playTo < playFrom))

    var startFrame = currentFrame.clamp(min(playFrom, playTo), max(playFrom, playTo))
    if startFrame == playTo {
      startFrame = playFrom
    }

    let timeOffset: TimeInterval = playingForward ?
      Double(startFrame - min(playFrom, playTo)) / framerate :
      Double(max(playFrom, playTo) - startFrame) / framerate

    ///使用CABasicAnimation實現動畫
    let layerAnimation = CABasicAnimation(keyPath: "currentFrame")
    layerAnimation.fromValue = playFrom
    layerAnimation.toValue = playTo
    layerAnimation.speed = Float(animationSpeed)
    layerAnimation.duration = TimeInterval(duration)
    layerAnimation.fillMode = CAMediaTimingFillMode.both

    switch loopMode {
    case .playOnce:
      layerAnimation.repeatCount = 1
    case .loop:
      layerAnimation.repeatCount = HUGE
    case .autoReverse:
      layerAnimation.repeatCount = HUGE
      layerAnimation.autoreverses = true
    case let .repeat(amount):
      layerAnimation.repeatCount = amount
    case let .repeatBackwards(amount):
      layerAnimation.repeatCount = amount
      layerAnimation.autoreverses = true
    }

    layerAnimation.isRemovedOnCompletion = false
    if timeOffset != 0 {
      let currentLayerTime = viewLayer?.convertTime(CACurrentMediaTime(), from: nil) ?? 0
      layerAnimation.beginTime = currentLayerTime - (timeOffset * 1 / Double(animationSpeed))
    }
    layerAnimation.delegate = animationContext.closure
    animationContext.closure.animationLayer = animationlayer
    animationContext.closure.animationKey = activeAnimationName

    animationlayer.add(layerAnimation, forKey: activeAnimationName)
    updateRasterizationState()
  }
複製代碼

Lottie實現本質是經過Layer來實現動畫的,是一個很是好的動畫框架,你們趕忙操動起來吧

相關文章
相關標籤/搜索