You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
拿到了这个需求,第一直觉是怎么做?假设第一行第一个图片移动到了第二行第三列,是不是要计算出第一行的高度,再计算出第二行前两个元素的宽度,然后从初始的坐标点通过 CSS 或者一些动画 API 移动过去?这样做是可以,但是在图片不定高不定宽,并且一次要移动很多图片情况下,这个计算方法就非常复杂了。并且这种情况下,图片的坐标都需要我们手动管理,非常不利于维护和扩展。
换种思路,能不能直接很自然的把 DOM 元素通过原生 API 添加到 DOM 树中,然后让浏览器帮我们好这个终点值,最后我们再动画位移过去?
首先随机的取出几张图片作为待放入数组的元素,利用 new Image 预加载这些图片,防止渲染一堆空白图片到屏幕上。
然后定义一个计算一组 DOM 元素位置的函数 getRects,利用 getBoundingClientRect 可以获得最新的位置信息,这个方法在接下来获取图片元素旧位置和新位置时都要使用。
functiongetRects(doms){returndoms.map((dom)=>{constrect=dom.getBoundingClientRect()const{ left, top }=rectreturn{ left, top }})}// 当前已有的图片constprevImgs=this.$refs.imgs.slice()constprevPositions=getRects(prevImgs)
记录完图片的旧位置后,就可以向数组里追加新的图片了:
this.imgs=newData.concat(this.imgs)
随后就是比较关键的点了,我们知道 Vue 是异步渲染的,也就是改变了这个 imgs 数组后不会立刻发生 DOM 的变动,此时我们要用到 nextTick 这个 API,这个 API 把你传入的回调函数放进了 microTask 队列,正如上文提到的事件循环的文章里所说,microTask队列的执行一定发生在浏览器重新渲染前。
前言
在 Vue 的官网中的过渡动画章节中,可以看到一个很酷炫的动画效果
乍一看,让我们手写出这个逻辑应该是非常复杂的,先看看本文最后要实现的效果吧,和这个案例是非常类似的。
预览
也可以直接进预览网址里看:
http://sl1673495.gitee.io/flip-animation
图片素材依然引用自知乎问题《有个漂亮女朋友是种怎样的体验?》,侵删。
分析需求
拿到了这个需求,第一直觉是怎么做?假设第一行第一个图片移动到了第二行第三列,是不是要计算出第一行的高度,再计算出第二行前两个元素的宽度,然后从初始的坐标点通过 CSS 或者一些动画 API 移动过去?这样做是可以,但是在图片不定高不定宽,并且一次要移动很多图片情况下,这个计算方法就非常复杂了。并且这种情况下,图片的坐标都需要我们手动管理,非常不利于维护和扩展。
换种思路,能不能直接很自然的把 DOM 元素通过原生 API 添加到 DOM 树中,然后让浏览器帮我们好这个终点值,最后我们再动画位移过去?
在文档里我们发现一个名词:
FLIP
,这给了我们一个线索,是不是用这个玩意就可以写出这个动画呢?答案是肯定的,顺着这个线索找到
Aerotwist
社区里的一篇文章:flip-your-animations,以这篇文章为切入点,一步步来实现一个类似的效果。FLIP
FLIP
究竟是什么东西呢?先看下它的定义:First
即将做动画的元素的初始状态(比如位置、透明度等等)。
Last
即将做动画的元素的最终状态。
Invert
这一步比较关键,假设我们图片的初始位置是
左: 0, 上:0
,元素动画后的最终位置是左:100, 上100
,那么很明显这个元素是向右下角运动了100px
。但是,此时我们不按照常规思维去先计算它的最终位置,然后再命令元素从
0, 0
运动到100, 100
,而是先让元素自己移动过去(比如在 Vue 中用数据来驱动,在数组前面追加几个图片,之前的图片就自己移动到下面去了)。这里有一个关键的知识点要注意了,也是我在之前的文章《深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调》中提到过的:
DOM 元素属性的改变(比如
left
、right
、transform
等等),会被集中起来延迟到浏览器的下一帧统一渲染,所以我们可以得到一个这样的中间时间点:DOM 状态(位置信息)改变了,而浏览器还没渲染。有了这个前置条件,我们就可以保证先让 Vue 去操作 DOM 变更,此时浏览器还未渲染,我们已经能得到 DOM 状态变更后的位置了。
说的具体点,假设我们的图片是一行两个排列,图片数组初始化的状态是
[img1, img2
,此时我们往数组头部追加两个元素[img3, img4, img1, img2]
,那么img1
和img2
就自然而然的被挤到下一行去了。假设
img1
的初始位置是0, 0
,被数据驱动导致的 DOM 改变挤下去后的位置是100, 100
,那么此时浏览器还没有渲染,我们可以在这个时间点把img1.style.transform = translate(-100px, -100px)
,让它 先 Invert 倒置回位移前的位置。Play
倒置了以后,想要让它做动画就很简单了,再让它回到
0, 0
的位置即可,本文会采用最新的Web Animation API
来实现最后的Play
。MDN 文档:Web Animation
实现
首先图片渲染很简单,就让图片通过简单的排成 4 列即可:
那么关键点就在于怎么往这个
imgs
数组里追加元素后,做一个流畅的路径动画。我们来实现追加图片的方法
add
:首先随机的取出几张图片作为待放入数组的元素,利用
new Image
预加载这些图片,防止渲染一堆空白图片到屏幕上。然后定义一个计算一组 DOM 元素位置的函数
getRects
,利用getBoundingClientRect
可以获得最新的位置信息,这个方法在接下来获取图片元素旧位置和新位置时都要使用。记录完图片的旧位置后,就可以向数组里追加新的图片了:
随后就是比较关键的点了,我们知道 Vue 是异步渲染的,也就是改变了这个
imgs
数组后不会立刻发生 DOM 的变动,此时我们要用到nextTick
这个 API,这个 API 把你传入的回调函数放进了microTask
队列,正如上文提到的事件循环的文章里所说,microTask
队列的执行一定发生在浏览器重新渲染前。由于先调用了
this.imgs = newData.concat(this.imgs)
这段代码,触发了 Vue 的响应式依赖更新,此时 Vue 内部会把本次 DOM 更新的渲染函数先放到microTask
队列中,此时的队列是[changeDOM]
。调用了
nextTick(callback)
后,这个callback
函数也会被追加到队列中,此时的队列是[changeDOM, callback]
。这下聪明的你肯定就明白了,为什么
nextTick
的回调函数里一定能获取到最新的 DOM 状态。由于我们之前保存了图片元素节点的数组
prevImgs
,所以在nextTick
里调用同样的getRect
方法获取到的就是旧图片的最新位置了。此时我们已经拥有了
Invert
步骤的关键信息,新位置和旧位置,那么接下来就很简单了,把图片数组循环做一个倒置后Play
的动画即可。此时一个非常流畅的路径动画效果就完成了。
完整实现如下:
乱序
现在我们想要实现官网 demo 中的
shuffle
效果,有了追加图片逻辑的铺垫,是不是已经觉得思路如泉涌了?没错,即使图片被打乱的再厉害,只要我们有「图片开始时的位置」和「图片结束时的位置」,那就可以轻松做到路径动画。现在我们需要做的是把动画的逻辑抽离出来,我们分析一下整条链路:
保存旧位置 -> 改变数据驱动视图更新 -> 获得新位置 -> 利用 FLIP 做动画
其实外部只需要传入一个
update
方法告诉我们如何去更新图片数组,就可以把这个逻辑完全抽象到一个函数里去。那么追加图片和乱序的函数就变得非常简单了:
源码地址
https://github.com/sl1673495/flip-animation
总结
FLIP
FLIP 不光可以做位置变化的动画,对于透明度、宽高等等也一样可以很轻松的实现。
比如电商平台中经常会出现一个动画,点击一张商品图片后,商品从它本来的位置慢慢的放大成了一张完整的页面。
FLIP
的思路掌握后,只要你知道元素动画前的状态和元素动画后的状态,你都可以轻松的通过「倒置状态」后,让它们做一个流畅的动画后到达目的地,并且此时的 DOM 状态是很干净的,而不是通过大量计算的方式强迫它从0, 0
位移到100, 100
,并且让 DOM 样式上留下transform: translate(100px, 100px)
类似的字样。Web Animation
利用
Web Animation API
可以让我们用 JavaScript 更加直观的描述我们需要元素去做的动画,想象一下这个需求如果用 CSS 来做,我们大概会这样去完成这个需求:这也是 Vue 内部
transition-group
组件实现FLIP
动画的大致思路,Vue 应该是为了兼容性和代码体积等一些方面的权衡,还是选择用比较原生的方式去实现 FLIP 动画,这段代码让我觉得不舒服的点在于:class
的增加和删除来和 CSS 来进行交互,整体流程不太符合直觉。document.body.offsetHeight
这样的方式触发强制同步布局
,比较 hack 的知识点。this._reflow = document.body.offsetHeight
这样的方式向元素实例上增加一个没有意义的属性,防止被 Rollup 等打包工具tree-shaking
误删。 比较 hack 的知识点 +1。而利用
Web Animation API
的代码则变得非常符合直觉和易于维护:关于兼容性问题,W3C 已经提供了
Web Animation API Polyfill
,可以放心大胆的使用。期待在不久的未来,我们可以抛弃旧的动画模式,迎接这种更新更好的 API。
希望这篇文章能让对动画发愁的你有一些收获,谢谢!
❤️ 感谢大家
1.如果本文对你有帮助,就点个赞支持下吧,你的「赞」是我创作的动力。
2.关注公众号「前端从进阶到入院」即可加我好友,我拉你进「前端进阶交流群」,大家一起共同交流和进步。
The text was updated successfully, but these errors were encountered: