美文网首页
Lottie-分享(一)

Lottie-分享(一)

作者: 恩说吧 | 来源:发表于2021-12-30 14:10 被阅读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 优势

    • 开发成本低。设计师导出 json 文件后,扔给开发同学即可,可以放在本地,也支持放在服务器。原本要1天甚至更久的动画实现,现在只要不到一小时甚至更少时间了。

    • 动画的实现成功率高了。设计师的成果可以最大程度得到实现,试错成本也低了。

    • 支持服务端 URL 方式创建。所以可以通过服务端配置 json 文件,随时替换客户端的动画,不用通过发版本就可以做到了。比如 app 启动动画可以根据活动需要进行变换了。

    • 性能。可以替代原来需要使用帧图完成的动画。节省了客户端的空间和加载的内存。对硬件性能好一些。

    • 跨平台。iOS、安卓平台可以使用一套文件。省时省力,动画一致。不用设计师跑去两边去跟着微调确认了。

    六、Lottie 适用场景:

    • 首次启动引导页(这个要做比较好的效果,也比较复杂)

    • 启动(splash)动画:典型场景是APP logo动画的播放

    • 上下拉刷新动画:所有APP都必备的功能,利用 Lottie 可以做的更加简单酷炫了

    • 加载(loading)动画:典型场景是网络请求的loading动画

    • 提示(tips)动画:典型场景是空白页的提示

    • 按钮(button)动画:典型场景如switch按钮、编辑按钮等按钮的切换过 渡动画

    • 视图转场动画(目前不支持push和pop)[Swift不支持,也可能是我没有找到对应的API @山竹]

    相关文章

      网友评论

          本文标题:Lottie-分享(一)

          本文链接:https://www.haomeiwen.com/subject/ifbvqrtx.html