阅读 2563
2021年了,大厂如何使用高效的动画方案解放生产力!字节淘宝都在用,XDM,快上车!👍

2021年了,大厂如何使用高效的动画方案解放生产力!字节淘宝都在用,XDM,快上车!👍

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前言

大厂案例

今年 618 互动的动效可以大致划分为两类,星秀猫主形象和入口引导动效。

在项目开始之前,我们就开始沟通整体的技术选型和美术创作工具,也就是创作选型,我们沟通了几种动效选型。一种是帧动画,以逐帧的方式来播放动画;第二种是骨骼动画,基于一套基础的骨架,来播放动画;然后是 Lottie 动画,设计师可以通过 Adobe After Effects(以下简称 AE) 设计动画然后通过插件导出即可渲染出动效。

动效选型

由于骨骼动画天然适合这类有一个主体形象的动画,我们选择了2D 骨骼动画 Spine 来制作星秀猫主形象,通过网格自由变形和蒙皮技术在视觉上呈现“3D轴”的偏转。正是在 Spine 强大动画创作的支持下,星秀猫才有了“3D化”的动画化表现力。

动效制作 - 星秀猫主形象,Spine

而入口引导动效,存在量大且需要频繁调整的诉求,而 AE 是设计师们比较熟悉的软件,使用 AE 软件制作 Lottie 动画设计师们都比较愿意,因此我们采用 Lottie 来制作入口动效,制作简单的同时,时间较长的动画场景下相对 apng 和 gif 所需资源也更小。

640 (2).png

前置知识

在看动画方案之前,我们先来了解一下位图和矢量图的概念。

  • 矢量图是使用直线和曲线来描述的图形,构成这些图形的元素是一些点、线、矩形、多边形、圆和弧线等,它们都是通过数学公式计算获得的,具有编辑后不失真的特点。
  • 位图是由被称作像素(图片元素)的单个点组成的,放大会失真。

方案介绍

那了解完矢量图和位图,我们来看看前端要实现一个动画效果,有多少种方案可以选择呢? 常见的动画方案有Gif动画、png序列帧、SVG动画,我们来看看他们有什么区别。

Gif动画

图像互换格式(GIF,Graphics Interchange Format)是一种位图图形文件格式,以8位色(即256种颜色)重现真彩色的图像。它实际上是一种压缩文档,采用LZW压缩算法进行编码,有效地减少了图像文件在网络上传输的时间。适用于小动画如下拉loading、小动态logo、直播小礼物。

  • 优点
    • 使用简单,只需要引入一张图片
    • 还原度高
  • 缺点
    • 图片容易过大,需要权衡动画帧数和文件大小
    • Gif图是位图,放大会失真

png序列帧

帧动画是一种常见的动画形式,通过系列图片的连续播放来达到动画效果。具体实现是用css keyframes操作每一帧需要展示的图片,当然也可以将图片合并成精灵图(Sprites Map),可参考AlloyTeam的这个方案,使用 gka 一键生成帧动画

  • 优点
    • 可插入多帧,从而实现动画效果
  • 缺点
    • 每一帧都是一张图片,占比较大的体积
    • png是位图,放大会失真

SVG动画

SVG 意为可缩放矢量图形(Scalable Vector Graphics)。 在 SVG 中,要实现一个动画效果,可以使用 CSS,JS,或者直接使用 SVG 中自带的 animate 元素添加动画。 开发示例可参考SVG 动画标签,图形渐变,路径动画,线条动画

  • 优点
    • 矢量图,不失真
  • 缺点
    • 对初学者不友好,上手成本高,需要先了解SVG的API

更高效方案

了解了上面的常用方案后,我们可以看到,这些普通方案要么适配会有失真问题,要么上手有难度。我们更希望找到简单、高效、性能好、还原度高的动画方案。 ​

那有没有更高效的方案呢? 答案是肯定的,下面有请主角登场。 ​

