当React遇见了Lottie, React动画组件该如何实现?

2,011 阅读9分钟

上几期写过关于结合Lottie-web源码的深入分析以及 lotite-web实践与应用,这里就不再介绍 lottie 了,感兴趣的朋友可以看看这两篇文章。

如果在React项目直接使用 lottie-web 也不是不可以,但是,为了提高复用率,使用简单方便,所以还是有必要的, 废话不多说,直接开干...

React 版组件封装

首先需要安装 lottie-web 渲染的库

yarn add lottie-web

新建 index.tsx 组件页面,代码如下

import React, { useRef, useEffect, useMemo, forwardRef, useImperativeHandle, Ref } from 'react';
import lottie, { AnimationItem } from 'lottie-web';

// 渲染类型
type rendererType = 'svg' | 'canvas' | 'html';

/**
 * 参数属性
 *
 * @interface IProps
 */
interface IProps {
  renderer?: rendererType; // 设置渲染器类型
  loop?: boolean; // 是否循环
  autoPlay?: boolean; //是否自动播放
  animationData?: Record<string, any>; //  AE 或者 Lottie 导出的JSON数据
  imageAssetsFolder?: string; // 从animationData获取数据时,调整assets图片路径
  path?: string; // AE或者Lottie导出的JSON文件路径。(animationData和path互斥)
  rendererSettings?: rendererSettingsType;
  initLoad?: Function; // 动画加载之后处理函数
}
/**
 * rendererSettings options
 *
 * @interface rendererSettingsType
 */
interface rendererSettingsType {
  width?: number;
  height?: number;
  context?: CanvasRenderingContext2D; // canvas 上下文, only canvas 2d
  scaleMode?: string; // 刻度模式 noScale
  clearCanvas?: boolean; // 是否清除画布
  progressiveLoad?: boolean; // Boolean, only svg renderer, loads dom elements when needed. Might speed up initialization for large number of elements.
  hideOnTransparent?: boolean; // Boolean, only svg renderer, hides elements when opacity reaches 0 (defaults to true)
  preserveAspectRatio?: string; // viewBox属性,主要用于控制viewBox的位置,或者说viewbox的对齐方式 , ,x,y 分别各有3种值xMin、xMid、xMax、yMin、yMid、yMax
  className?: string;
}

export default forwardRef((props: IProps, ref: Ref<any>) => {
  const {
    renderer = 'svg',
    loop = true,
    autoPlay = true,
    animationData,
    path = '',
    imageAssetsFolder,
    rendererSettings,
    initLoad,
  } = props;

  const refContainer = useRef(null); // 动画渲染容器
  const refAnimation = useRef<AnimationItem | null>(null); // 对外输出的ref 对象
  useImperativeHandle(ref, () => ({
    init: () => refAnimation.current,
  })); // 指定父级组件调用暴露的 ref对象,方便控制动画
  let cacheAnimationOptions: any = useMemo(() => {
    const options: IProps = {
      loop,
      renderer,
      autoPlay,
      rendererSettings,
    };
    if (animationData) {
      options.animationData = animationData;
      if (imageAssetsFolder) setImageAssetsFolder(options.animationData.assets, imageAssetsFolder);
    } else {
      options.path = path;
    }
    return options;
  }, [loop, renderer, animationData, path]);

  useEffect(() => {
    if (!refContainer.current) return;
    // 渲染动画
    const lottieAnimationItem: AnimationItem = lottie.loadAnimation({
      container: refContainer.current,
      ...cacheAnimationOptions,
    });
    if (typeof initLoad === 'function') initLoad(lottieAnimationItem);

    // 渲染后的动画实例对象复制给 refAnimation.current,返回
    refAnimation.current = lottieAnimationItem;
    return () => {
      if (!refContainer.current) return;
      refAnimation.current = null; // 重置下
      cacheAnimationOptions = null;
      lottieAnimationItem.destroy(); // 避免内存泄漏
    };
  }, [refContainer.current, cacheAnimationOptions, initLoad]);
  const width = rendererSettings?.width ? rendererSettings?.width + 'px' : '100%';
  const height = rendererSettings?.height ? rendererSettings?.height + 'px' : '100%';

  return <div ref={refContainer} style={{ width: width, height: height }}></div>;
});
/**
 * 批量更新assets里的图片目录
 * @param assets assets object
 * @param imgUrl 图片目录|path
 */
const setImageAssetsFolder = (assets: Array<{ u: string; p: string }>, imgUrl: string) => {
  const t = new Date();
  assets.forEach((item) => {
    if (item.u === imgUrl || item.p.indexOf('?t=') !== -1) return;
    const img = new Image();
    item.u = imgUrl;
    item.p = item.p + `?t=${t.getTime()}`;
    img.src = imgUrl + item.p; // 预加载
  });
};

