【第1542期】让动画变得更简单之FLIP技术
前言
一开始被FLIP所吸引,一查原来15年就出来了。今日早读文章由转转@KQ授权分享。
@KQ,来自转转的一名前端,喜好挑战技术难题,崇尚实用主义
正文从这开始~~
某次被问到如何实现以下动画效果:
若干个元素卡片从上而下排列,当增加或删除某个卡片的时候,其余的卡片会以一种 transition动画的形式移动到适当的位置上,而不是生硬地闪现
当时我恰好看过 Vue中的内置组件 transition的实现,意识到完全可以用 transition组件的部分原理来完成这个效果,但是由于没有深入地探究过为什么是这样,只停留在表面,知其然而不知其所以然,所以尽管我知道如何实现这个效果,但很难解释为什么是这样,语言组织地比较困难
后来我无意间看到一篇文章 FLIP技术给Web布局带来的变化,立马恍然大悟,原来这个东西叫 FLIP
FLIP
FLIP是 First、Last、Invert和 Play四个单词首字母的缩写
First,指的是在任何事情发生之前(过渡之前),记录当前元素的位置和尺寸,即动画开始之前那一刻元素的位置和尺寸信息,可以使用 getBoundingClientRect()这个 API来处理(大部分情况下其实 offsetLeft和 offsetTop也是可以的)
Last:执行一段代码,让元素发生相应的变化,并记录元素在动画最后状态的位置和尺寸,即动画结束之后那一刻元素的位置和尺寸信息
Invert:计算元素第一个位置(First)和最后一个位置(Last)之间的位置变化(如果需要,还可以计算两个状态之间的尺寸大小的变化),然后使用这些数字做一定的计算,让元素进行移动(通过 transform来改变元素的位置和尺寸),从而创建它位于第一个位置(初始位置)的一个错觉
即,一上来直接让元素处于动画的结束状态,然后使用 transform属性将元素反转回动画的开始状态(这个状态的信息在 First步骤就拿到了)
Play:将元素反转(假装在first位置),我们可以把 transform设置为 none,因为失去了 transform的约束,所以元素肯定会往本该在的位置(即动画结束时的那个状态)进行移动,也就是last的位置,如果给元素加上 transition的属性,那么这个过程自然也就是以一种动画的形式发生了
按照我的理解,就是对动画元素起止状态的一个量化,量化成一个公式,绝大部分的连续动画都可以通过套用这个公式来完成,提升动画的开发效率,更加详细的请自行参见 FLIP技术给Web布局带来的变化
实现卡片 Card增删动画
了解了 FLIP这个概念之后,再来实现开头提到的那个动画效果,其实就很简单了
First
记录在动画开始之前每个卡片的位置和尺寸信息,这里因为卡片的尺寸在动画过程中其实是不会发生任何变化的, 所以可以略过这一步,只记录卡片的位置信息
另外,如果所有卡片的尺寸都是相同的,那么也无需记录所有卡片的位置信息,因为无论是插入卡片还是删除卡片,都只有那些位于位置坐标在变化卡片坐标的后面的卡片才会受到影响的,前面的是不会变的
// First
activeList.forEach((itemEle, index) => {
rectInfo = itemEle.getBoundingClientRect()
transArr[index + stepIndex][0] = rectInfo.left
transArr[index + stepIndex][1] = rectInfo.top
})
Last
动画的结束状态,其实就是增加或者删除了卡片之后,其余卡片的状态:
if (updateStatus === 0) {
// 增加卡片
newListData = this.state.listData.slice(0, activeIndex).concat({
index: cardIndex++
}, this.state.listData.slice(activeIndex))
} else {
// 删除卡片
newListData = this.state.listData.filter((value, index) => index !== activeIndex)
}
因为这个时候没给卡片加 transition属性,所以卡片数量更新这个过程,其实就是一瞬间的事情,人眼是无法察觉到任何变化的,但是页面上的元素确实是发生了变化,然后此时测量卡片的位置信息,即 Last所需要的数据
Invert
获取了动画起始阶段受影响的卡片的位置信息后,就可以通过 transform属性对元素的位置进行反转了
// Last + Invert
const stepIndex = updateStatus === 0 ? 1 : 0
activeList.forEach((itemEle, index) => {
rectInfo = itemEle.getBoundingClientRect()
transArr[index + stepIndex][0] = transArr[index + stepIndex][0] - rectInfo.left
transArr[index + stepIndex][1] = transArr[index + stepIndex][1] - rectInfo.top
}
Play
准备阶段就绪,就可以进行最后一步 Play起来了,这一步的关键就是给元素加上 transition属性,并移除 transform给元素带来的位置变化:
// Play
// 重置
transArr = getArrByLen(this.state.listData.length)
setTimeout(() => {
this.setState({
animateStatus: 3
})
}, 0)
因为浏览器会对页面的 DOM变化进行合并优化,所以为了能在视觉上呈现出想要的动画效果,这里必须要打断这种优化,setTimeout是一个很常用的方式
到此为止,就完成了文章开头的那个动画效果,我做了个 Live Demo,有兴趣的可以亲自试下,另外代码也可以上传到 Github
实现图片放大/恢复动画
微信app里聊天界面点击预览图片时,图片从对话框到全屏预览的这个过程,用了一个过渡的动画,呈现出图片从小图到大图和从大图恢复到小图的全过程,缩放过程类似于下面这种:
这种也属于连续动画,当然也可以通过 FLIP来轻松实现
First
这里涉及到图片的位置和尺寸的变化,图片从 First的小图原位置和小图尺寸,变成了 Last状态下的大图位置和大图尺寸,所以需要同时获取这两个数据,其实都是可以通过一次调用 getBoundingClientRect完成
Last
获取图片已经处于预览状态下的尺寸和位置信息,同样使用 getBoundingClientRect完成
另外,为了更好地利用 transform动画,我这里将图片两个状态下的尺寸变化转变为 scale值的变化,First与 Last状态下宽度或者高度的比例就是这个 scale的应当取值(在没有改变图片宽高比例的前提下)
scaleValue = rectInfo.width / lastRectInfo.width
Invert
使用 transform进行位置和尺寸(即改变 scale值)的反转
这里有一点需要注意的是,由于 transform动画默认的 transform-origin为元素的中心,即50% 50%,但是计算出来的 left和 top却是相对于没有缩放的图片而言的,所以当 scale取值不唯一时,图片动画的 First状态就会发生偏差,需要将 transform设为 0 0以消除这种偏差
Play
为图片添加 transition属性,并移除相关 transform属性,即可启动动画
可以看到,套用了 FLIP之后,原本看起来比较棘手的一个动画,被轻易模式化实现了
至于放大后的图片恢复到小图这一个阶段,可以看成是另外一个 FLIP动画,继续套用即可,只不过这个动画就是上一个放大动画的逆向,所需的尺寸和位置信息都已经拿到了,可以省去调用 getBoundingClientRect的过程
同样做了个 Live Demo:https://accforgit.github.io/flip/index.html?showIndex=1,有兴趣的可以亲自试下,另外代码也可以上传到 Github
小结
很多前端同学似乎不太在意动画,认为这只是一个辅助能力,业务逻辑才是最重要的,其他的全都靠后站,就算是有时间也要看心情再决定搞不搞
我的看法是,业务逻辑当然是要放在首位的,但是同样也不要小看了其余的细枝末节,例如动画,一个体验良好的动效完全可以吸引用户的更多停留,以一种通用的方式从侧面提升业务的转化效果,某些特定场景下,其所能起到的作用甚至可以与业务的目标并驾齐驱
关于本文
作者:@KQ
原文:https://juejin.im/post/5c5258ffe51d45299a08e012
最后,为你推荐
【第1433期】CSS3动画实战之多关键帧实现无限循环动效的时间间隔
2月份文章Top3推荐
【第1525期】Vuex、Flux、Redux、Redux-saga、Dva、MobX
欢迎读者投稿
邮箱:181422448@qq.com
微信:zhgb_f2er