一号嘉宾:SVGA

SVGA 是一种跨平台的开源动画格式,同时兼容 iOS / Android / Web。 SVGA Converter 可以将 After Effects 动画导出成 .SVGA 文件,供 SVGA Player 在各平台播放。 SVGA Player 支持在 iOS / Android / Web / ReactNative / LayaBox 等平台、游戏引擎播放。

更多介绍:SVGA官网

预览svga文件的动画效果:SVGA工具

各端播放器集成指南:SVGA集成

SVGA 动画库源码思路

通过阅读源码,我们捋一下他实现的思路

  • 一帧一帧
  • 通过设置帧率,来生成一个配置文件,使得每一帧都有一个配置,每一帧都是关键帧,通过帧率去刷每一帧的画面,这个思路跟gif很像,但是通过配置使得动画过程中图片都可以得到复用。性能就得到很大提升。并且不用解析高阶插值(比如二次线性方程,贝塞尔曲线方程)

优缺点

  • 优点
    • 使用简单,性能较好
    • 可动态添加或替换动画中的元素
    • 解析、播放器的库比lottie的精简许多(gzip前57KB),导出的文件也较小
  • 缺点
    • 动画是压缩产物,不支持二次编辑
    • 动画帧数低会造成视觉上的卡顿
    • 支持AE特性少
    • 内存占用比lottie稍高
    • 不支持复杂的矢量形状图层、不支持AE自带的渐变、生成、描边、擦除等

二号嘉宾:Lottie

Lottie是一个用于Android,iOS,Web和Windows的库。 设计师制作好动画,并且利用Bodymovin插件导出JSON文件。 前端直接引用lottie-web库即可,原理就是用JS操作Svg API,当然也可以指定为canvas。 前端完全不需要关心动画的过程,JSON文件里有每一帧动画的信息,而库会帮我们执行每一帧。

更多介绍:lottie官方文档

动画在线预览:预览地址社区热门动画

设计师安装插件:AE安装Bodymovin教程

1.png

Lottie 动画库源码思路

通过阅读源码,我们捋一下他实现的思路

  • 一层一层
  • 完全按照设计工具的设计思路来进行还原,将动画脚本导出并解析。动画脚本非常的轻量。
  • 将所有的动画拆成多个层级,每个层级layer都有一个动画配置,播放时解析多个layer的配置,并给每个layer做相应的动画。也达到了图片可以复用。当需要解析高阶插值(比如二次线性方程,贝塞尔曲线方程)时,性能相对而言差一点。

优缺点

  • 优点
    • 开发成本低,设计师导出JSON后,开发同学只需引用文件即可
    • 支持服务端URL创建,服务端可以配置JSON文件,随时替换动画
    • 性能提升,替换原使用帧图完成的动画,节省客户端空间和内存
    • 动画比例不会被拉伸,播放较为流畅
  • 缺点
    • 对某些AE属性不支持,具体可对照表
    • 对缓动曲线的解析会占用非常多的内存
    • 各平台效果的支持都不是很稳定,容易出 Bug

动画组件封装

Talk is cheap. Show me the code. image.png

我们团队在项目中是如何落地实践更高效的动画方案的呢?来看看组件封装的Demo吧! 小伙伴们可以从中学习组件的思路,并且尝试着在自己项目中落地,推动设计部门的UI小姐姐,使用高效动画方案,解放我们的生产力。 ​

以下代码均采用【Umi+React Hook】实现,仅供参考

Lottie组件代码

lottie组件效果示例2.gif

<h2>---------  循环播放  ---------</h2>
<XLottie url="https://assets9.lottiefiles.com/datafiles/gUENLc1262ccKIO/data.json" size={80}></XLottie>

<h2>---------  播放片段  ---------</h2>
<XLottie url="./anime/lottie/big.json" width={300} height={150} loop={false} startFrame={300} endFrame={500}></XLottie>

<h2>-----  0.5倍速 和 2倍速  -----</h2>
<XLottie url="./anime/lottie/loading.json" size={80} autoplay={true} loop={true} speed={0.5}></XLottie>
<XLottie url="./anime/lottie/loading.json" size={80} autoplay={true} loop={true} speed={2}></XLottie>
复制代码
import React, { useEffect } from 'react';
import lottie from 'lottie-web';

/**
 * @param url lottie的json地址
 * @param size 尺寸
 * @param width 宽度
 * @param height 高度
 * @param autoplay 是否自动播放
 * @param loop 是否循环
 * @param startFrame 播放片段(开始帧)
 * @param endFrame 播放片段(结束帧)
 * @param speed 播放速度,默认为1倍
 */
export type XLottieProps = {
  url: string;
  size?: string | number | undefined;
  width?: string | number | undefined;
  height?: string | number | undefined;
  autoplay?: boolean | undefined;
  loop?: boolean | undefined;
  startFrame?: number;
  endFrame?: number;
  speed?: number;
};

export default function XLottie(props: XLottieProps) {
  const { size, width, height, url, loop, autoplay, startFrame, endFrame, speed = 1 } = props;

  useEffect(() => {
    const element: HTMLElement = document.getElementById(id) as HTMLElement;

    const animation = lottie.loadAnimation({
      container: element,
      renderer: 'svg',
      loop: loop ?? true,
      autoplay: autoplay ?? true,
      path: url,
    });

    animation.setSpeed(speed);

    animation.addEventListener('data_ready', () => {
      if (startFrame !== undefined && endFrame !== undefined) {
        animation.playSegments([startFrame, endFrame], true);
      } else if (startFrame) {
        animation.goToAndPlay(startFrame, true);
      }
    });

    return () => {
      animation.destroy();
    };
  }, []);

  const id = `canvas_${randomNum(0, 1000)}_${new Date().getMilliseconds()}`;

  const style = {
    width: `${width || size}px`,
    height: `${height || size}px`,
  };

  return <div id={id} style={style}></div>;
}


function randomNum(min: number, max: number) {
  const Range = max - min;
  const Random = Math.random();
  return Math.round(Random * Range) + min;
}
复制代码

SVGA组件代码

import React, { useEffect } from 'react';
import SVGA from 'svga.lite';

// SVGA轻量版: https://github.com/svga/SVGAPlayer-Web-Lite

/**
 * XSvga props
 * @param url svga的资源地址
 * @param size 尺寸
 * @param width 宽度
 * @param height 高度
 * @param loop 循环次数,默认一直循环(0)
 * @param startFrame 播放片段(开始帧)
 * @param endFrame 播放片段(结束帧)
 */
export interface XSvgaProps {
  url: string;
  size?: string | number | undefined;
  width?: string | number | undefined;
  height?: string | number | undefined;
  startFrame?: number;
  endFrame?: number;
  loop?: number;
}

export default function XSvga(props: XSvgaProps) {
  const { url, size, width, height, startFrame, endFrame, loop } = props;

  useEffect(() => {
    const downloader = new SVGA.Downloader();
    // 禁止WebWorker线程解析
    const parser = new SVGA.Parser({ disableWorker: true });
    // #canvas 是 HTMLCanvasElement
    const player = new SVGA.Player(`#${id}`);

    (async () => {
      const fileData = await downloader.get(url);
      const svgaData = await parser.do(fileData);
      player.set({ loop: loop ?? 0, cacheFrames: true, startFrame, endFrame, intersectionObserverRender: false });
      await player.mount(svgaData);

      // 开始播放动画
      player.start();

      // 暂停播放动画
      // player.pause()

      // 停止播放动画
      // player.stop()

      // 清空动画
      // player.clear()
    })();
    return () => {
      downloader.destroy();
      parser.destroy();
      player.destroy();
    };
  }, []);

  const id = `canvas_${randomNum(1, 1000)}${new Date().getMilliseconds()}`;

  const style = {
    width: `${size || width}px`,
    height: `${size || height}px`,
  };

  return <canvas id={id} style={style} />;
}

