Lottie-分享(一)

2021-12-30  本文已影响0人  恩说吧

一、Lottie是什么?

Lottie 是一个可应用于Andriod和iOS的动画库,它通过bodymovin插件来解析Adobe After Effects 动画并导出为json文件,通过手机端原生的方式或者通过React Native的方式渲染出矢量动画。

官方使用文档:http://airbnb.io/lottie/ios/dynamic.html

二、Lottie 使用

最基本的方式是用AnimationView来使用它:

// JSONFileName 指的就是用 AE 导出的动画 本地 JSON文件名
let animationView = AnimationView(name: "JSONFileName")
    
// 可以使用 frame 也可以 使用自动布局
animationView.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
    
view.addSubview(animationView)
    
animationView.play { (isFinished) in
    
    // 动画执行完成后的回调
    // Do Something
}

如果你使用到了跨bundle的JSON文件,或者需要从磁盘加载JSON文件,可使用对应的初始化方法:

/**
从本地支持的JSON文件加载Lottie动画.
   
- Parameter name: JSON文件名.
- Parameter bundle: 动画所在的包.
- Parameter imageProvider: 加载动画需要的图片资源(有些动画需要图片配合【可以是本地图片资源,也可以是网络图片资源,实现该协议返回对应的CGImage】).
- Parameter animationCache: 缓存机制【需要自己实现缓存机制,Lottie本身不支持】).
*/
convenience init(name: String,
                      bundle: Bundle = Bundle.main,
                      imageProvider: AnimationImageProvider? = nil,
                      animationCache: AnimationCacheProvider? = LRUAnimationCache.sharedCache) { ... }

// 从磁盘路径加载动画
convenience init(filePath: String,
                          imageProvider: AnimationImageProvider? = nil,
                          animationCache: AnimationCacheProvider? = LRUAnimationCache.sharedCache) { ... }

// 从网络加载
convenience init(url: URL,
                  imageProvider: AnimationImageProvider? = nil,
                  closure: @escaping AnimationView.DownloadClosure,
                  animationCache: AnimationCacheProvider? = LRUAnimationCache.sharedCache) { ... }

Lottie 支持iOS中的UIView.ContentMode的 scaleAspectFit, scaleAspectFill 和 scaleToFill 这些属性。

let animationView = AnimationView(name: "JSONFileName")
        
// 填充模式
animationView.contentMode = .scaleToFill

Lottie 动画的播放控制

/** 
播放、暂停、停止
*/
let animationView = AnimationView(name: "someJSONFileName")
// 从上一次的动画位置开始播放
animationView.play()
// 暂停动画播放
animationView.pause()
// 停止动画播放,此时动画进度重置为0
animationView.stop() 

/// 设置`play`调用的循环行为。 默认为“playOnce”
/// 定义动画循环行为
public enum LottieLoopMode {
 /// 动画播放一次然后停止。
  case playOnce
   /// 动画将从头到尾循环直到停止。
  case loop
 /// 动画将向前播放,然后向后播放并循环直至停止。
  case autoReverse
  /// Animation will loop from end to beginning up to defined amount of times.
  case `repeat`(Float)
  /// Animation will play forward, then backwards a defined amount of times.
  case repeatBackwards(Float)
}

// 循环模式
animationView.loopMode = .playOnce

/**
到后台时AnimationView的行为。
默认为“暂停”,在到后台暂停动画。 回调会以“false”调用完成。
*/
/// 到后台时AnimationView的行为
public enum LottieBackgroundBehavior {
    /// 停止动画并将其重置为当前播放时间的开头。 调用完成回调。
    case stop
    /// 暂停动画,回调会以“false”调用完成。
    case pause
    /// 暂停动画并在应到前台时重新启动它,在动画完成时调用回调
    case pauseAndRestore
}
        
// 到后台的行为模式
animationView.backgroundBehavior = .pause

