# lottie-web 动画解析
# 前言
Lottie 是一个复杂帧动画的解决方案,它提供了一套从设计师使用 AE(Adobe After Effects)到各端开发者实现动画的工具流。在设计师通过 AE 完成动画后,可以使用 AE 导出一份 JSON 格式的动画数据,然后开发同学可以通过 Lottie 将生成的 JSON 数据渲染成动画。
# 使用介绍
# 1. 基本用法
加载 Lottie 库结合 JSON 文件和下面几行代码就可以实现一个 Lottie 动画
import lottie from 'lottie-web'
const lot = lottie.loadAnimation({
container: this.$refs.lottieBox,
renderer: 'svg',
loop: true,
autoplay: true, // 是否自动播放
path: 'data.json' // json地址
// animationData: ''
})
注:json 动画数据,与 path 互斥,建议使用 path,因为 animationData 会将数据打包,使得 js bundle 过大
# 2. 常用方法
lot.play():播放,从当前帧开始播放
lot.stop():停止,并回到第 0 帧
lot.pause():暂停,并保持当前帧
lot.goToAndStop(value, isFrame):跳到某个时刻/帧并停止
isFrame(可省略,默认false:毫秒;true:帧)指明value的单位是毫秒还是帧
lot.goToAndPlay(value, isFrame):跳到某个时刻/帧并播放
lot.goToAndStop(30, true) // 跳转到第30帧并停止
lot.goToAndPlay(300) // 跳转到第300毫秒并播放
lot.playSegments(arr, forceFlag):以帧为单位,播放指定片段
arr可以包含两个数字或者两个数字组成的数组,forceFlag表示是否立即强制播放该片段
lot.playSegments([10,20], false) // 播放完之前的片段,播放10-20帧
lot.playSegments([[0,5],[10,18]], true) // 直接播放0-5帧和10-18帧
lot.setSpeed(speed):设置播放速度,speed 为 1 表示正常速度
lot.setDirection(direction): 设置播放方向,1 表示正向播放,-1 表示反向播放
lot.destroy(): 删除该动画,移除相应的元素标签等。
# 实现过程
JSON 文件数据格式在此不做解读,接下来结合 Demo (opens new window) 的 Lottie 部分源码阅读展示,了解如何把 JSON 数据动起来的。
# 1. 初始渲染容器

