logo
当前位置:首 页 > 编程技术 >前端开发 > 查看文章

打造高性能剪切动画

前端开发, 编程技术 你是第2152个围观者 0条评论 供稿者: 标签:

在谷歌的产品中我们经常会看到各种各样的交互动画,对于前端开发来说实现动画效果也是经常遇到且头疼的事情,原作者 Paul Lewis 是谷歌的一名工程师,在这篇文章中我们将探讨如何实现如下的交互设计

上面的交互效果是将一个菜单分为收起状态和展开状态,在收起状态下只展现收起状态下的一部分,所以我们可以看成收起状态是把展开态的一部分给剪切掉了,这也是为什么原文中作者使用 animating clips 来描述这种交互形式。

方案1:通过动画修改容器的宽高

我们定义好菜单收起和展开两种状态下的 width 和 height 属性,并且给元素加上 transition 过渡,通过切换元素的 class 实现动画效果。具体的实现代码如下:

.menu {
  overflow: hidden;
  width: 350px;
  height: 600px;
  transition: width 600ms ease-out, height 600ms ease-out;}.menu--collapsed {
  width: 200px;
  height: 60px;}

这个方法简单粗暴见效快,但对于老司机来说这并不是一个满意的方案,因为在修改元素的样式属性时浏览器会重新渲染页面,对 width 和 height 属性而言,每一次修改之后页面都需要进行重排,这个渲染过程开销相对而言比较大,所以动画很难达到60fps。

方案2:使用 CSS clip 或 clip-path 属性

除了上面说的修改宽和高之外我们还可以使用 clip 属性(已经废弃)来实现。当然,因为 clip 已经弃用了,我们应该使用 clip-path 属性搞定,但 clip-path 目前的浏览器兼容性并不好,所以目前使用clip 或 clip-path 也不是一个完美的解决方案。

.menu {
  position: absolute;
  clip: rect(0px 112px 175px 0px);
  transition: clip 600ms ease-out;}.menu--collapsed {
  clip: rect(0px 70px 34px 0px);}

这个方案性能上比方案1要好,但仍旧会在每一帧触发重绘。并且 clip 属性需要元素是 absolute 或 fixed 定位,这样可能会让 CSS 代码略麻烦。

方案3:CSS Animating 和 scale

方案1修改的是元素的大小,方案2是裁剪元素的可视区域,现在我们换一种思路。

首先从效果上看我们可以认为菜单的收起态是展开态缩放而来的,也就是说我们将展开态高度所放到原来的 1/5 就是收起态的高度。我们对元素做 transform: scale(0.2, x)(这里没考虑宽度的缩放比例) 操作,效果如下

现在菜单的高度缩小为原来的 1/5,但是问题就是菜单的内容也被相应的缩小了,要解决这个问题很简单,我们直接把菜单的内容放大 5 倍即可。

所以方案三其实是在容器上设置一个缩放,然后在内容上设置一个反向缩放(counter-scale),这样就可以保证在容器收起或展开的时候菜单里的内容不会被压缩或者放大。这个方法逻辑上稍显麻烦,但是修改 transfrom 属性不会触发重排或者重绘,并且可以利用 GPU 进行运算,因此可以达到更高的性能从而达到更高的帧率。

下面详细说一下实现步骤:

第一步:计算开始和结束状态

首先要得到菜单收起和展开状态下的大小,由此来计算出我们的缩放比例。当然,我们并不能一次取到菜单收起和展开状态下的大小,因此需要先切换 class 来改变元素的状态,然后获取到对应状态下的大小(或者将菜单第一个子元素的大小看做收起态的大小,下面代码就是这样做的)。

需要注意的是当我们执行 getBoundingClientRect()(或者 offsetWidth 和 offsetHeight)时,如果页面当前样式有变化,浏览器会强制重排。

function calculateCollapsedScale () {
  // The menu title can act as the marker for the collapsed state.
  const collapsed = menuTitle.getBoundingClientRect();

  // Whereas the menu as a whole (title plus items) can act as
  // a proxy for the expanded state.
  const expanded = menu.getBoundingClientRect();
  return {
    x: collapsed.width / expanded.width,
    y: collapsed.height / expanded.height  }}

菜单默认状态下是展开的,通过上面的代码可以得到菜单收起态和展开态的缩放比例,然后使用 JavaScript 修改元素的 tansform 属性,这样就可以实现展开和收起动画。

.menu {
  will-change: transform;
  transition: 200ms linear;}
var { x, y } = calculateCollapsedScale();var invX = 1 / x;var invY = 1 / y;function toggle() {
  if (menu.classList.contains('expanded')) {
    collapse();
    return;
  }

  expand();}function collapse() {
  menu.classList.remove('expanded');
  menu.style.transform = `scale(${x}, ${y})`;
  menuContents.style.transform = `scale(${invX}, ${invY})`;}function expand() {
  menu.classList.add('expanded');
  menu.style.transform = `scale(1, 1)`;
  menuContents.style.transform = `scale(1, 1)`;}menuBtn.addEventListener('click', toggle);

从效果看可以发现虽然菜单的收起态和展开态没有问题,但是在变换过程中内容看起来变形了。因为我们是通过容器的缩放和内容的反向缩放(counter-scale)来实现动画的,要保证内容在动画过程中不变形,就要保证容器的缩放比例和内容的缩放比例在动画过程中始终保持相乘等于1。我们用高度举例,收起态的高度是展开态的 1/5(0.2),所以收起态下内容的高度是放大了5倍,那么展开动画就是容器高度缩放比例从 0.2 -> 1,内容高度缩放比例从 5 -> 1。假设缓动函数是线性的,我们把过程分成五个阶段,用表格表示:

容器高度缩放比例内容高度缩放比例0.250.440.630.8211
所以我们可以看到,在整个动画过程中,容器高度缩放比例 * 内容高度缩放比例 > 1,这也就是解释了为什么我们看到的动画中内容变形了。

第二步:构造CSS动画

针对上面的问题,我们可以使用 CSS 动画解决,因为 CSS 动画是基于关键帧,因此我们可以将动画过程分成 100 个关键帧,计算出每一个关键帧下容器和内容的缩放比例,并保证在这 100 个关键帧中这两个比例相乘始终等于 1,接着将这 100 个关键帧拼接成 CSS 动画并插入到页面中给元素调用。这样我们就可以保证在动画过程中不会出现变形,并且一开始就完成所有的计算量,避免了在 JavaScript 中动态计算,从而避免了由于 JavaScript 阻塞导致的动画卡顿。

将 CSS 动画插入到页面中会导致浏览器重新渲染页面样式,但这个过程只会在最开始执行一次,所以影响并不大。下面是通过 JavaScript 生成 CSS 动画的代码

function createKeyframeAnimation () {
  // Figure out the size of the element when collapsed.
  let {x, y} = calculateCollapsedScale();
  let animation = '';
  let inverseAnimation = '';

  for (let step = 0; step <= 100; step++) {
    // Remap the step value to an eased one.
    let easedStep = ease(step / 100);

    // Calculate the scale of the element.
    const xScale = x + (1 - x) * easedStep;
    const yScale = y + (1 - y) * easedStep;

    animation += `${step}% {
      transform: scale(${xScale}, ${yScale});
    }`;

    // And now the inverse for the contents.
    const invXScale = 1 / xScale;
    const invYScale = 1 / yScale;
    inverseAnimation += `${step}% {
      transform: scale(${invXScale}, ${invYScale});
    }`;

  }

  return `
  @keyframes menuAnimation {
    ${animation}
  }

  @keyframes menuContentsAnimation {
    ${inverseAnimation}
  }`;}

代码中的 ease() 表示缓动函数,在实际应用中,我们可以如下一样自己定义一个,或者使用 Tween.js 这样的项目。

function ease (v, pow=4) {
  return 1 - Math.pow(1 - v, pow);}

震惊!谷歌竟然能看函数曲线!So Google It!

第三步:执行 CSS 动画

通过前两步我们已经生成了 CSS 动画并且插入到了页面中,接下来我们通过切换元素的 class 来触发动画。

.menu--expanded {
  animation-name: menuAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;}.menu__contents--expanded {
  animation-name: menuContentsAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;}

结合上面的 CSS 代码,使用 JavaScript 切换元素的 class 就可以触发元素执行动画。这里要注意在 JavaScript 中我们已经引入了缓动函数,所以在 CSS 代码中需要将 animation-timing-function 属性设置为 linear,如果设置成其他值的就相当于在我们原先的缓动基础上再叠加一次缓动效果。

现在我们搞定了展开动画,对于收起动画可以将展开动画逆向执行,这个方法看起来是没有问题的,但是因为是完全逆向执行的,所以假设我们展开动画缓动函数是 ease-out ,那么收起动画看起来就是 ease-in,所以视觉效果上会有一些区别。所以更好的方案是在 JavaScript 中生成两套 CSS 动画,分别是展开动画和收起动画,这样就可以保证两个动画的关键帧是遵循同样的缓动函数。

const xScale = 1 + (x - 1) * easedStep;const yScale = 1 + (y - 1) * easedStep;

再进一步:圆形菜单

现在可以用同样的原理实现一个圆形的菜单动画,其实代码层面基本是一致的。只是圆形这种情况下我们需要设置一个 border-raidu: 50% 来实现圆形,然后用一个设置了 overflow: hidden 的元素将菜单包裹起来,这样就可以在菜单展开后看起来是一个矩形。

当然,这里还涉及一些其他样式上的修改,因为在圆形彩蛋动画中变换中心并不是左上角或者中心,而且点击后加号按钮会消失,所以还需要注意这些细节。在低 DPI 的屏幕上 Chrome 浏览器会出现文字模糊的 bug,具体的 bug 说明在这里。圆形菜单动画代码戳这里;

总结

现在我们已经实现了一种高性能动画方案,原理是将容器和内容同时缩放并且保证两个缩放比例相乘等于 1。具体的过程是先通过 JavaScript 计算出 100 个关键帧并拼接成 CSS 动画,然后触发元素执行 CSS 动画即可。

如果浏览器支持 Web Animation 的话可以直接调用 Web Animation API 执行动画,但问题是 Web Animation 的兼容性不太好,所以使用的话需要做如下兼容处理。

if ('animate' in HTMLElement.prototype) {
  // Animate with Web Animations.} else {
  // Fall back to generated CSS Animations or JS.}

想要看具体的实现代码,移步 UI Element Samples Github 仓库。

说说梦想,谈谈感悟 ,聊聊技术,有啥要说的来github留言吧 https://github.com/cjx2328

—— 陈 建鑫

陈建鑫
footer logo
未经许可请勿自行使用、转载、修改、复制、发行、出售、发表或以其它方式利用本网站之内容。站长联系:cjx2328#126.com(修改#为@)
Copyright ©ziao Studio All Rights Reserved. E-mail:cjx2328#126.com(#号改成@) 沪ICP备14052271号-3