/**
 播放动画,进度(0 ~ 1).
 
 - Parameter fromProgress: 动画的开始进度。 如果是'nil`,动画将从当前进度开始。
 - Parameter toProgress: 动画的结束进度。
 - Parameter toProgress: 动画的循环行为。 如果是`nil`,将使用视图的`loopMode`属性。默认是 .playOnce
 - Parameter completion: 动画停止时要调用的可选完成闭包。
 */
//        public func play(fromProgress: AnimationProgressTime? = nil,
//                         toProgress: AnimationProgressTime,
//                         loopMode: LottieLoopMode? = nil,
//                         completion: LottieCompletionBlock? = nil)
animationView.play(fromProgress: 0, toProgress: 1, loopMode: .playOnce) { (isFinished) in
    // 播放完成后的回调闭包
}
// 设置当前进度
animationView.currentProgress = 0.5

/**
 使用帧的方式播放动画
 
 - Parameter fromProgress: 动画的开始进度。 如果是'nil`,动画将从当前进度开始。
 - Parameter toProgress: 动画的结束进度
 - Parameter toProgress: 动画的循环行为。 如果是`nil`,将使用视图的`loopMode`属性。
 - Parameter completion: 动画停止时要调用的可选完成闭包。
 */
//        public func play(fromFrame: AnimationFrameTime? = nil,
//                         toFrame: AnimationFrameTime,
//                         loopMode: LottieLoopMode? = nil,
//                         completion: LottieCompletionBlock? = nil)
animationView.play(fromFrame: 50, toFrame: 80, loopMode: .loop) { (isFinished) in
    // 播放完成后的回调闭包
}
// 设置当前帧
animationView.currentFrame = 65

三、Lottie如何加载JSON文件

JSON文件格式化后类型如下:
json文件格式.png
其中的部分参数定义为:

v :版本号
ip:原大小
op:目标大小
w:宽度
h:高度
nm:文件名称
assets:图片文件
fonts:字体
layers:动画效果
markers:
chars:文字效果

public class Animation: Codable {
  
  /// The version of the JSON Schema.
  let version: String
  
  /// The coordinate space of the composition.
  let type: CoordinateSpace
  
  /// The start time of the composition in frameTime.
  public let startFrame: AnimationFrameTime
  
  /// The end time of the composition in frameTime.
  public let endFrame: AnimationFrameTime
  
  /// The frame rate of the composition.
  public let framerate: Double
  
  /// The height of the composition in points.
  let width: Int
  
  /// The width of the composition in points.
  let height: Int
  
  /// The list of animation layers
  let layers: [LayerModel]
  
  /// The list of glyphs used for text rendering
  let glyphs: [Glyph]?
  
  /// The list of fonts used for text rendering
  let fonts: FontList?
  
  /// Asset Library
  let assetLibrary: AssetLibrary?
  
  /// Markers
  let markers: [Marker]?
  let markerMap: [String : Marker]?
  
  enum CodingKeys : String, CodingKey {
    case version = "v"
    case type = "ddd"
    case startFrame = "ip"
    case endFrame = "op"
    case framerate = "fr"
    case width = "w"
    case height = "h"
    case layers = "layers"
    case glyphs = "chars"
    case fonts = "fonts"
    case assetLibrary = "assets"
    case markers = "markers"
  }
  
  required public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: Animation.CodingKeys.self)
    self.version = try container.decode(String.self, forKey: .version)
    self.type = try container.decodeIfPresent(CoordinateSpace.self, forKey: .type) ?? .type2d
    self.startFrame = try container.decode(AnimationFrameTime.self, forKey: .startFrame)
    self.endFrame = try container.decode(AnimationFrameTime.self, forKey: .endFrame)
    self.framerate = try container.decode(Double.self, forKey: .framerate)
    self.width = try container.decode(Int.self, forKey: .width)
    self.height = try container.decode(Int.self, forKey: .height)
    self.layers = try container.decode([LayerModel].self, ofFamily: LayerType.self, forKey: .layers)
    self.glyphs = try container.decodeIfPresent([Glyph].self, forKey: .glyphs)
    self.fonts = try container.decodeIfPresent(FontList.self, forKey: .fonts)
    self.assetLibrary = try container.decodeIfPresent(AssetLibrary.self, forKey: .assetLibrary)
    self.markers = try container.decodeIfPresent([Marker].self, forKey: .markers)
    
    if let markers = markers {
      var markerMap: [String : Marker] = [:]
      for marker in markers {
        markerMap[marker.name] = marker
      }
      self.markerMap = markerMap
    } else {
      self.markerMap = nil
    }
  }

}

