使用FLIP实现高性能动画

marvin

marvin

使用FLIP实现高性能动画

什么是 FLIP

该技术可用于以有效的方式对任何DOM元素的位置和尺寸进行动画处理,而不管其布局是如何计算或呈现的, 它的过程是这样的:

  1. FIRST: 在任何事情发生之前,记录将要转换的元素的当前位置和尺寸
  2. LAST: 执行使过渡瞬间发生的代码,并记录元素的最终位置和尺寸
  3. INVERSE: 由于元素位于最后一个位置,我们想通过transform修改其位置和尺寸来创建它位于第一个位置的错觉。这需要一点数学运算,但并不难。
  4. PLAY: 元素反转(并假装在第一个位置),我们可以通过将其设置为transform来将其移回到最后一个位置

以下是使用Web Animations API来实施这些步骤的例子:

const elm = document.querySelector('.some-element');

// 获取当前的位置和尺寸对象
const first = elm.getBoundingClientRect();

// 执行一些会引起布局发生变化的事情
doSomething();

// 获取当前元素最终的位置和尺寸对象
const last = elm.getBoundingClientRect();

// 确定当前和最终位置之间的增量,以反转元素
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
const deltaW = first.width / last.width;
const deltaH = first.height / last.height;

// 播放动画, 从当前位置到最终位置
elm.animate([{
  transformOrigin: 'top left',
  transform: `
    translate(${deltaX}px, ${deltaY}px)
    scale(${deltaW}, ${deltaH})
  `
}, {
  transformOrigin: 'top left',
  transform: 'none'
}], {
  duration: 300,
  easing: 'ease-in-out',
  fill: 'both'
});

虽然并非所有浏览器都支持 Web 动画 API, 但我们可以使用 https://github.com/web-animations/web-animations-js , 如果元素的大小已更改,则可以进行变换scale以“调整”其大小,而不会影响性能;但是,请确保设置transformOrigin: 'top left'因为这是我们基于增量计算的地方

共享元素过渡

在应用视图和状态之间转换元素的一种常见用例是,最终元素可能与初始元素不是同一DOM元素, 尽管如此,我们仍然可以通过一个翻转过渡假象来实现:

const firstElm = document.querySelector('.first-element');

// 获取当前元素的位置和尺寸对象,隐藏元素
const first = firstElm.getBoundingClientRect();
firstElm.style.setProperty('visibility', 'hidden');

// 执行一些会引起布局发生变化的事情
doSomething();

// 获取新的元素的位置和尺寸对象
const lastElm = document.querySelector('.last-element');
const last = lastElm.getBoundingClientRect();

// 其他的步骤和 FLIP 的步骤一样

亲子过渡

在以前的实现中,元素范围基于window。对于大多数用例来说,这很好,但是请考虑以下情形:

  • 元素会改变位置,需要转换。
  • 该元素包含一个子元素,该子元素本身需要过渡到父元素内的其他位置。

由于先前计算的范围是相对于window,因此我们对子元素的计算将不可用。为了解决这个问题,我们需要确保相对于父元素计算位置和尺寸:

const parentElm = document.querySelector('.parent');
const childElm = document.querySelector('.parent > .child');

// 计算父元素和子元素当前的位置和尺寸
const parentFirst = parentElm.getBoundingClientRect();
const childFirst = childElm.getBoundingClientRect();

// 执行一些会引起布局发生变化的事情
doSomething();

// 计算父元素和子元素最终的位置和尺寸
const parentLast = parentElm.getBoundingClientRect();
const childLast = childElm.getBoundingClientRect();

// 翻转父元素
const parentDeltaX = parentFirst.left - parentLast.left;
const parentDeltaY = parentFirst.top - parentLast.top;

// 相对于父元素翻转子元素
const childDeltaX = (childFirst.left - parentFirst.left)
  - (childLast.left - parentLast.left);
const childDeltaY = (childFirst.top - parentFirst.top)
  - (childLast.top - parentLast.top);
  
// 播放动画
parentElm.animate([
  { transform: `translate(${parentDeltaX}px, ${parentDeltaY}px)` },
  { transform: 'none' }
], { duration: 300, easing: 'ease-in-out' });

childElm.animate([
  { transform: `translate(${childDeltaX}px, ${childDeltaY}px)` },
  { transform: 'none' }
], { duration: 300, easing: 'ease-in-out' });

这里还可以将共享元素过渡和亲子过渡结合使用, 以获得更大的灵活性

使用Flipping.js降低复杂性

上面的技术看似简单明了,但是一旦您必须跟踪多个元素的转换,它们就会变得很乏味, 而 flippingl.js 帮我们处理了这些问题, 通过向元素添加 data-flip-key 属性, 就可以预测和有效地跟踪多个元素, 例如,考虑以下html结构:

<section class="gallery">
	<div class="photo-1" data-flip-key="photo-1">
    <img src="/photo-1">
	</div>
	<div class="photo-2" data-flip-key="photo-2">
    <img src="/photo-2">
	</div>
	<div class="photo-3" data-flip-key="photo-3">
    <img src="/photo-3">
	</div>
</section>
<section class="details">
	<div class="photo" data-flip-key="photo-1">
	  <img src="/photo-1">
	</div>
	Lorem ipsum dolor sit amet...
</section>

在这里,有2个元素具有相同的data-flip-key="photo-1". Flipping.js通过选择满足以下条件的第一个元素来进行跟踪:

  • 元素存在于DOM中
  • 元素不是隐藏的(对于隐藏的元素得到的 getBoundingClientRect 中的 width, height 均为0)
  • 在 selectActive 选项中指定有任意的自定义逻辑

Flipping.js入门

安装

我们可以直接从以下CDN 获取压缩过后的代码:

或者也可以通过

yarn add flipping

命令安装到项目中

使用

当添加了 data-flip-key 属性之后, 我们可以直接执行:

import Flipping from 'flipping/adapters/web';

const flipping = new Flipping();

// 读取所有初始化的位置和尺寸信息
flipping.read();

// 执行一些会引起布局发生变化的事情
doSomething();

// 执行反转, 并播放动画
flipping.flip();

处理由于函数调用而导致的FLIP转换是一种常见的模式, 因此 flipping.js 提供了一种更加简洁的方式:

const flipping = new Flipping();

const flippingDoSomething = flipping.wrap(doSomething);

// 这里会依次执行 read, doSomething, flip 函数
flippingDoSomething();

这里我们只需要传入一个函数, flipping.js 会将其包装成一个调用函数供我们调用