function randomNum(min: number, max: number) {
  const Range = max - min;
  const Random = Math.random();
  return Math.round(Random * Range) + min;
}

复制代码

方案对比

了解到更高效的方案后,我们来看看哪个动画库更加优秀,根据他们的特性和团队项目实践效果,得到下面的表格~

动画库SVGALottie
社区生态600+ Star24k+ Star
开发成本
支持平台Android/iOS/H5Android/iOS/H5
资源产物压缩文件(不支持二次编辑)JSON(支持二次编辑)
动态替换元素支持不支持
循环指定某帧支持支持
占用内存
支持AE动效较少较多
适合场景直播、光效渐变、矢量、图标、微交互

我们可以看到,Lottie整体上更胜一筹,当然还要看具体的使用场景。

  • 在平面转换、移动、帧率较高、特效较多的场景,推荐使用Lottie。
  • 在直播送礼物的时候展示一些浮夸的光效,3D转换的效果,推荐使用SVGA。

注意

下面引用字节跳动飞书团队的case,给大家分享下使用时需要注意的细节点

不需要动画时,及时卸载 Lottie 动画组件

飞书出现过偶现 CPU 升高的异常案例,经过排查定位到是 Lottie 动画没有卸载引起的 CPU 升高。页面中已经没有动画,但 Lottie 一直在调用 requestAnimationFrame,导致在没有任何操作的情况下,CPU 占用升高至 2%-5% 左右,一般情况在没有任何操作的情况下,cpu 占用 0.1% ~0.2%。

image.pngimage.png Lottie 动画调用 react-lottie 组件,组件在 componentWillUnmount 时,会销毁该动画实例。 ​

飞书中 CPU 升高的时候,发现 Lottie 动画中有动画实例尚未销毁,导致会不停的调用 requestAnimationFrame,导致异常的动画是局部加载动画。 ​

飞书中用到局部加载的动画的模块有主端(切换会话、联系人页面 、新添加的联系人、机器人、外部联系人、onCall、升级提示弹窗、添加联系搜索、发送云盘文件弹窗、Pin 列表、Docs Webview 加载动画、切换租户)、日历、应用中心等。 ​

经过定位发现,应用中心里用到了局部加载动画, 在不需要动画的时候,没有卸载组件,只是通过 CSS 来隐藏了组件,导致没有销毁 Lottie 动画实例,requestAnimationFrame 会一直执行,代码如下:

// AppHome.js

 <div className={!isLoaded ? 'app-home-loadingImg' : 'display-none'}><PartialLoading /></div>
复制代码

image.pngimage.png

解决方案:不需要 Lottie 动画的时候,卸载 Lottie 动画组件。

// AppHome.js
{
  !!isLoading && (
    <div className='app-home-loadingImg'>
      <PartialLoading />
    </div>
  )
}
复制代码

结尾

  • 如果项目中需要一些动画,我们优先考虑高效的动画库,兼容性和效果都不错,可以提高团队开发效率。
  • 作为工程师,跨部门沟通也是非常重要的职场技能之一。我们前端可以通过调研一些动画方案,然后去推动设计部门形成一个动画设计标准,以及对动画转换工具的使用,形成开发闭环。
  • 作为工程师,我们要深刻意识到软件编程领域没有银弹,只有合适和不合适。我们可以根据每个团队的开发习惯和产品定位选择对应的技术栈,提高ROI,为业务产生更多价值!
  • 📃创作不易,如果我的文章对你有帮助,辛苦大佬们点个赞👍🏻,支持我一下~
  • 📌如果有错漏,欢迎大佬们指正~
  • 👏欢迎转载分享,转载请注明出处,谢谢~

参考

文章分类
前端
文章标签