AnimationManager.js (opens new window)
function setupAnimation(animItem, element) {
// 监听事件
animItem.addEventListener('destroy', removeElement)
animItem.addEventListener('_active', addPlayingCount)
animItem.addEventListener('_idle', subtractPlayingCount)
// 注册动画
registeredAnimations.push({ elem: element, animation: animItem })
len += 1
}
function loadAnimation(params) {
// 生成当前动画实例,AnimationItem方法来源AnimationItem.js
var animItem = new AnimationItem()
// 注册动画
setupAnimation(animItem, null)
// 初始化动画实例参数
animItem.setParams(params)
return animItem
}
AnimationItem 这个类是 Lottie 动画的基类,loadAnimation 方法会先生成一个 AnimationItem 实例并返回,开发者使用的 配置参数和方法 都是来自于这个类。
生成 animItem 实例后,调用 setupAnimation 方法。这个方法首先监听了 destroy、_active、_idle 三个事件等待被触发。由于可以多个动画并行,因此定义了全局的变量 len、registeredAnimations 等,用于判断和缓存已注册的动画实例。
接下来调用 animItem 实例的 setParams 方法初始化动画参数,除了初始化 loop 、 autoplay 等参数外,最重要的是选择渲染器,如下:
AnimationItem.prototype.setParams = function (params) {
// 渲染容器
if (params.wrapper || params.container) {
this.wrapper = params.wrapper || params.container
}
// 默认是svg格式
var animType = 'svg'
if (params.animType) {
animType = params.animType
} else if (params.renderer) {
animType = params.renderer
}
// 根据配置选择渲染器,如果不存在走html类型
switch (animType) {
case 'canvas':
this.renderer = new CanvasRenderer(this, params.rendererSettings)
break
case 'svg':
this.renderer = new SVGRenderer(this, params.rendererSettings)
break
default:
this.renderer = new HybridRenderer(this, params.rendererSettings)
break
}
this.imagePreloader.setCacheType(animType, this.renderer.globalData.defs)
this.renderer.setProjectInterface(this.projectInterface)
this.animType = animType
if (
params.loop === '' ||
params.loop === null ||
params.loop === undefined ||
params.loop === true
) {
this.loop = true
} else if (params.loop === false) {
this.loop = false
} else {
this.loop = parseInt(params.loop, 10)
}
// 默认自动播放
this.autoplay = 'autoplay' in params ? params.autoplay : true
// 动画名称
this.name = params.name ? params.name : ''
this.autoloadSegments = Object.prototype.hasOwnProperty.call(params, 'autoloadSegments')
? params.autoloadSegments
: true
this.assetsPath = params.assetsPath
this.initialSegment = params.initialSegment
if (params.audioFactory) {
this.audioController.setAudioFactory(params.audioFactory)
}
// 渲染数据初始化
if (params.animationData) {
this.setupAnimation(params.animationData)
} else if (params.path) {
if (params.path.lastIndexOf('\\') !== -1) {
this.path = params.path.substr(0, params.path.lastIndexOf('\\') + 1)
} else {
this.path = params.path.substr(0, params.path.lastIndexOf('/') + 1)
}
this.fileName = params.path.substr(params.path.lastIndexOf('/') + 1)
this.fileName = this.fileName.substr(0, this.fileName.lastIndexOf('.json'))
// 源码在 player/js/utils/dataManager.js
// 创建一个Worker子线程去加载json数据
dataManager.loadAnimation(params.path, this.configAnimation, this.onSetupError)
}
}
Lottie 提供了 SVG、Canvas 和 HTML 三种渲染模式,默认 SVG 模式。
SVG 渲染器支持的特性最多,也是使用最多的渲染方式。并且 SVG 是可伸缩的,任何分辨率下不会失真。
Canvas 渲染器就是根据动画的数据将每一帧的对象不断重绘出来。
HTML 渲染器受限于其功能,支持的特性最少,只能做一些很简单的图形或者文字,也不支持滤镜效果。
每个渲染器均有各自的实现,复杂度也各有不同,但是动画越复杂,其对性能的消耗也就越高,这些要看实际的状况再去判断。渲染器源码在 player/js/renderers/ 文件夹下。
# 2. 初始化动画属性,加载静态资源
AnimationItem.prototype.configAnimation = function (animData) {
if (!this.renderer) {
return
}
try {
this.animationData = animData
// 总帧数
if (this.initialSegment) {
this.totalFrames = Math.floor(this.initialSegment[1] - this.initialSegment[0])
this.firstFrame = Math.round(this.initialSegment[0])
} else {
this.totalFrames = Math.floor(this.animationData.op - this.animationData.ip)
this.firstFrame = Math.round(this.animationData.ip)
}
// 渲染器初始化参数
this.renderer.configAnimation(animData)
if (!animData.assets) {
animData.assets = []
}
this.assets = this.animationData.assets
// 帧率
this.frameRate = this.animationData.fr
this.frameMult = this.animationData.fr / 1000
this.renderer.searchExtraCompositions(animData.assets)
this.markers = markerParser(animData.markers || [])
this.trigger('config_ready')
// 加载静态资源
this.preloadImages()
this.loadSegments()
this.updaFrameModifier()
// 等待静态资源加载完毕
this.waitForFontsLoaded()
// 暂停声音
if (this.isPaused) {
this.audioController.pause()
}
} catch (error) {
this.triggerConfigError(error)
}
}
在这个方法中将会初始化更多动画对象的属性,比如总帧数 totalFrames 、帧率 frameMult 等。然后加载一些其他资源,比如图像、字体等。如下图所示:

同时在 waitForFontsLoaded 方法中等待静态资源加载完毕,加载完毕后便会调用 SVG 渲染器的 initItems 方法绘制动画图层,也就是将动画绘制出来。
AnimationItem.prototype.waitForFontsLoaded = function () {
if (!this.renderer) {
return
}
// 检查加载完毕
if (this.renderer.globalData.fontManager.isLoaded) {
this.checkLoaded()
} else {
setTimeout(this.waitForFontsLoaded.bind(this), 20)
}
}
AnimationItem.prototype.checkLoaded = function () {
if (
!this.isLoaded &&
this.renderer.globalData.fontManager.isLoaded &&
(this.imagePreloader.loadedImages() || this.renderer.rendererType !== 'canvas') &&
this.imagePreloader.loadedFootages()
) {
this.isLoaded = true
if (expressionsPlugin) {
expressionsPlugin.initExpressions(this)
}
// 初始化所有元素
this.renderer.initItems()
setTimeout(
function () {
this.trigger('DOMLoaded')
}.bind(this),
0
)
// 渲染第一帧
this.gotoFrame()
// 自动播放
if (this.autoplay) {
this.play()
}
}
}
在 checkLoaded 方法中可以看到,通过 initItems 初始化所有元素后,便通过 gotoFrame 渲染第一帧,如果开发者配置了 autoplay 为 true,则会直接调用 play 方法播放。接下来先看 initItems 实现细节。
# 3. 绘制动画初始图层
initItems 方法主要是调用 buildAllItems 创建所有图层。buildItem 方法又会调用 createItem 确定具体图层类型,下面看下 createItem 源码 player/js/renderers/BaseRenderer.js 实现:

