header-bg.jpg
在 JavaScript 中利用 FLIP 技术制作卡片弹出动画
发表于 2019-05-18 02:43
|
分类于 JavaScript
|
评论次数 0
|
阅读次数 560

动画将用户界面带入生活,UI 动画在用户体验方面是重要的一环,特别是移动端开发的同学更清楚这一点。

本文将介绍一种你可能熟悉的某一类有意义的增强用户体验的 UI 动效。这种技术有一个专业术语,即 FLIP (First, Last, Invert, Play)。

FLIP 技术可以以一种高性能的方式来动态的改变 DOM 元素的位置和尺寸,而不需要管它的布局是如何计算或渲染的(比如,heightwidthfloatpositionFlexboxGrid 等)。在改变的过程中将赋予一定的动效,从而达到我们所需要的目的,让 UI 动效更为合理,相应增强用户的体验。

FLIP 的实际应用

下图是知乎的图片查看功能,一个看似很简单的动画,其实就运用了 FLIP 技术。

trigger.gif

具体策略

FLIP 其实我们可以理解为将动画倒放,而不是直接过渡。因为动画在过渡时,浏览器对每帧动画都需要进行昂贵的计算,而我们通过动态预计算的方式让浏览器倒放动画,可以使动画更流畅地被完成。

FLIP 代表 FirstLastInvertPlay

它们分别是:

First:元素参与 transtion 的初始状态。

Last:动画开始前我们需要让元素先变成最终状态。

Invert:元素变成最终状态后,我们需要利用 transfromopacity 等属性来反转它们。比如最终状态的元素需要由初始位置沿 Y 轴向下移动 90 px,我们的做法便是:先让元素变成最终状态,然后让元素在 Y 轴向上移动 90pxtransform: 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 轴的移动来完成了:

Play

计算完成后,直接利用 WAAPIanimate 接口完成动画,也不需要改变 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 的一些相关知识,可以查看以下链接:

Animate API

animista

发布评论
还没有评论,快来抢沙发吧!