Twitter 家的 JS 动画库:Velocity React

5,224 阅读12分钟
原文链接: www.w3ctech.com

如果你习惯于使用CSS来制作动画效果,那么Velocity.js带给你的感觉很可能是“原来这一切竟然可以是真的”。

我们跟很多人一样,在很久以前就放弃使用jQuery动画来制作CSS过渡和关键帧,放弃前者令我们感到欣喜,放弃后者令我们感到无奈。我们hack了setTimeout,给它添加了动画触发的后渲染类;我们小心翼翼地避开“animationEnd”迷惑人的地方;我们的动画使用max-height属性而不是height属性,但愿永远不会有人用这个库显示超过9999px的文字。

我们做Velocity.js是因为它能带来很高的性能,可以利用到许多硬件加速的特性。整个编写代码的过程令我们感到痛苦万分,但是要想获得极佳的性能不就是要付出这些代价么!最终,我们所付出的一切变成了自我开脱的理由。传统的动画理念一直坚信这样的一个事实:“jQuery,甚至是JavaScript,它们的语法简单,但是性能太低;CSS很难,但是它运行起来很快。”

我们为自己选择了艰难的征程而感到自豪不已。

照这些说法来看,Velocity.js 简直就是骗人的,不可能有这样一种兼得JS与高性能的工具。毫不夸张地说,它回到了jQuery时代:Velocity钩挂到$上,它的velocity方法与那个卑鄙的(scoundrel )animate API兼容。Velocity的创始人Julian Shapiro告诉我们,它们的区别都是在一些细小的技巧里展现:Velocity有一个单一的timer循环,还有严格正确的DOM操作顺序,将这两方面结合起来就能对性能有一个夜以继日的影响(译者注:原文为night-and-day effect,没明白作者想表达什么,此处选择直译)。突然间,由JavaScript控制的动画就变得可行了,在这个基础上,你终于可以用你精通的那门语言来构建各种各样的功能:自定义渐变(包括弹簧物理特效),可靠的完成回调——更不用说Velocity的UI包里已经为你实现了许多现成的效果。

我们愿意打破旧习拥抱React,我们对于拥抱Velocity有着同样的热情,这个库里有许多动画相关的特性可以帮助我们消除大量工程中的苦恼,我们迫不及待地吸收它的精华,因为它可以轻松地为我们的页面带来更多乐趣与活力。

话虽这么说,事实上React和动画一开始很难融合到一起:React的强大之处源自对于UI进行了抽象的状态转换(这是一种声明式范式,它可以又快又准地实现我们的目标,我们慢慢会爱上这一种编码方式),动画就杂乱地散落在这些状态转换之中。

在这种情境下,CSS过渡属性可以很好地解决问题:“每当其它属性改变,便优雅地应用这些变更。”只要声明了动画,其间的过渡效果就自动生成了。而关键帧却无法很好地解决问题:“每当一个值改变,过一段时间才会为一些属性赋予一些特定的值。”这些语义会导致CSS关键帧动画可能在你不希望的时间生效(例如初始渲染时),一般来说总是最开始的时候,最重要的是,你不能通过任何方式对其进行动态修改。

为了将Velocity与React声明式的生态平滑地集成在一起,我们必须要确保渲染方法输出的内容可以完整地描述目标动画的行为:亦即达到一个理想的最终状态。

让我们感到开心的是,Velocity已经提供了我们所需的大部分功能。如果你为一个元素添加Velocity动画,你会清楚地知道它最终将达到一个理想中的最终状态1。它比CSS关键帧更加强大,它可以被随时停止,也可以从当前的显示状态沿着一条顺滑的最短路径到达新动画的最终状态[2]。你可以把它想象成React的reconciliation,只不过Velocity的动作(相对)较慢。

我们的Velocity/React集成可以按照这个简单的算法继续进行下去:

  • 最初,一个动态组件立即显示出来,此时它处于最终的目标动画状态(至少默认上是这样的)。通过这种方法我们可以立即显示一个处于最新状态的组件。

  • 如果指定的动画变了,我们会将这个动画的最终状态改成最新的,然后按部就班地执行动画;如果动画正在运行,我们会先停止动画,然后从它留给我们的中间状态开始顺滑地向下一个目标运动。

我们构建的这个组件命名为VelocityComponent

接下来是一个我们用它制作统计用“flaps(活板门)”动画的示例:当你的鼠标移至其中一个统计板上时,相应的活板会掀起来并显示更多的数据;当你的鼠标移开时,活板会掉下来,附加一个小幅度的摇摆效果。

render: function () {
  var animationProps;
  if (this.state.hovering) {
    animationProps = {
      duration: 200,
      animation: {
        rotateX: 160
      }
    };
  } else {
    animationProps = {
      duration: 1100, // longer due to swinging
      animation: {
        rotateX: [0, 'spring']
      }
    };
  }

  return (
    
{this.renderTopState()} {this.renderUnderneathStats()}
); }

Velocity保留了React的非动态性,我们的渲染预留了一个基于组件props和state属性的函数,改变state的值就会改变最终的动画效果,同时会触发VelocityComponent执行最新的变更。

了解了VelocityComponent的原理后,你会认为它非常简单。当它挂载时,使用Velocity的finish方法来即刻执行初始动画,迅速让我们得到理想的最终外观,然后它的componentWillReceiveProps方法会持续监视“animation”属性的变化。当它们出现时,当前所有动画都会停止并触发最新的动画。

VelocityComponent赋予了组件动起来的能力,这些组件通常在运动期间会保留在页面上。我们还构建了一个可以与附加组件ReactTransitionGroup集成的Velocity包装器,当子组件被添加到父节点或从父节点移除时运行动画(ReactTransitionGroup句柄会在DOM中一直保持“removed”组件直到过渡动画结束)。

有一个最好的VelocityTransitionGroup使用案例,如下所示,由实时事件组成的列表组件。当你浏览页面时这个列表会自动更新,当新的事件发生时,从顶部翻入最新的事件,并从底部滑出多余的事件。这样的运动信号可以提示你有新的内容可以浏览,你根本不需要笨笨地再去重绘整个组件。

render: function () {
  return React.createElement(VelocityTransitionGroup, {
      component: 'div',
      enter: {
        animation: EventAnimations.In,
        duration: 500
      },
      leave: {
        animation: EventAnimations.Out,
        duration: 500
      },
    }, this.state.events.map(this.renderEvent));
}

*在Github仓库中,你可以找到这个效果的完整实现代码,其中包括了动画定义的部分。

这些组件的核心价值是,在保持代码简洁的同时,可以轻快地为你的app添加一些活泼的元素。只要在为渲染方法中引入一点点VelocityComponents方法和VelocityTransitionGroups方法,你的元素从此就可以不再闪入闪出了,它们可以滑动,可以摇摆,还可以做更多有趣的效果。

我们一起来看一下如何整合所有的这些特性来通过我们的UI展示测试者的设备页面。我们先从切换按钮开始,它将为其控制的组件提供折叠和展开的状态切换功能。

renderDeviceToggle: function () {
  var arrowAnimation = {
    rotateX: this.state.expanded ? 180 : 0,
    transformOriginY: ['42%', '42%']
  };

  return (
    
); }

组件默认渲染的是向下的箭头,如果它处于“展开”状态,它会反转这个箭头变成向上箭头。

接下来要展示测试者设备的页面。在我们以前的经验中,我们可能尝试把每一行的页面都渲染出来,然后用jQuery控制它们展开。如果在React中实现这个功能,只有当组件处于“展开”状态时才会去渲染设备页面,仅这样的小改变就可以不再提前处理很多用户可能都不会去看的DOM元素,从而节省许多时间和内存。

想要平滑地展开这一行,我们需要借助Velocity中“slideDown”和“slideUp”动画的力量,它们是内建的动画效果,可以使元素从原来的状态滑动到另一个状态(尝试不用JavaScript来实现这个功能)。我们可以用VelocityTransitionGroup来自动触发“slideDown”和“slideUp”动画,设备列表在下滑时渲染,上滑时不渲染。

render: function () {
  return (
    
{this.renderTesterInfo()} {this.renderDeviceToggle()} {this.state.expanded ? this.renderDeviceList() : null}
); }

详细的来说,整个过程是这样的:当我们点击切换按钮时会将它的状态设置为“展开”,此时会触发React重新进行渲染;切换动画改变了,触发VelocityComponent将按钮改成“向上”箭头;与此同时,我们实际上也开始渲染设备列表,这一行为也会被VelocityTransitionGroup捕捉到,它会在新的子节点上附加“slideDown”动画。

再次点击按钮会执行相反的操作:箭头重新变回向下,设备列表停止渲染,VelocityTransitionGroup平滑地滑上去并移出DOM。

当然,我们还漏掉了一样东西:数据!当列表第一次展开时,事实上我们尚未从服务器上获取设备的详细信息。我们需要在发出请求后展示一个加载中的状态,当请求返回数据时去除加载动画并显示结果,我们脑海中已经浮现出这个平滑的效果了。

当被点击的行展开时,我们会在列表中渲染几个虚拟出来的“加载中”设备,然后当数据到达时,我们再将数据平滑地淡入到已经渲染出来的列表中。

renderDeviceList: ->
  var loaded = this.state.devices != null;
  var deviceList = loaded ? this.state.devices : Array.apply(null, Array(this.state.deviceCount));

  return React.createElement(VelocityTransitionGroup, {
      style: { position: 'relative' },
      enter: {
        animation: { opacity: [1, 0] },
      },
      leave: {
        animation: { opacity: 0 },
        style: {
          position: 'absolute',
          top: 0,
          left: 0,
          bottom: 0,
          right: 0,
          zIndex: 1
        }
      }
    },
    React.DOM.div({ key: loaded ? 'devices' : 'loading'},
      deviceList.map(this.renderDevice)));
}

我们可以用VelocityTransitionGroup来增强淡入淡出的功能。淡入的元素其不透明度由0到1,淡出的元素其不透明度由(可能是1)到0。此外,我们使用的是绝对定位,所以两个元素在过渡过程中会互相重叠。

这段方面有以下两个方面值得额外一提:

  • 我们的renderDevice方法可以接受一条设备信息或undefined,如果设备信息是异步的,那么它会渲染一个“加载中”的设备。我们用同样的方式维护这两个状态,因为想得到一个平滑的淡入淡出效果,必须要保持加载中和加载后拥有一致的外观。

  • 属性key的改变是React中reconciliation算法的信号,对“加载中”和“已加载”的两个状态应该区别对待,这将会使潜在的ReactTransitionGroup方法将其判断为老元素离开并且新元素进入,而非单一元素的状态改变。

由于我们特别喜欢淡入淡出特效,因此我们将它封装在了LoadingCrossfadeComponent中,你可以从我们Github仓库的demo代码中找到它。现在,你就可以用一个独立的包装器在任何我们需要的地方渲染带有淡入淡出效果的元素,这一过程为我们省却很多与React组件模型值的沟通交互,使得在任何视图层次结构的层级上提取可重用UI片段变得轻而易举。

有了VelocityComponent,可以很轻松地在你的app中使组件动画化,也可以很方便地维护它们。我们用它做了很多事情,小到翻转效果,大到滑动选项指示器,以后还会有更多更有趣的功能等着你实现。Velocity特殊的能力可以让元素的任意外观推动到下一状态,这也使得它可以非常好地为反复无常的渲染方法添加更多的过渡效果。

VelocityTransitionGroup使元素远离突兀地出现又消失,通过它我们只需要选择是否渲染就可以不费吹灰之力地实现滑动、翻转和淡入淡出这些效果。

你现在就可以通过NPM安装velocity-react包,配合着React开发的app使用这两个组件,记得clone我们的Github仓库来查看我们构建的demo,请让我们了解它能为你做什么,也欢迎你提交未来发现的任何问题。

脚注:

1:我想在这里额外解释一下,通常来说,直接修改React为你创建的DOM元素不是一个好的选择,但是在我们的经验中,Velocity可以安全地更新DOM元素的样式,即使元素的其它属性甚或是样式属性有所改变,通过Velocity添加的属性依然会保留如初。当然啊,你不可能同时通过Velocity和React一起改变相同的样式属性,这一定会出错的喔。

2:这里还要给大家一个预警,你至少应该了解这一个事实。Velocity可以从任意初始状态开始运行动画,但是它执行每一段动画都需要有一个固定的时间片段。所以如果你想在500ms内将元素的不透明度调整为0,实际上会在完全不透明的元素和已经半透明的元素状态各持续500ms。事实上,这也不太是个问题,动画通常都会有自然休止状态的固定起点,只是在中断时出现动画通常无人能够察觉。

我们正在帮助我们的用户构建世界上最好的app。想要成为Fabric团队的一份子一起来构建超赞的东西么?请查看一下我们开放的岗位吧!