使用FLIP实现高性能动画

marvin

什么是 FLIP
该技术可用于以有效的方式对任何DOM元素的位置和尺寸进行动画处理,而不管其布局是如何计算或呈现的, 它的过程是这样的:
- FIRST: 在任何事情发生之前,记录将要转换的元素的当前位置和尺寸
- LAST: 执行使过渡瞬间发生的代码,并记录元素的最终位置和尺寸
- INVERSE: 由于元素位于最后一个位置,我们想通过transform修改其位置和尺寸来创建它位于第一个位置的错觉。这需要一点数学运算,但并不难。
- 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 获取压缩过后的代码:
- https://unpkg.com/flipping@latest/dist/flipping.js
- https://unpkg.com/flipping@latest/dist/flipping.web.js
- https://unpkg.com/flipping@latest/dist/flipping.gsap.js
或者也可以通过
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 会将其包装成一个调用函数供我们调用