JSON文件加载:

Lottie读取json文件将动画映射存储到Animation对象中

/**
   Loads a Lottie animation from a JSON file in the supplied bundle.
   
   - Parameter name: The string name of the lottie animation with no file
   extension provided.
   - Parameter bundle: The bundle in which the animation is located.
   Defaults to the Main bundle.
   - Parameter imageProvider: An image provider for the animation's image data.
   If none is supplied Lottie will search in the supplied bundle for images.
   */
  convenience init(name: String,
                          bundle: Bundle = Bundle.main,
                          imageProvider: AnimationImageProvider? = nil,
                          animationCache: AnimationCacheProvider? = LRUAnimationCache.sharedCache) {
    let animation = Animation.named(name, bundle: bundle, subdirectory: nil, animationCache: animationCache)
    let provider = imageProvider ?? BundleImageProvider(bundle: bundle, searchPath: nil)
    self.init(animation: animation, imageProvider: provider)
  }

 // MARK: Animation (Loading)
  
  /**
   Loads an animation model from a bundle by its name. Returns `nil` if an animation is not found.
   
   - Parameter name: The name of the json file without the json extension. EG "StarAnimation"
   - Parameter bundle: The bundle in which the animation is located. Defaults to `Bundle.main`
   - Parameter subdirectory: A subdirectory in the bundle in which the animation is located. Optional.
   - Parameter animationCache: A cache for holding loaded animations. Optional.
   
   - Returns: Deserialized `Animation`. Optional.
   */
  static func named(_ name: String,
                           bundle: Bundle = Bundle.main,
                           subdirectory: String? = nil,
                           animationCache: AnimationCacheProvider? = nil) -> Animation? {
    /// Create a cache key for the animation.
    let cacheKey = bundle.bundlePath + (subdirectory ?? "") + "/" + name
    
    /// Check cache for animation
    if let animationCache = animationCache,
      let animation = animationCache.animation(forKey: cacheKey) {
      /// If found, return the animation.
      return animation
    }
    /// Make sure the bundle has a file at the path provided.
    guard let url = bundle.url(forResource: name, withExtension: "json", subdirectory: subdirectory) else {
      return nil
    }
    
    do {
      /// Decode animation.
      let json = try Data(contentsOf: url)
      let animation = try JSONDecoder().decode(Animation.self, from: json)
      animationCache?.setAnimation(animation, forKey: cacheKey)
      return animation
    } catch {
      /// Decoding error.
      return nil
    }
  }
JSON动画解析缓存:

Lottie对Animation采用LRU策略以文件路径+文件名为key进行缓存

public class LRUAnimationCache: AnimationCacheProvider {

  public init() { }
  
  /// Clears the Cache.
  public func clearCache() {
    cacheMap.removeAll()
    lruList.removeAll()
  }
  
  /// The global shared Cache.
  public static let sharedCache = LRUAnimationCache()
  
  /// The size of the cache.
  public var cacheSize: Int = 100
  
  public func animation(forKey: String) -> Animation? {
    guard let animation = cacheMap[forKey] else {
      return nil
    }
    if let index = lruList.firstIndex(of: forKey) {
      lruList.remove(at: index)
      lruList.append(forKey)
    }
    return animation
  }
  
  public func setAnimation(_ animation: Animation, forKey: String) {
    cacheMap[forKey] = animation
    lruList.append(forKey)
    if lruList.count > cacheSize {
      lruList.remove(at: 0)
    }
  }
  
  fileprivate var cacheMap: [String : Animation] = [:]
  fileprivate var lruList: [String] = []
  
}

四、Lottie 动画核心

Lottie 是以layer为核心,以CABasicAnimation的currentFrame进行动画,

1. json文件加载

