Lottie-Swift
Lottie中的两大顶级模型 分别为Animation和AnimationView
AnimationModel
Animation是从Json中反序列化出来的,持有所有支持Lottie动画的数据
其即为Lottie的json文件内容解析的结果,
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"
}
//animation即为从json中反序列化
let json = try Data(contentsOf: url)
let animation = try JSONDecoder().decode(Animation.self, from: json)
LayerModel
LayerModel是动画中存在的所有图层的model
// Json反序列化之后生成Animation模型中存在动画的所有Layer => let layers: [LayerModel]
private enum CodingKeys : String, CodingKey {
case name = "nm"
case index = "ind"
case type = "ty"
case coordinateSpace = "ddd"
case inFrame = "ip"
case outFrame = "op"
case startTime = "st"
case transform = "ks"
case parent = "parent"
case blendMode = "bm"
case masks = "masksProperties"
case timeStretch = "sr"
case matte = "tt"
case hidden = "hd"
}
而layer又有很多类型
public enum LayerType: Int, Codable {
case precomp
case solid
case image
case null
case shape
case text
}
其中每个类型 都是继承自LayerModel的
例如
- ImageLayerModel中存在属性
refId
即为图片id 而在图片asset中存在PrecompAsset
内部包含了一组其他的LayerModel - ShapeLayerModel 中存在一组shapes 即
ShapeItem
,根据不同类型 ShapeItem拥有不同的子类,表示其行为swift enum ShapeType: String, Codable {
在layerModel中存在
case ellipse = "el"
case fill = "fl"
case gradientFill = "gf"
case group = "gr"
case gradientStroke = "gs"
case merge = "mm"
case rectangle = "rc"
case repeater = "rp"
case round = "rd"
case shape = "sh"
case star = "sr"
case stroke = "st"
case trim = "tm"
case transform = "tr"
case unknown
}ks
对应的let transform: Transform
即为对应layer的要做的所有动画效果
总结:
在lottie 的json反序列化的Layer数组即为所有的最外层父视图Layer LayerModel
每个Layer存在
* ks 即为当前layer要做的动画transform
* 其子Layer 以及子Layer要做的动画
加载Animation
// 从bunlde中加载
static func named(_ name: String,
bundle: Bundle = Bundle.main,
subdirectory: String? = nil,
animationCache: AnimationCacheProvider? = nil) -> Animation?
// 从文件中加载
static func filepath(_ filepath: String,
animationCache: AnimationCacheProvider? = nil) -> Animation?
// 从Url下载
static func loadedFrom(url: URL,
closure: @escaping Animation.DownloadClosure,
animationCache: AnimationCacheProvider?)
AnimationCacheProvider
AnimationCacheProvider
为一个协议,定义了缓存Animation和从缓存总获取的方法
AnimationCacheProvider
即为AnimationView的缓存,
其其实就是一个key为路径、value为Animation的字典 之后会详细解释
例如在从Bundle中加载时
let cacheKey = bundle.bundlePath + (subdirectory ?? "") + "/" + name
if let animationCache = animationCache,
let animation = animationCache.animation(forKey: cacheKey) {
// 如果在缓存中存在该动画直接返回
return animation
}
...
animationCache?.setAnimation(animation, forKey: cacheKey)
AnimationView
AnimationView
是用于展示动画内容的视图,继承自UIView
,可以用来加载、播放甚至改变动画
AnimationView构造器
AnimationView的初始化其实就是主要对以下几项赋值
- Animation: 动画
- imageProvider: 动画需要的图片资源
- textProvider: 文字
public init(animation: Animation?, imageProvider: AnimationImageProvider? = nil, textProvider: AnimationTextProvider = DefaultTextProvider()) {
self.animation = animation
// 可以看到默认为MainBundle
self.imageProvider = imageProvider ?? BundleImageProvider(bundle: Bundle.main, searchPath: nil)
self.textProvider = textProvider
super.init(frame: .zero)
commonInit()
makeAnimationLayer()
if let animation = animation {
frame = animation.bounds
}
}
因此Animation的初始化类似,其便利构造器,分别为Bundle、FilePath、Url
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)
}
convenience init(filePath: String,
imageProvider: AnimationImageProvider? = nil,
animationCache: AnimationCacheProvider? = LRUAnimationCache.sharedCache) {
let animation = Animation.filepath(filePath, animationCache: animationCache)
// 删除路径的最后组件 作为imageProvide
let provider = imageProvider ?? FilepathImageProvider(filepath: URL(fileURLWithPath: filePath).deletingLastPathComponent().path)
self.init(animation: animation, imageProvider: provider)
}
convenience init(url: URL,
imageProvider: AnimationImageProvider? = nil,
closure: @escaping AnimationView.DownloadClosure,
animationCache: AnimationCacheProvider? = LRUAnimationCache.sharedCache) {
self.init(animation: nil, imageProvider: imageProvider)
Animation.loadedFrom(url: url, ...... }
在将Animation赋值给AniamtionView时,会初始化AnimationView中获取这个动画中所有的Layers
let animationLayer = AnimationContainer(animation: animation, imageProvider: imageProvider, textProvider: textProvider)
// 属性animationLayer
var animationLayer: AnimationContainer? = nil
在AnimationContainer
中 初始化
init(animation: Animation, imageProvider: AnimationImageProvider, textProvider: AnimationTextProvider) {
// 获取所有Asset中的ImageLayer
self.layerImageProvider = LayerImageProvider(imageProvider: imageProvider, assets: animation.assetLibrary?.imageAssets)
// 获取所有的TextLayer
self.layerTextProvider = LayerTextProvider(textProvider: textProvider)
// 通过将Animation中的最外层Layer和其内部所有的Layer进行串联 获取所有的Layers
let layers = animation.layers.initializeCompositionLayers(assetLibrary: animation.assetLibrary, layerImageProvider: layerImageProvider, textProvider: textProvider, frameRate: CGFloat(animation.framerate))
}
animation.layers.initializeCompositionLayers
还会降LayerModel模型 根据转换为可具体做动画的CompositionLayer
子类型
- ShapeCompositionLayer
- NullCompositionLayer
- SolidCompositionLayer
- ImageCompositionLayer
- TextCompositionLayer
其和各种Layermodel类型是分别对应的
Supplying Images
AnimationView
使用AnimationImageProvider
检索其动画中需要的图片。
在AniamtionView初始化时或者之后设置其imageProvider
属性来提供image provider
通过调用AnimationView.reloadImages()
来强制AnimationView刷新其images
播放动画
Time
Lottie中描述时间有以下几种方式
- Frame Time: 帧时间 即当前播放到哪一帧,时间和具体帧速率有关
- Progress Time: 进度时间 从0到1
Time: 实际的动画时间 单位为seconds
- 基本播放
swift AnimationView.play(completion: LottieCompletionBlock?)
从当前状态播放至结束时间,当动画停止时,调用回调 - 进度时间播放
swift AnimationView.play(fromProgress: AnimationProgressTime?, toProgress: AnimationProgressTime, loopMode: LottieLoopMode?, completion: LottieCompletionBlock?)
帧时间播放
AnimationView.play(fromFrame: AnimationFrameTime?, toFrame: <#T##AnimationFrameTime#>, loopMode: <#T##LottieLoopMode?#>, completion: <#T##LottieCompletionBlock?##LottieCompletionBlock?##(Bool) -> Void#>)
public typealias AnimationFrameTime = CGFloat
public typealias AnimationProgressTime = CGFloat从标记位置Markers播放
AnimationView.play(fromMarker: String?, toMarker: String, loopMode: LottieLoopMode?, completion: LottieCompletionBlock?)
Markers
是编码在Lottie的jason文件中,如果找不到将退出播放停止播放
AnimationView.stop()
停止播放并将其置为开始帧 aniamtion的completion将以false结束
暂停播放
AnimationView.pause()
暂停动画的当前状态 completion的block 将以false结束
所有的动画,每个layer的 Transform关键字
enum CodingKeys : String, CodingKey {
case anchorPoint = "a"
case position = "p"
case positionX = "px"
case positionY = "py"
case scale = "s"
case rotation = "r"
case rotationZ = "rz"
case opacity = "o"
}
AnimationView的播放动画 就是在其
var animationLayer: AnimationContainer?
layer上添加了每帧播放的CABasicAnimation
此处具体的播放逻辑还需要学习参考源码
// 在AnimationView类中
let layerAnimation = CABasicAnimation(keyPath: "currentFrame")
layerAnimation.fromValue = playFrom
layerAnimation.toValue = playTo
layerAnimation.speed = Float(animationSpeed)
layerAnimation.duration = TimeInterval(duration)
layerAnimation.fillMode = CAMediaTimingFillMode.both
let animationlayer = self.animationLayer
animationlayer.add(layerAnimation, forKey: activeAnimationName)
在AnimationContainer
中
override public class func needsDisplay(forKey key: String) -> Bool {
if key == "currentFrame" {
return true
}
return super.needsDisplay(forKey: key)
}
override public func action(forKey event: String) -> CAAction? {
if event == "currentFrame" {
let animation = CABasicAnimation(keyPath: event)
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.fromValue = self.presentation()?.currentFrame
return animation
}
return super.action(forKey: event)
}
public override func display() {
guard Thread.isMainThread else { return }
var newFrame: CGFloat = self.presentation()?.currentFrame ?? self.currentFrame
if respectAnimationFrameRate {
newFrame = floor(newFrame)
}
animationLayers.forEach( { $0.displayWithFrame(frame: newFrame, forceUpdates: false) })
}
CAAniamtion动画再学习
通过重写以上方法,进行动画,即将AnimationContainer
中的animationLayers.forEach
遍历,执行其displayWithFrame:
方法展示其该帧时间的“形状”
Markers
Marker
即为标记 通过key来描述时间点。其编码在lottie的json文件中,提供标记点给开发使用,而不需要具体了解或者跟踪对应的动画帧
读取标记
/// Animation View Methods
AnimationView.progressTime(forMarker named: String) -> AnimationProgressTime?
AnimationView.frameTime(forMarker named: String) -> AnimationFrameTime?
/// Animation Model Methods
Animation.progressTime(forMarker named: String) -> AnimationProgressTime?
Animation.frameTime(forMarker named: String) -> AnimationFrameTime?
当找不到对应标记时 返回nil
我们在从json中反序列号Animation时,就有markers字段其对应的模型为Markes
类,内部包含属性为name、AnimationFrameTime
分别对应key和帧动画时间
marker其实就是提供了一种方便的获取指定帧时间的方案
调用AnimationView的marker方法 其内部实现就是调用其Animation的Marker方法
Animation的动态属性
通过在Animation KeyPath
设置ValueProvider
,会使动画更新其内容 读取新的Value
,来更改几乎所有的动画属性
也可以使用Animation KeyPath
来读取动画属性
首先我们可以使用log来查看所有的keypath
AnimationView.logHierarchyKeypaths()
// 源码 在AnimationContainer中
func logHierarchyKeypaths() {
print("Lottie: Logging Animation Keypaths")
animationLayers.forEach({ $0.logKeypaths(for: nil) })
}
在AnimationView的layContainer中,拥有所有动画的layer
//AnimationContainer
var animationLayers: ContiguousArray<CompositionLayer>
//CompositionLayer
class CompositionLayer: CALayer, KeypathSearchable {
}
在animationLayers
属性中即为动画的所有layers,在每个layer中拥有其子keypath
,即为动画的关键路径
// CompositionLayer
final var childKeypaths: [KeypathSearchable]
当知道指定的keypath
,就可以设置或者获取keypath的当前value值
animationView.setValueProvider(ColorValueProvider(Color.init(r: 1, g: 0, b: 0, a: 1)), keypath: AnimationKeypath(keypath: "Eye Right 2.Ellipse 1.Fill 1.Color"))
AnimationView.getValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any?
setValueProvider:
其实现
// AnimationContainer
func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) {
for layer in animationLayers {
if let foundProperties = layer.nodeProperties(for: keypath) {
for property in foundProperties {
// 找到对应路径节点 设置值
property.setProvider(provider: valueProvider)
}
// 强制Layer 当前帧刷新
layer.displayWithFrame(frame: presentation()?.currentFrame ?? currentFrame, forceUpdates: true)
}
}
}
实现原理:
func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) {
for layer in animationLayers {
if let foundProperties = layer.nodeProperties(for: keypath) {
for property in foundProperties {
property.setProvider(provider: valueProvider)
}
layer.displayWithFrame(frame: presentation()?.currentFrame ?? currentFrame, forceUpdates: true)
}
}
}
对Animation中Node的学习
AniamtionView中添加自定义视图
自定义视图可以被添加到AnimationViews
中
这些视图将和动画一起动画
let subView = AnimationSubview()
animationView.addSubview(subView, forLayerAt: AnimationKeypath(keypath: "Eye Right 1"))
搜索最接近keypath的子图层,将子视图添加到该层。则将会一起进行动画
因为添加的为layer 无法添加点击事件或者手势
启用或禁用Animation的node节点
通过keyPath找到Node 启用或者禁用,启用的节点会影节点树 而禁用的节点将会从渲染树移除
// 禁用图层中的填充图层
animationView.setNodeIsEnabled(isEnabled: false, keypath: AnimationKeypath(keypath: "Eye Right 1.Ellipse 1.Fill 1"))
Converting CGRect and CGPoint to Layers
Image Provide
AnimationImageProvider
是一个用来给AnimationView
提供图片的协议
当AnimationView需要引用图像,通过ImageProvider来将图像提供给AnimationView
在Lotties框架中预置了从Bundle或者filePath加载的ImageProvide,当然也可以实现自定义的Provide,例如从URL来加载或者缓存图片
其实ImageProvide就是实现了func imageForAsset(asset: ImageAsset) -> CGImage
方法,可以根据ImageAsset
来找到对应的Image
而ImageAsset
来源
func reloadImages() {
for imageLayer in imageLayers {
if let asset = imageAssets[imageLayer.imageReferenceID] {
imageLayer.image = imageProvider.imageForAsset(asset: asset)
}
}
}
即为遍历Lottie的Json中标记的imageLayers
,查找该layer中使用的image的ID即imageReferenceID
,在所有的Json的所有imageAssets
找到该asset
然后我们imageProvide作用就是使可以通过asset
找到可以使用的Image
Lottie的Json文件内容
//Lottie的JSON中的图片资源, `imageAssets`根据其name即`img_0.png`和目录images/寻找符合条件的图片资源
"assets": [{
"id": "image_0",
"w": 408,
"h": 546,
"u": "images/",
"p": "img_0.png",
"e": 0
}]
// 在Lottie动画中使用的layer 其中`refId`即为使用的图片id`imageReferenceID`
"layers": [{
"ddd": 0,
"ind": 1,
"ty": 2,
"nm": "组 342/星光.psb",
"cl": "psb",
"refId": "image_0",
"sr": 1,
....
}]
BundleImageProvide
public func imageForAsset(asset: ImageAsset) -> CGImage? {
// 当name中有data开头 说明是通过base64字符串编码的图片资源 直接使用
if asset.name.hasPrefix("data:"),
let url = URL(string: asset.name),
let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
return image.cgImage
}
//搜索bundle指定目录下的
if let searchPath = searchPath {
URL(fileURLWithPath: searchPath).appendPathComponent(asset.directory)
let path = bundle.path(forResource: asset.name, ofType: nil, inDirectory: directoryPath.path)
let path = bundle.path(forResource: asset.name, ofType: nil, inDirectory: searchPath)
imagePath = bundle.path(forResource: asset.name, ofType: nil)
} else {
path = bundle.path(forResource: asset.name, ofType: nil, inDirectory: asset.directory)
imagePath = bundle.path(forResource: asset.name, ofType: nil)
}
let image = UIImage(contentsOfFile: foundPath)
return image.cgImage
}
针对一个bundle只有一个ImageProvide,否则设置给AnimationView提供的第二个imageProvide是无效的
默认会搜索main bundle下的图片资源
FilepathImageProvider
public func imageForAsset(asset: ImageAsset) -> CGImage? {
let directPath = filepath.appendingPathComponent(asset.name).path
if FileManager.default.fileExists(atPath: directPath) {
return UIImage(contentsOfFile: directPath)?.cgImage
}
let pathWithDirectory = filepath.appendingPathComponent(asset.directory).appendingPathComponent(asset.name).path
if FileManager.default.fileExists(atPath: pathWithDirectory) {
return UIImage(contentsOfFile: pathWithDirectory)?.cgImage
}
}
Animation Cache
AnimationCacheProvider是描述动画缓存的协议。加载动画模型时使用动画缓存。多次加载动画时,使用动画缓存可以提高性能。
public protocol AnimationCacheProvider {
func animation(forKey: String) -> Animation?
func setAnimation(_ animation: Animation, forKey: String)
func clearCache()
}
Lottie带有内建的LRUAnimationCache。
LRUAnimationCache
cacheSize
sharedCache
LRUAnimationCache.sharedCache.clearCache()
Animation Keypaths
可使用通配符 *(搜索但层级深度)
或者**(搜索任何层级深度)