在制作动画时,设计师操作的图层元素有很多种,比如图片、形状、文字等等。所以 layers 中每个图层会有一个字段 ty 来区分。结合 createItem 方法来看,一共有以下 9 中类型。
BaseRenderer.prototype.createItem = function(layer) {
// 根据图层类型,创建相应的 svg 元素类的实例
switch (layer.ty) {
case 2:
return this.createImage(layer); // 图片
case 0:
return this.createComp(layer); // 合成
case 1:
return this.createSolid(layer); // 固态
case 3:
return this.createNull(layer); // 空元素
case 4:
return this.createShape(layer); // 形状
case 5:
return this.createText(layer); // 文字
case 6:
return this.createAudio(layer); // 音频
case 13:
return this.createCamera(layer); // 摄像机
case 15:
return this.createFootage(layer); // 镜头
default:
return this.createNull(layer);
};
如上所知,图层类型的渲染逻辑,如 Image、Text、Audio 等等,每一种元素的渲染逻辑都实现在源码 player/js/elements/ 文件夹下,具体实现逻辑这里就不进行展开了,感兴趣的同学自行查看。
# 4. 动画播放
图层绘制完毕后,就来看看动画播放。Lottie 动画播放主要是使用 AnimationItem 实例的 play 方法。如果开发者配置了 autoplay 为 true,则会在所有初始化工作准备完毕后,直接调用 play 方法播放。否则由开发者主动调用 play 方法播放。
接下来从 play 方法了解一下整个播放流程的细节
AnimationItem.prototype.play = function (name) {
if (name && this.name !== name) {
return
}
if (this.isPaused === true) {
this.isPaused = false
this.audioController.resume()
if (this._idle) {
this._idle = false
this.trigger('_active')
}
}
}
play 方法主要是触发了 _active 事件,这个 _active 事件便是初始化时注册的,该事件调用 addPlayingCount 方法,addPlayingCount 里再调用 activate 方法,如下:
function activate() {
if (!_isFrozen && playingAnimationsNum) {
if (_stopped) {
// 触发第一帧渲染
window.requestAnimationFrame(first)
_stopped = false
}
}
}
触发后通过调用 requestAnimationFrame 方法,不断的调用 resume 方法来控制动画。
function first(nowTime) {
initTime = nowTime
// requestAnimationFrame 每次都进行计算修改 DOM
window.requestAnimationFrame(resume)
}
requestAnimationFrame 在正常情况下能达到 60 fps(每隔 16.7ms 左右)。通过 requestAnimationFrame 方法,每隔 16.7ms 去计算,计算的更细致而已,而且还会使得动画更流畅。来看下resume源码是如何处理的。
function resume(nowTime) {
// 两次 requestAnimationFrame 间隔时间
var elapsedTime = nowTime - initTime
var i
for (i = 0; i < len; i += 1) {
registeredAnimations[i].animation.advanceTime(elapsedTime)
}
initTime = nowTime
if (playingAnimationsNum && !_isFrozen) {
window.requestAnimationFrame(resume)
} else {
_stopped = true
}
}
首先会计算当前时间和上次时间的 diff 时间。
之后计算动画开始到现在的时间的当前帧数。
最后通过 renderFrame() 方法更新当前帧对应的 DOM 变化。
在 Lottie 中,每一次变化是根据 requestAnimationFrame 的间隔(每隔 16.7ms 左右)计算了更细致,保证动画的流畅运行。requestAnimationFrame 采用系统时间间隔,保持最佳绘制效率,让动画能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。
# 生成 SVG

# 总结
- Lottie 的优势
- 动画还原度高,使动画实现更加方便,动画效果也更好。
- SVG 是可伸缩的,任何分辨率下不会失真。
- JSON 文件,可以多端复用(Web、Android、iOS、React Native)。
- JSON 文件大小会比 GIF 以及 APNG 等文件小很多,性能也会更好。
- Lottie 的不足
- Lottie-web 文件本身仍然比较大,未压缩
lottie.js大小为 532k,轻量版压缩后lottie_light.min.js也有 147k。所以,需要注意 Lottie-web 的加载。 - 不必要的序列帧。Lottie 的主要动画思想是绘制某一个图层不断的改变 CSS 属性,如果用了一些插件实现的动画效果,可能会造成每一帧都是一张图,那就会造成这个 JSON 文件非常大。
- 部分 AE 特效不支持。有少量的 AE 动画效果,Lottie 无法实现,有些是因为性能问题,有些是没有做。
- Lottie 动画其实可以理解为 svg/canvas 动画,不能给已存在的 html 添加动画效果。