Lottie-Swift

Lottie的框架地址
官方文档地址

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 {
    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
    }
    在layerModel中存在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

  1. 基本播放 swift AnimationView.play(completion: LottieCompletionBlock?) 从当前状态播放至结束时间,当动画停止时,调用回调
  2. 进度时间播放 swift AnimationView.play(fromProgress: AnimationProgressTime?, toProgress: AnimationProgressTime, loopMode: LottieLoopMode?, completion: LottieCompletionBlock?)
  3. 帧时间播放

    AnimationView.play(fromFrame: AnimationFrameTime?, toFrame: <#T##AnimationFrameTime#>, loopMode: <#T##LottieLoopMode?#>, completion: <#T##LottieCompletionBlock?##LottieCompletionBlock?##(Bool) -> Void#>)
    

    public typealias AnimationFrameTime = CGFloat
    public typealias AnimationProgressTime = CGFloat

  4. 从标记位置Markers播放

    AnimationView.play(fromMarker: String?, toMarker: String, loopMode: LottieLoopMode?, completion: LottieCompletionBlock?)
    

    Markers是编码在Lottie的jason文件中,如果找不到将退出播放

  5. 停止播放

    AnimationView.stop()
    

    停止播放并将其置为开始帧 aniamtion的completion将以false结束

  6. 暂停播放

    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

可使用通配符 *(搜索但层级深度)或者**(搜索任何层级深度)

Animation Control