lottie-web 实践与应用

9,154 阅读5分钟

前言

上一期根据源码对 lottie 做了分析,今天我们结合 lottie-web 文档来说具体的使用和开发中遇到过的坑。

我的初步思路是这样的,开发一个基于 lottie-web 的 react 基础组件,把动画组件作为一个子组件放入弹窗组件中去应用。接下来我们就开始造轮子。

Lottie-web 使用和常用方法

我们先来讲讲 lottie 的常用方法。

基本用法

    const anim = window.bodymovin.loadAnimation({
      container: element, // 挂载动画的容器dom元素
      renderer: 'svg', // 渲染方式,svg、canvas、html(轻量版仅svg渲染)
      loop: true, // 是否循环播放
      autoplay: true, // 是否自动播放
      path: animJsonPath, // 动画json文件路径
    });
 

常用方法

lottie-web 提供了很多控制动画的方法,我们沿用上面的animation对象来演示。

 
anim.play(); // 播放动画,从目前停止的帧开始播放
anim.stop(); // 停止播放动画,回到第0帧
anim.pause(); // 暂停动画,在当前帧停止并保持

anim.goToAndStop(value, isFrame); // 跳到某个时刻/帧并停止。isFrame(默认false)指示value表示帧还是时间(毫秒)
anim.goToAndPlay(value, isFrame); // 跳到某个时刻/帧并进行播放
anim.goToAndStop(20, true); // 跳转到第20帧并停止
anim.goToAndPlay(200); // 跳转到第200毫秒并播放

anim.playSegments(arr, forceFlag); // arr可以包含两个数字或者两个数字组成的数组,forceFlag表示是否立即强制播放该片段
anim.playSegments([10,30], false); // 播放完之前的片段,播放10-30帧
anim.playSegments([[0,10],[20,30]], true); // 直接播放0-10帧和20-30帧

anim.setSpeed(speed); // 设置播放速度,speed为1表示正常速度
anim.setDirection(direction); // 设置播放方向,1表示正向播放,-1表示反向播放
anim.destroy(); // 销毁动画,移除相应的元素标签等。在unmount的时候,需要调用该方法
 

常用事件

我们在 lottie-web 中可能也需要监听一些事件,比如动画相关的 dom 已经被添加到 html 后触发 的DOMLoaded事件。监听方法如下:


  anim.addEventListener('DOMLoaded', () => {
    if (typeof callback === 'function') {
      callback(anim); // 处理动画内交互逻辑
    }
  });
 

除了DOMLoaded事件,下面还有一些其他常用的事件可以监听:

  • complete: 播放完成(循环播放下不会触发)
  • loopComplete: 当前循环下播放(循环播放/非循环播放)结束时触发
  • enterFrame: 每进入一帧就会触发,播放时每一帧都会触发一次,stop 方法也会触发
  • segmentStart: 播放指定片段时触发,playSegments、resetSegments 等方法刚开始播放指定片段时会发出,如果 playSegments 播放多个片段,多个片段最开始都会触发。
  • data_ready: 动画 json 文件加载完毕触发
  • DOMLoaded: 动画相关的 dom 已经被添加到 html 后触发
  • destroy: 将在动画删除时触发

JSON数据动态更新

要实现 Lottie 的文本动态修改,需要对 Lottie 的运行机制有一定了解,简单来说, lottie-web 解析 JSON 之后产生相应的 JS 对象,并在动画播放期间,由 JS 对象计算并修改 HTML 中相应的 svg 元素属性,从而实现动画播放,这里我们只针对 SVG 类型说明。lottie原理可参考结合Lottie-web源码的深入分析

image.png

react-lottie 实现源码

考虑到通用性,我需要考虑传入存在可变的配置参数,比如动画 JSON、控制动图弹窗、回调函数等。

import React, { useEffect, useRef } from 'react';
import lottie from 'lottie-web';