从代码了可以看出,有个 setImageAssetsFolder 函数是处理动画文件里图层图片资源路径以及预加载,另一种方案就是把 asset 里所有图片资源合并变成 base64为一张图,不过前提是得有base64 编码的图片集才行,赶兴趣的同学可以试试。

Demo 示例

demo.tsx

import React, { useEffect, useState, useRef, useCallback } from 'react';
import  Lottie from './index';
import lottie_api from 'lottie-api';
 
export default () => {
  const [visible, setVisible] = useState(false);
  const lottieRef = useRef(null);
  
  const lottieAction = useCallback(
    (type: string) => {
      if (!lottieRef.current) return;
      const lottieAnimation = lottieRef.current.init();
      if (type === 'play') {
        // 从当前状态继续向前播放
        lottieAnimation.play();
      } else if (type === 'pause') {
        lottieAnimation.pause(); //暂停
      } else if (type === 'stop') {
        // 停止动画,恢复到初始状态
        lottieAnimation.stop();
      }
    },
    [lottieRef],
  );
  
   // 动画内的交互操作,均可在此处处理
  const addListener = useCallback((lottieAnimation) => {
    lottieAnimation.addEventListener('DOMLoaded', () => {
      const api = lottie_api.createAnimationApi(lottieAnimation);
      // 可以通过 api 给目标元素增加监听事件,这里就不演示 
    });
  }, []);

 return (
     <>
     ```
  <div title="控制动画">
    <button  onClick={() => lottieAction('play')}>
      播放
    </button>
    <button  onClick={() => lottieAction('pause')}>
      暂停
    </button>
    <button onClick={() => lottieAction('stop')}>
      停止
    </button>
    <div style={{ width: '200px', height: '200px' }}>
      <Lottie
        ref={lottieRef} path="https://assets10.lottiefiles.com/packages/lf20_a3emlnqk.json"
        rendererSettings={{ className: 'lf20_a3emlnqk', width: 200, height: 150 }}
        initLoad={addListener}
      />
    </div>
      </div>
 
     </>
 )
 }

效果

lt.gif

文档说明

Lottie 组件可以通过设置该组件的 ref,通过 ref.current 上挂载了 init 方法,它返回动画实例对象。通过该实例对象可以对动画进行相关操作,具体详情请参考官方 usage api

官方还提供了神奇的lottie-api运行时修改 lottie 的库,可惜文档缺失,用的人并不多,但是功能很强大。测试 demo 里有相关示例。该库续根据项目需要是否安装即可。

IProps

属性说明类型默认值
renderer设置渲染器类型 ,分别'svg', 'canvas' ,'html' 3 种类型 可选string'svg'
loop是否循环 可选booleantrue
autoPlay是否自动播放 可选booleantrue
animationDataAE 或者 Lottie 导出的动画 JSON 数据 可选Record<string,any>true
imageAssetsFolderimageAssetsFolder 情况,设置带图的 assets 目录 可选string--
pathAE 或者 Lottie 导出的 JSON 文件路径。(animationData 和 path 互斥) 可选string''
rendererSettings画布设置 可选rendererSettingsType--
initLoad渲染前调用函数,函数参数:动画对象 ,动画的事件监听可在这里处理 可选Function--

rendererSettingsType

属性说明类型默认值
contextcanvas 上下文 可选CanvasRenderingContext2D--
scaleMode刻度模式 可选string
clearCanvas是否清楚画布 可选boolean
progressiveLoad仅 svg 渲染器,在需要时加载 dom 元素。 可能会加快大量元素的初始化。 可选boolean--
hideOnTransparent仅 svg 渲染器,当不透明度达到 0 时隐藏元素 可选booleantrue
preserveAspectRatio参考下面参说明 可选string'xMidYMid meet'
className自定义样式 Name 可选string--
widthsvg 宽 并且父级元素同宽 可选number--
heightsvg 高 并且父级元素同高 可选number--

preserveAspectRatio

viewBox 属性,主要用控制 viewbox 的对齐方式,其值为空格分隔的两个值组合而成;第 1 个值表示,viewBox 如何与 SVG viewport 对齐;值由 x,y 分别各有 3 种值 xMin、xMid、xMax、yMin、yMid、yMax 组合而成;第 2 个值表示,如何维持高宽比,三种类型:

  • meet 保持纵横比缩放 viewBox 适应 viewport
  • slice 保持纵横比同时比例小的方向放大填满 viewport
  • none 扭曲纵横比以充分适应 viewport

AE设计师——注意事项

支持度和兼容性方面的重要建议

  • 不使用表达式和特效。lottie 暂不支持。
  • 注意遮罩尺寸。若使用 alpha 遮罩,遮照的大小会对性能产生很大的影响。尽可能地把遮罩尺寸维持到最小。
  • 不使用混合模式和亮度蒙版。
  • 描边动效不支持,因为这个属性会导致 lottie 性能问题,所以后来 lottie 去掉了对该属性的支持。
  • 设置空白对象。若使用空白对象,需确保勾选可见并设置透明度为 0% ,否则不会被导出到 json 文件。
  • Matte 和 mask 有尺寸问题,使用半透明遮罩会影响性能,可能会造成潜在的渲染错误以及严重的性能损耗!很多设计师习惯画图形用纯色图层,纯色图层形成的形状是基于蒙版的,这个要注意。如果必须使用遮罩,请覆盖最小的区域。
  • lottie 不支持图层样式,图层效果不支持 drop shadow, color overlay 或 stroke。
  • lottie 不支持合成路径(merged path),如果从 AI 中制作的图形有,请删掉。
  • 图层命名尽量用英文。如果你使用了渐变,那么工程名(包括 aep 文件名)以及渐变图层、到其父级图层、到根级图层这条链路中的每一层都要用英文,包括变换属性命名,否则会出现渲染错误。

兼容性上的建议

  • 滤镜效果在客户端上不全支持;颜色滤镜的试用对客户端的运行上存在额外的性能损耗。
  • 图层如果存在 “自动定向” 特性,在 Web 和 Android 上不支持。
  • 不推荐使用“遮罩层”,遮罩层对客户端运行性能的损耗极大,建议避免使用。如果可以用蒙版效果替代,建议用蒙版替代使用。
  • 蒙版的使用对客户端运行性能消耗比较大,如果存在蒙版为“非必需”的,务必删除。
  • 图层如果使用 “时间重映射” 特性,在 iOS 上不支持。
  • 矢量的形状在客户端运行时存在性能的损耗。尤其是在形状图层数量比较大的情况下。如果一个形状无形变要求,可以考虑转化成 png 图片替代,以提高运行性能。
  • 建议用“形状”图层替代“纯色”;纯色模块自带一个无意义的“蒙版”,对客户端运行有不必要的性能损耗,量大时引起客户端卡顿;

性能方面的建议

  • 动画简单化。创建动画时需时刻记着保持 json 文件的精简,比如尽量不使用占用空间最多的路径关键帧动画。诸如自动跟踪描绘、颤动之类的技术会使得 json 文件变得非常大且耗性能。
  • 如果有循环的帧,请不要在动画文件里面循环,请数出帧数,让开发自行控制这段动画的循环,能节省相同图层和动画的体积。
  • 建立形状图层。将 AI、EPS、SVG 和 PDF 等资源转换成形状图层否则无法在 lottie 中正常使用,转换好后注意删除该资源以防被导出到 json 文件。
  • 设置尺寸。在 AE 中可设置合成尺寸为任意大小,但需确保导出时合成尺寸和资源尺寸大小保持一致。
  • 在尽量满足效果的情况下,请对路径做适当的裁剪,这个对性能影响很大。
  • lottie 进行动画的时候会按照 AE 的设计进行分层,所以要尽量减少层数。
  • 若确实没有必要使用路径动画,请将矢量图形替换为 png 图片,并用 transfrom 属性完成动画。
  • 可以根据实际状况,斟酌降低动画帧率或者减少关键帧数量,这会减少每秒绘制的次数。
  • 精简动画时长,可以循环的动作,就不要在时间轴做两遍,每一次读取关键帧都会消耗性能。编排上尽量避免 a 动作结束,b 动作开始,可以让动作有所重叠,减少动画长度。
  • 同类项合并,有些元素是相似的,或者相同的用在了不同的地方,那就把这个元素预合成重复使用这一个元件,可以通过对该预合成的动画属性的调整达到想要的动画效果。
  • 尽量减少图层个数。每个图层都会导出成相应的 json 数据,图层减少能从很大程度上减小 json 大小。
  • 尽可能所有的图层都是在 AE 里面画出来的,而不是从其他软件引入的。如果是其他软件引入的,很可能导致描述这个图形的 json 部分变得很大。
  • 制作的时候,请将动画元素铺满整个画布,这样可以避免浪费,也方便前端进行尺寸的调整。
  • 如果矢量图形是在 AI 中导出的,请将多余的“组”等没有任何实际效用的元素删掉。
  • 删除那些关闭了和无用的属性。
  • 只导出 1x 图。
  • 为了防止 lottie 导出的兼容性问题,请尽量使用英文版本 AE ,图层需简洁,命名清晰
  • 避免大面积矢量部分,以及大面积粒子效果
  • 避免使用 3D 等高性能损耗样式

最后

以上就是本文全部内容,希望这篇文章对大家有所帮助,有不到之处欢迎在评论区指正交流你的想法和心得,欢迎一起探索前端。

Lottie 相关资料

lottie web 官方文档

lottie 动画在线预览

lottieFile 免费动画库

官方提供的 lottie-api

oasisengine