Lottie-分享(一)

一旧巾、Lottie是什么?

Lottie 是一個可應(yīng)用于Andriod和iOS的動畫庫蜕劝,它通過bodymovin插件來解析Adobe After Effects 動畫并導(dǎo)出為json文件,通過手機端原生的方式或者通過React Native的方式渲染出矢量動畫轰异。

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

二岖沛、Lottie 使用

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

// JSONFileName 指的就是用 AE 導(dǎo)出的動畫 本地 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
    
    // 動畫執(zhí)行完成后的回調(diào)
    // Do Something
}

如果你使用到了跨bundle的JSON文件,或者需要從磁盤加載JSON文件搭独,可使用對應(yīng)的初始化方法:

/**
從本地支持的JSON文件加載Lottie動畫.
   
- Parameter name: JSON文件名.
- Parameter bundle: 動畫所在的包.
- Parameter imageProvider: 加載動畫需要的圖片資源(有些動畫需要圖片配合【可以是本地圖片資源婴削,也可以是網(wǎng)絡(luò)圖片資源,實現(xiàn)該協(xié)議返回對應(yīng)的CGImage】).
- Parameter animationCache: 緩存機制【需要自己實現(xiàn)緩存機制牙肝,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) { ... }

// 從網(wǎng)絡(luò)加載
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() 

/// 設(shè)置`play`調(diào)用的循環(huán)行為雹姊。 默認為“playOnce”
/// 定義動畫循環(huán)行為
public enum LottieLoopMode {
 /// 動畫播放一次然后停止。
  case playOnce
   /// 動畫將從頭到尾循環(huán)直到停止衡楞。
  case loop
 /// 動畫將向前播放吱雏,然后向后播放并循環(huán)直至停止。
  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)
}

// 循環(huán)模式
animationView.loopMode = .playOnce

/**
到后臺時AnimationView的行為瘾境。
默認為“暫推缧樱”,在到后臺暫停動畫迷守。 回調(diào)會以“false”調(diào)用完成犬绒。
*/
/// 到后臺時AnimationView的行為
public enum LottieBackgroundBehavior {
    /// 停止動畫并將其重置為當前播放時間的開頭。 調(diào)用完成回調(diào)兑凿。
    case stop
    /// 暫停動畫凯力,回調(diào)會以“false”調(diào)用完成。
    case pause
    /// 暫停動畫并在應(yīng)到前臺時重新啟動它急膀,在動畫完成時調(diào)用回調(diào)
    case pauseAndRestore
}
        
// 到后臺的行為模式
animationView.backgroundBehavior = .pause

/**
 播放動畫沮协,進度(0 ~ 1).
 
 - Parameter fromProgress: 動畫的開始進度。 如果是'nil`卓嫂,動畫將從當前進度開始慷暂。
 - Parameter toProgress: 動畫的結(jié)束進度。
 - Parameter toProgress: 動畫的循環(huán)行為晨雳。 如果是`nil`行瑞,將使用視圖的`loopMode`屬性。默認是 .playOnce
 - Parameter completion: 動畫停止時要調(diào)用的可選完成閉包餐禁。
 */
//        public func play(fromProgress: AnimationProgressTime? = nil,
//                         toProgress: AnimationProgressTime,
//                         loopMode: LottieLoopMode? = nil,
//                         completion: LottieCompletionBlock? = nil)
animationView.play(fromProgress: 0, toProgress: 1, loopMode: .playOnce) { (isFinished) in
    // 播放完成后的回調(diào)閉包
}
// 設(shè)置當前進度
animationView.currentProgress = 0.5

/**
 使用幀的方式播放動畫
 
 - Parameter fromProgress: 動畫的開始進度血久。 如果是'nil`,動畫將從當前進度開始帮非。
 - Parameter toProgress: 動畫的結(jié)束進度
 - Parameter toProgress: 動畫的循環(huán)行為氧吐。 如果是`nil`,將使用視圖的`loopMode`屬性末盔。
 - Parameter completion: 動畫停止時要調(diào)用的可選完成閉包筑舅。
 */
//        public func play(fromFrame: AnimationFrameTime? = nil,
//                         toFrame: AnimationFrameTime,
//                         loopMode: LottieLoopMode? = nil,
//                         completion: LottieCompletionBlock? = nil)
animationView.play(fromFrame: 50, toFrame: 80, loopMode: .loop) { (isFinished) in
    // 播放完成后的回調(diào)閉包
}
// 設(shè)置當前幀
animationView.currentFrame = 65