export interface LottieType {
 animJson: any; // 动画 JSON
 setShow: Function; // 控制弹窗的显示
 showMark?: boolean; // 多层动画时,控制弹层蒙层
 callback: Function; // 回调函数
}

const Lottie = (props: LottieType) => {
 const {
   animJson,
   setShow, // 控制关闭
   callback,
   showMark = false, // 控制显示蒙层
 } = props;
 const lottieRef = useRef(null);
 let anim = null;
 useEffect(() => {
   if (lottieRef && lottieRef.current && !showMark) {
     lottieRef.current.parentElement.parentElement.parentElement.parentElement.parentElement.previousElementSibling.remove();
   }
   if (animJson) {
     anim = lottie.loadAnimation({
       container: lottieRef.current, // the dom element that will contain the animation
       renderer: 'svg',
       loop: false,
       autoplay: false,
       animationData: animJson,
       rendererSettings: {
         progressiveLoad: true,
         preserveAspectRatio: 'xMidYMid slice',
         imagePreserveAspectRatio: 'xMidYMid slice',
       },
     });
     anim.addEventListener('DOMLoaded', () => {
       if (typeof callback === 'function') {
         callback(anim); // 处理动画内交互逻辑
       }
     });
     anim.onComplete = () => {
       setShow(false);
     };
   }
   return () => {
     anim && anim.destroy();
   };
 }, []);
 return <div className="lottie-container" ref={lottieRef}></div>;
};

export default Lottie;

动态更新动画 JSON 的基类

animationActionBase.m.ts基类里提供动画操作的类型接口、加载动画的基类方法以及动态修改动画 JSON 内容的方法。

代码如下:

 
/**
 * 基类动画接口
 */
export interface AnimationActionBaseType {
  initPopLoad: Function; // 初始化动画弹窗和动画组件
  complete?: Function; // 自动以完成动画后操作
  loadDataPreUpdate: Function; // load动画数据前更新数据,返回修改后的数据
  loadedAnimationCallBack: Function; // 加载完动画后操作
}
/**
 * 加载动画基类
 * @param props
 *  initPopLoad: 初始化动画弹窗和动画组件
    complete?: 自动以完成动画后操作
    loadDataPreUpdate: load动画数据前更新数据,返回修改后的数据
    loadedAnimationCallBack, 加载完动画后操作
 */
export const animationActionBase = (props: AnimationActionBaseType) => {
  const {
    initPopLoad,
    complete = null,
    loadDataPreUpdate,
    loadedAnimationCallBack,
  } = props;
  if (typeof loadDataPreUpdate === 'function') {
    const data = loadDataPreUpdate();
    const callback = (anim) => {
      if (typeof loadedAnimationCallBack === 'function')
        loadedAnimationCallBack(anim);
      if (typeof complete === 'function') complete(anim); // 手动控制完成后的操作
    };
    if (typeof initPopLoad === 'function') initPopLoad(data, callback); // 初始化渲染弹窗
  }
};

/**
 * 修改动画图层layers中的文案
 * @param layers layers object
 * @param index  下标
 * @param value  内容
 */
export const setLayersText = (layers, index, value) => {
  layers[index].nm = layers[index].t.d.k[0].s.t = value;
};
/**
 * 修改动画assets里的图片数据
 * @param assets assets object
 * @param index  下标
 * @param imgUrl  图标地址  不修改地址传 null
 * @param imgFile 图片名称(带扩展名)
 */
export const setAssetImg = (assets, index, imgUrl, imgFile) => {
  if (imgUrl) assets[index].u = imgUrl;
  assets[index].p = imgFile;
};

/**
 * 批量更新assets里的图片 URL
 * @param assets assets object
 * @param imgUrl 图片 URL
 * @param version 版本清理 CDN 缓存
 */
export const setAssetListImgUrl = (assets, imgUrl, version = 1) => {
  assets.forEach((item) => {
    item.u = imgUrl;
    item.p = item.p.split('?')[0] + `?v=${version}`;
  });
};

参考文献