Lottie-分享(一)
一、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 @山竹]