动画将用户界面带入生活,UI 动画在用户体验方面是重要的一环,特别是移动端开发的同学更清楚这一点。
本文将介绍一种你可能熟悉的某一类有意义的增强用户体验的 UI 动效。这种技术有一个专业术语,即 FLIP
(First, Last, Invert, Play)。
FLIP 技术可以以一种高性能的方式来动态的改变 DOM 元素的位置和尺寸,而不需要管它的布局是如何计算或渲染的(比如,height
、width
、float
、position
、Flexbox
和 Grid
等)。在改变的过程中将赋予一定的动效,从而达到我们所需要的目的,让 UI 动效更为合理,相应增强用户的体验。
FLIP 的实际应用
下图是知乎的图片查看功能,一个看似很简单的动画,其实就运用了 FLIP 技术。
具体策略
FLIP 其实我们可以理解为将动画倒放,而不是直接过渡。因为动画在过渡时,浏览器对每帧动画都需要进行昂贵的计算,而我们通过动态预计算的方式让浏览器倒放动画,可以使动画更流畅地被完成。
FLIP 代表 First、Last、Invert 和 Play。
它们分别是:
First:元素参与 transtion
的初始状态。
Last:动画开始前我们需要让元素先变成最终状态。
Invert:元素变成最终状态后,我们需要利用 transfrom
或 opacity
等属性来反转它们。比如最终状态的元素需要由初始位置沿 Y 轴向下移动 90 px
,我们的做法便是:先让元素变成最终状态,然后让元素在 Y 轴向上移动 90px
:transform: translateY(-90px)
。这种反转会让元素看起来仍然在初始的位置,所以元素并没有达到最终的位置。
Play:最后一步,为我们之前改变的属性开启过渡,然后移除反转的属性即可。因为在移除反转的属性之前,元素已经在它的最终位置了,所以这会缓解从初始位置到最终位置的计算量,从而达到性能优化的目的。
代码实现
First
首先我们需要做的就是获取动画处于第一帧时,元素的状态。
const fisrtDom = params.fisrtDom;
const fisrtReact = fisrtDom.getBoundingClientRect();
console.log( fisrtReact )
Last
第二步,我们需要做的就是获取动画处于最后一帧时,元素的状态。
const lastDom = params.lastDom;
const lastReact = lastDom.getBoundingClientRect();
console.log( lastReact );
Invert
获取到两个状态的数据之后,直接进行差值计算。
从知乎的截图上可以看到,要实现这个动画,是需要两张图片的。
鼠标点击图片时,第一步先复制一张大图,在屏幕中绝对定位,垂直水平居中,然后将大图缩小到和原图一模一样,并且能够完美覆盖原图,两张图片叠在一起,看起来就只有一张图片。
第二步就是, 将大图从缩小状态逐渐还原回放大状态。
这个效果实现起来的难点就在于如何定位复制出来的图片, 保证图片能够覆盖原图, 并且和原图一样大小。
创建一张大图,并且将大图缩小到和原图一样大小
下面的代码中使用了 MutationObserver
用来监听图片属性的更新
img.onclick = () => {
let photo = null, srcChange = false;
photo = document.createElement('img');
photo.className = 'photo';
photo.src = d.src;
ptCtrl.appendChild(photo);
const mo = new MutationObserver(firstDone);
mo.observe(photo, {
attributes: true
});
first = d.getBoundingClientRect();
const last = photo.getBoundingClientRect();
scale = first.width / last.width;
srcChange = true;
photo.style.cssText = `transform: scale(${scale})`;
}
计算缩小后的大图第一帧在屏幕中的位置
如何将缩小后的大图完美覆盖到原图上方, 这就需要借助 translate
的 X 轴与 Y 轴的移动来完成了:
-
X 轴的计算是:
first.left - last.left
-
Y 轴的计算是:
first.top - last.top
Play
计算完成后,直接利用 WAAPI
的 animate
接口完成动画,也不需要改变 style
属性什么的,非常省心。
function firstDone() {
if (srcChange) {
srcChange = false;
const last = photo.getBoundingClientRect(), x = first.left - last.left, y = first.top - last.top;
transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
photo.animate([
{transform},
{transform: 'translate3d(0, 0, 0) scale(1)'}
], {
duration: 300,
easing: 'cubic-bezier(0,0,0.32,1)',
fill: 'forwards'
});
}
}
放大后再次缩小图片
当图片被放大时,再次点击大图,图片需要缩小到原位置,其实只要完成了上面的步骤,最后这一步就很简单了。
还是利用 animate
接口,直接将参数倒着来一遍就完事了。然后监听 finish
事件,结束时销毁复制出来的 img
节点。
img.onclick = () => {
const player = photo.animate([
{transform: 'translate3d(0, 0, 0) scale(1)'},
{transform}
], {
duration: 300,
easing: 'cubic-bezier(0,0,0.32,1)',
fill: 'forwards'
});
player.addEventListener('finish', () => {
photo.parentNode.removeChild(photo);
});
}
最后,关于 Animate API 的一些相关知识,可以查看以下链接: