lottie原理与案例

3,780 阅读5分钟

最近项目大量使用动画效果,如果想用原生实现的话,无疑会大大增加研发人员的难度,即使最终实现了,可能还是达不到UI要的效果!搜索了很多相关技术,找到了可以友好解决项目需求的技术-lottie   - github.com/airbnb/lott…

Lottie简介

lottie 是Airbnb开源的动画库,UI通过AE设计出动画,使用Lottie提供的BodyMovin插件将设计好的动画导出成JSON格式,就可以直接在各个平台上运用,无需其他额外的操作。lottie 目前已经支持了iOS,macOS,以及Android和React Native。 对于iOS目前支持了Swift 4.2 ,也支持了CocoaPods 和 Carthage方式导入,对于导入和使用可以参考上面github链接,里面有相应的步骤,在这就不做讲述。下面是lottie提供了一套完整的跨平台动画实现工作流:

Lottie文件结构

UI给大家的.json文件大概如下:

JSON文件结构

第一层

第二层 assets

第二层 layers

Lottie应用

由于本项目运用到了特别多的lottie动画,特地抽取封装到Assitant模块中,如下:

应用一

下面就以hccEngine.json为主,看下如何使用的?前提要倒入Lottie第三方库,我们查看一下HCCEngineAnimationView代码

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

然后再适配屏幕

在合适的时机调用开始动画

在合适的时机移除动画

运行结果如下:

应用二

上面只是简单的页面用到了一处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来实现动画的,是一个非常好的动画框架,大家赶紧操动起来吧

机会❤️❤️❤️🌹🌹🌹

如果想和我一起共建抖音,成为一名bytedancer,Come on。期待你的加入!!!

截屏2022-06-08 下午6.09.11.png