一旧巾、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文件格式化后類型如下:
其中的部分參數(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 @山竹]