某次被问到如何实现如下动画效果:javascript
若干个元素卡片从上而下排列,当增长或删除某个卡片的时候,其他的卡片会以一种 transition
动画的形式移动到适当的位置上,而不是生硬地闪现html
当时我刚好看过 Vue
中的内置组件 transition
的实现,意识到彻底能够用 transition
组件的部分原理来完成这个效果,可是因为没有深刻地探究过为何是这样,只停留在表面,知其然而不知其因此然,因此尽管我知道如何实现这个效果,但很难解释为何是这样,语言组织地比较困难前端
后来我无心间看到一篇文章 FLIP技术给Web布局带来的变化,立马恍然大悟,原来这个东西叫 FLIPjava
FLIP是 First
、Last
、Invert
和 Play
四个单词首字母的缩写git
First
,指的是在任何事情发生以前(过渡以前),记录当前元素的位置和尺寸,即动画开始以前那一刻元素的位置和尺寸信息,可使用 getBoundingClientRect()
这个 API
来处理(大部分状况下其实 offsetLeft
和 offsetTop
也是能够的)github
Last
:执行一段代码,让元素发生相应的变化,并记录元素在动画最后状态的位置和尺寸,即动画结束以后那一刻元素的位置和尺寸信息浏览器
Invert
:计算元素第一个位置(First
)和最后一个位置(Last
)之间的位置变化(若是须要,还能够计算两个状态之间的尺寸大小的变化),而后使用这些数字作必定的计算,让元素进行移动(经过 transform
来改变元素的位置和尺寸),从而建立它位于第一个位置(初始位置)的一个错觉微信
即,一上来直接让元素处于动画的结束状态,而后使用 transform
属性将元素反转回动画的开始状态(这个状态的信息在 First
步骤就拿到了)app
Play
:将元素反转(伪装在first
位置),咱们能够把 transform
设置为 none
,由于失去了 transform
的约束,因此元素确定会往本该在的位置(即动画结束时的那个状态)进行移动,也就是last
的位置,若是给元素加上 transition
的属性,那么这个过程天然也就是以一种动画的形式发生了布局
按照个人理解,就是对动画元素起止状态的一个量化,量化成一个公式,绝大部分的连续动画均可以经过套用这个公式来完成,提高动画的开发效率,更加详细的请自行参见 FLIP技术给Web布局带来的变化
了解了 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,有兴趣的能够亲自试下,另外代码也能够上传到 Github
有些人可能比较疑惑,若是想要实现动画的话,直接 transform
不就行了,为何要画蛇添足搞个 FLIP
的概念出来?
我一开始也有这个疑惑,可是当我实际实现一个动画的时候,好比开头的那个卡片动画,这个疑问就当即获得了解答。
对于一些动画,你明确的知道它的初始态(First
)和结束态(Last
),好比你就想让一个元素从 left:10px;
移动到 left:100px;
,那么你直接 transform
就行了,根本不必 FLIP
,用了反而画蛇添足;
但除此以外,还有一部分你没法明确的初始态(First
)或结束态(Last
)的动画,好比开头那个卡片动画,除非你限定死了每一个卡片的尺寸以及总体页面的尺寸,不然你没法明确当你任意插入或者删除了某个卡片以后,其余卡片应当在什么位置。
好比,在你的浏览器下,每一个卡片宽高都是 100,浏览器页面宽度为 1380,因此每一列能够排布 13个卡片,但这只是在你的浏览器上,用户的浏览器页面宽度多是 1280,也多是1980,每一列排布的卡片数量多是12也多是19,不一而足,甚至你还能够任意 resize
页面的尺寸,那么这个时候,你怎么肯定每一个时刻全部卡片 last
状态的信息?
你可能会说,我固然不知道,可是我可使用浏览器 API
进行测量啊。
很差意思,这正是 FLIP
要作的事情之一,你仍是在无心识地状况下用到了这个东西,只不过相对于被前人总结并优化后的 FLIP
来讲,你的总体用法可能更零散更不规范一些。
就像标题说的那样,让动画变得更简单
,你能够不用,可是若是你知道怎么用了,那么动画对于你来讲就是一个公式一把梭,更 easy
。
不少前端同窗彷佛不太在乎动画,认为这只是一个辅助能力,业务逻辑才是最重要的,其余的全都靠后站,就算是有时间也要看心情再决定搞不搞
个人见解是,业务逻辑固然是要放在首位的,可是一样也不要小看了其他的细枝末节,例如动画,一个体验良好的动效彻底能够吸引用户的更多停留,以一种通用的方式从侧面提高业务的转化效果,某些特定场景下,其所能起到的做用甚至能够与业务的目标并驾齐驱