本文也发布在知乎 zhuanlan.zhihu.com/p/373244605

开发移动端项目时,特别是活动页面,免不了会涉及动画。而且好的动画会给产品加分。本文总结了 web 端常见动画实现的方式,希望读者在阅读完本文后,掌握动画开发技巧,更加自信地解决交互设计师提出的需求。

Toolbox

实现 web 端动画有多种方式,常见的有 GIF/APNG,CSS3,JavaScript,lottie,SVG,canvas 等。具体在业务中使用何种方式,需要综合考虑实现成本与运行效率。需要对动画进行较多控制的,使用 JavaScript 或 lottie,其他使用 CSS3 与 GIF/APNG。

在上述的动画方式中,GIF/APNG 无疑最省力。相较于 GIF,APNG 更有优势(参考 这篇文章 )。简单的场景下,使用 APNG 与 setTimeout 也可以实现流程的控制。 不过于在实际使用的时候,注意以下问题:

  • APNG 预加载
  • 通常 APNG 图片尺寸较大,最好提前加载。

  • APNG 不重复播放
  • 已缓存的 APNG 图片再次显示时,动画不播放。你可以在链接中通过 query 参数来重新加载图片。 Codepen 示例

    大家应该已经在项目中广泛应用 CSS3 动画,transition/animation 是动画利器。常见的过渡效果通过 CSS 都可以实现。如果没有思路的话,不妨去 Animate.css 或者 Animista.net 看看,也许答案就在上面。

  • animation-fill-mode
  • 该属性设置在动画结束后,保持终止状态还是恢复初始状态。经常使用 animation-fill-mode: forwards; 来保持终止状态。

  • animation-function 中的 steps 函数
  • 一般情况下,动画的过渡是连续的。通过 steps 函数可以让动画「断断续续」(每次切换一帧),实现帧动画的效果。借助该特性,实现一个简单的倒计时。 Codepen 示例

  • 好用的 clip-path
  • clip-path 属性用于控制元素的显示区域。虽然使用 overflow: hidden; 也可以实现部分效果,但是代码量会增多,且对于多边形的裁剪就无能为力。 Codepen 示例

    尽可能多地使用 transform ,有需要时使用 will-change 属性。如果同时要对大量的 DOM 元素做动效,或许你应该尝试使用 Canvas 而非 CSS。

    Vue 的封装

    Vue (特指 2.x) 对于动画进行了封装,提供 transition 与 transition-group 组件,过渡/动画用一套 API。Vue 中的过渡两个特性值得关注:

    设置 mode 可以同时对离开与出现的元素添加过渡效果。 Codepen 示例

  • transition-group
  • 当你要给多个元素做动画时,可以使用 transition-group组件。移动端场景中常见的跑马灯使用该组件实现。 Codepen 示例

    Lottie、SVG

    对于复杂的动画,推荐使用 Lottie ,并且动画制作软件 AE 支持导出 Lottie 文件。Lottie 使用方式极其简单,导入 JSON 文件完成动画的创建。

    import lottie from 'lottie-web';
    import animData from './animData.json';
    const anim = lottie.loadAnimation(animData);
    

    使用 lottie-api ,你甚至可以编辑原有的动画。 Codepen 示例

    Lottie 相当于使用 JS 来播放动画,你可以对动画的播放速度,次数,帧数,顺序进行精准的控制。事件或者方法参考 官方文档 ,这里不再赘述。

    如果 Lottie JSON 文件引入图片资源,要去调整图片的路径,避免出现 404 问题。当然更好的方法是使用自动化工具比如 lottie-loader 来处理 JSON 文件,调整图片路径。在使用了 lottie-loader 的前提下,你可以直接把 JSON 文件当做组件来用。

    // 将 JSON 当做 Vue 组件来使用
    import MyAnimate from './data.json'
    export default {
        components: { MyAnimate }
    

    Lottie JSON 字段含义

    遗憾的是, Lottie 官方文档并没有介绍 JSON 中各字段的含义,只能在代码仓库中找到一些 JSON schema 描述文件 。下面对部分字段做简要描述,当你有定制化需求时,或许需要:

        "fr"20,        // 每秒播放的帧数     "ip"0,         // 动画开始的开始帧数     "op"40,        // 动画开始的结束帧数     "w"700,        // 动画内容区域宽度     "h"500,        // 动画内容区域高度     "assets": [{    // 图片资源,为避免图片 404 问题,你可能需要编辑这里         "w"120,   // 图片宽度         "h"120,   // 图片高度         "u""images/"// 图片路径         "p""img_0.png"// 图片名称

    fr , ipop 可知动画播放一个周期需要 2s。通过调用 setSeed(2) 可以将播放时间降为 1s;通过 play(frame) 或者 goToAndPlay(frame) 类似的方法设置动画从某一帧开始播放。

    你可以编写一个简单的 lottie-loader 来处理 assets 中的图片路径

    动画实践指南

    借助上文中提到了多种工具,处理简单的动画不在话下。对于稍微复杂的动画,要用 JS 去编写动画逻辑。以下面的动画效果为例,来讲解实现思路。

    效果图尺寸较大,可以点击 这里 预览效果。

    通过分析发现,主要的逻辑集中在红包上。红包有停止,暂停两种状态;按照某个频率从顶部还会落下新的红包;按照某个频率移除可视区范围外的红包;移动的过程中旋转红包。 编写代码时,为了提高灵活性,最常用的策略是数据抽象过程抽象。先进行数据抽象,把红包雨整个区域用数据来描述。

    // 动画区域
    class Stage {
      // ...
      childrenPacket[]
      durationnumber
      destroyedboolean
    class Packet {
      xnumber
      ynumber
      degreenumber
      rotation'clockwise' | 'anticlockwise'
      status'idle' | 'moving' | 'removed'
    

    接下来,进行过程抽象,添加方法,让数据动起来。由于存在多种频率不同的动画,把一种动画想象成一个 task,用 async 函数描述过程。

    class Stage {
      init() { }
      // 按照一定的频率添加新的红包
      async add() {
        while (true) {
          if (this.destroyedreturn
          await wait(200)
          // 执行动画逻辑 ...
      // 与 add 类似,让红包移动,同时做清理工作
      async animateAndClear() { }
    class Packet {
      // 设置停止
      stop() { }
    

    通过前面两步,完成了对整个动效的抽象。此时,动画已经是「运动」的了。接下来都是 View 层的工作。借助我们已经掌握的技能,View 层不难实现。 在项目开发过程中,使用的框架是 Vue。如果你想用 Canvas 去实现,数据层可以复用,只需要针对 View 层做适配工作即可。

    状态的复用与组件抽象

    React 与 Vue3 都提供了对 Hook 的支持,数据或者处理数据的逻辑(Hook)是可以复用的。在基础组件库日益丰富的当下,View 层的工作以组合基础组件为主,相当多的业务逻辑是处理数据。回到刚才的例子,我们将先考虑状态的设计,再剥离状态,类似于对 Lottie/Hook 的简单模仿。毕竟调整数据的配置比编写业务代码成本低得多。

    Hook 与动画

    Vue 的封装/mode小节中的示例如果用 Hook( ReactSpring ) 来实现的话,核心代码如下:

    import { useTransition, animated } from "react-spring";
    export default function App() {
      // ...
      const transitions = useTransition(show, null, {
        from: { opacity0 },
        enter: { opacity1 },
        leave: { opacity0 }
      return <div>...</div>
    

    完整的代码

    从上面的例子可以看出,我们只需要设置好初始终止状态,然后再与 View 层做绑定,整个动画就完成了,便捷程度堪比 CSS3。在先前 Vue 的实现中,依赖 transition 组件的功能,而 ReactSpring 则是整个过渡的状态提供给你,你决定如何去渲染。

    如果后续需要对该场景进行封装,那么大概会这样拆分:

  • 过渡状态抽象成 Hook,对外导出
  • 基于 Hook 实现一个组件,默认导出该组件
  • 用户如果对组件不满意,是完全可以基于 Hook 实现新的组件,代码量并不大。
  • 随着 Hook 概念的推广,我们也要逐渐学习掌握它。开发项目过程中,有意识地去思考如何对数据进行抽象,如何更好的管理数据。

    本文未涉及的内容

    本文主要讨论的是 web 端简单的动画实现方式。对于更为复杂的场景,如 HTML 5 游戏,为了性能与开发效率的考量,建议使用 pixi.js 或者 phaser 之类的游戏框架。

  • 丰富你的工具箱,掌握更多工具
  • 对需求进行拆分,对数据抽象/过程进行抽象,提高可维护性
  • 学习 Hook,思考状态逻辑如何复用
  • ziyoung web 前端
    粉丝