利用FLIP技术制作卡片弹出动画

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

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

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

FLIP 的实际应用

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

trigger.gif

具体策略

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

FLIP 代表 First、Last、Invert、Play。

它们分别是:

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

Last:元素的最终状态。

Invert:这里有点意思。你弄清楚了元素从开始到结束是如何改变的,如它的 widthheightopacity。下一步,你应用 transformopacity 的变化来扭转或反转它们。如果元素已经向下移动到初始和结束状态之间的 90 px,然后在 Y 轴上应用 transform -90px。这让元素看起来仍然在初始的位置,所以元素并没有达到最终的位置。

Play:为你之前改变的属性开启过渡,然后移除反转的改变。因为当移除 transformopacity 时,元素都在它们的最终位置。这会缓解从伪造的初始位置到最终位置的计算量。

代码实现

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 轴的移动来完成了:

计算完成后,直接利用 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

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