三、Lottie如何加載JSON文件

JSON文件格式化后類型如下:
json文件格式.png
其中的部分參數(shù)定義為:

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策略進行內(nèi)存緩存游盲,設(shè)置AnimationImageProvider對象以便對json動畫里的圖片資源進行加載

2. 生成animationLayer和讀取圖片資源

移除之前的layer,通過AnimationContainer生成新的animationLayer误墓;AnimationContainer解析Animation的layers添加到animationLayers里蛮粮,并處理imageLayers并加載相關(guān)圖片資源。最后將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. 開始動畫

創(chuàng)建AnimationContext上下文谜慌,根據(jù)上下文生成以currentFrame為key的CABasicAnimation然想,將動畫提交到animationLayer,執(zhí)行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 優(yōu)勢

  • 開發(fā)成本低又沾。設(shè)計師導(dǎo)出 json 文件后,扔給開發(fā)同學(xué)即可熙卡,可以放在本地杖刷,也支持放在服務(wù)器。原本要1天甚至更久的動畫實現(xiàn)驳癌,現(xiàn)在只要不到一小時甚至更少時間了滑燃。

  • 動畫的實現(xiàn)成功率高了。設(shè)計師的成果可以最大程度得到實現(xiàn)颓鲜,試錯成本也低了表窘。

  • 支持服務(wù)端 URL 方式創(chuàng)建。所以可以通過服務(wù)端配置 json 文件甜滨,隨時替換客戶端的動畫乐严,不用通過發(fā)版本就可以做到了。比如 app 啟動動畫可以根據(jù)活動需要進行變換了衣摩。

  • 性能昂验。可以替代原來需要使用幀圖完成的動畫艾扮。節(jié)省了客戶端的空間和加載的內(nèi)存既琴。對硬件性能好一些。

  • 跨平臺泡嘴。iOS甫恩、安卓平臺可以使用一套文件。省時省力酌予,動畫一致磺箕。不用設(shè)計師跑去兩邊去跟著微調(diào)確認了。

六抛虫、Lottie 適用場景:

  • 首次啟動引導(dǎo)頁(這個要做比較好的效果松靡,也比較復(fù)雜)

  • 啟動(splash)動畫:典型場景是APP logo動畫的播放

  • 上下拉刷新動畫:所有APP都必備的功能,利用 Lottie 可以做的更加簡單酷炫了

  • 加載(loading)動畫:典型場景是網(wǎng)絡(luò)請求的loading動畫

  • 提示(tips)動畫:典型場景是空白頁的提示

  • 按鈕(button)動畫:典型場景如switch按鈕莱褒、編輯按鈕等按鈕的切換過 渡動畫

  • 視圖轉(zhuǎn)場動畫(目前不支持push和pop)[Swift不支持,也可能是我沒有找到對應(yīng)的API @山竹]

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涎劈,一起剝皮案震驚了整個濱河市广凸,隨后出現(xiàn)的幾起案子阅茶,更是在濱河造成了極大的恐慌,老刑警劉巖谅海,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脸哀,死亡現(xiàn)場離奇詭異,居然都是意外死亡扭吁,警方通過查閱死者的電腦和手機撞蜂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來侥袜,“玉大人蝌诡,你說我怎么就攤上這事》惆桑” “怎么了浦旱?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長九杂。 經(jīng)常有香客問我颁湖,道長,這世上最難降的妖魔是什么例隆? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任甥捺,我火速辦了婚禮,結(jié)果婚禮上镀层,老公的妹妹穿的比我還像新娘镰禾。我一直安慰自己,他們只是感情好鹿响,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布羡微。 她就那樣靜靜地躺著,像睡著了一般惶我。 火紅的嫁衣襯著肌膚如雪妈倔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天绸贡,我揣著相機與錄音盯蝴,去河邊找鬼。 笑死听怕,一個胖子當著我的面吹牛捧挺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播尿瞭,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼闽烙,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起黑竞,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤捕发,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后很魂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扎酷,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年遏匆,在試婚紗的時候發(fā)現(xiàn)自己被綠了法挨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡幅聘,死狀恐怖凡纳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情喊暖,我是刑警寧澤惫企,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站陵叽,受9級特大地震影響狞尔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜巩掺,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一偏序、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧胖替,春花似錦研儒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至燃箭,卻和暖如春冲呢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背招狸。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工敬拓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人裙戏。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓乘凸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親累榜。 傳聞我的和親對象是個殘疾皇子营勤,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容