将json文件解析成Animation对象并使用LRU策略进行内存缓存,设置AnimationImageProvider对象以便对json动画里的图片资源进行加载

2. 生成animationLayer和读取图片资源

移除之前的layer,通过AnimationContainer生成新的animationLayer;AnimationContainer解析Animation的layers添加到animationLayers里,并处理imageLayers并加载相关图片资源。最后将animationLayer添加到viewLayer里

 // MARK: - Private (Building Animation View)
  
  fileprivate func makeAnimationLayer() {
    
    /// Remove current animation if any
    removeCurrentAnimation()
    
    if let oldAnimation = self.animationLayer {
      oldAnimation.removeFromSuperlayer()
    }
    
    invalidateIntrinsicContentSize()
    
    guard let animation = animation else {
      return
    }
    
    let animationLayer = AnimationContainer(animation: animation, imageProvider: imageProvider)
    animationLayer.renderScale = self.screenScale
    viewLayer?.addSublayer(animationLayer)
    self.animationLayer = animationLayer
    reloadImages()
    animationLayer.setNeedsDisplay()
    setNeedsLayout()
    currentFrame = CGFloat(animation.startFrame)
  }
3. 开始动画

创建AnimationContext上下文,根据上下文生成以currentFrame为key的CABasicAnimation,将动画提交到animationLayer,执行animationLayer的display方法

 // MARK: - Public Functions
  
  /**
   Plays the animation from its current state to the end.
   
   - Parameter completion: An optional completion closure to be called when the animation completes playing.
   */
  public func play(completion: LottieCompletionBlock? = nil) {
    guard let animation = animation else {
      return
    }
    
    /// Build a context for the animation.
    let context = AnimationContext(playFrom: CGFloat(animation.startFrame),
                                   playTo: CGFloat(animation.endFrame),
                                   closure: completion)
    removeCurrentAnimation()
    addNewAnimationForContext(context)
  }
  
  /**
   Plays the animation from a progress (0-1) to a progress (0-1).
   
   - Parameter fromProgress: The start progress of the animation. If `nil` the animation will start at the current progress.
   - Parameter toProgress: The end progress of the animation.
   - Parameter toProgress: The loop behavior of the animation. If `nil` the view's `loopMode` property will be used.
   - Parameter completion: An optional completion closure to be called when the animation stops.
   */
  public func play(fromProgress: AnimationProgressTime? = nil,
                   toProgress: AnimationProgressTime,
                   loopMode: LottieLoopMode? = nil,
                   completion: LottieCompletionBlock? = nil) {
    guard let animation = animation else {
      return
    }
    
    removeCurrentAnimation()
    if let loopMode = loopMode {
      /// Set the loop mode, if one was supplied
      self.loopMode = loopMode
    }
    let context = AnimationContext(playFrom: animation.frameTime(forProgress: fromProgress ?? currentProgress),
                                   playTo: animation.frameTime(forProgress: toProgress),
                                   closure: completion)
    addNewAnimationForContext(context)
  }
  
  /**
   Plays the animation from a start frame to an end frame in the animation's framerate.
   
   - Parameter fromProgress: The start progress of the animation. If `nil` the animation will start at the current progress.
   - Parameter toProgress: The end progress of the animation.
   - Parameter toProgress: The loop behavior of the animation. If `nil` the view's `loopMode` property will be used.
   - Parameter completion: An optional completion closure to be called when the animation stops.
   */
  public func play(fromFrame: AnimationFrameTime? = nil,
                   toFrame: AnimationFrameTime,
                   loopMode: LottieLoopMode? = nil,
                   completion: LottieCompletionBlock? = nil) {
    removeCurrentAnimation()
    if let loopMode = loopMode {
      /// Set the loop mode, if one was supplied
      self.loopMode = loopMode
    }
    
    let context = AnimationContext(playFrom: fromFrame ?? currentProgress,
                                   playTo: toFrame,
                                   closure: completion)
    addNewAnimationForContext(context)
  }
  

五、Lottie 优势

六、Lottie 适用场景:

上一篇下一篇

猜你喜欢

热点阅读