本文代码均放在git仓库
FLIP是什么
首先FLIP并不是一项新技术,可以把它理解为一种实现动画的新的理念或者新的方法。
FLIP是 First、Last、Invert和 Play四个单词首字母的缩写。
- First,指的是在任何事情发生之前(过渡之前),记录当前元素的位置和尺寸,即动画开始之前那一刻元素的位置和尺寸信息,可以使用 getBoundingClientRect()这个 API来处理;
- Last:执行一段代码,让元素发生相应的变化,并记录元素在动画最后状态的位置和尺寸,即动画结束之后那一刻元素的位置和尺寸信息;
- Invert:计算元素第一个位置(First)和最后一个位置(Last)之间的位置或者尺寸变化,然后使用这些数字做一定的计算,让元素进行移动(通过 transform来改变元素的位置和尺寸),从而创建它位于初始位置的一个错觉。即让元素处于动画的结束状态,然后使用 transform属性将元素反转回动画的开始状态(这个状态的信息在 First步骤就拿到了);
- Play:将元素反转(假装在first位置),我们可以把 transform设置为 none,因为失去了 transform的约束,所以元素肯定会往本该在的位置(即动画结束时的那个状态)进行移动,也就是last的位置,如果给元素加上 transition的属性,那么这个过程自然也就是以一种动画的形式发生了。
举个?
var app = document.getElementById('app');
var first = app.getBoundingClientRect(); // 初始态
app.classList.add('app-to-end');
var last = app.getBoundingClientRect(); // 终态
var invert = first.top - last.top;
// 使元素看起来好像在起始点
app.style.transform = `translateY(${invert}px)`;
requestAnimationFrame(function() {
// 启用tansition,并移除翻转的改变
app.classList.add('animate-on-transforms');
app.style.transform = '';
// 此时,方块就会从假装在起始点开始transition
});
app.addEventListener('transitionend', () => {
app.classList.remove('animate-on-transforms');
});
复制代码
<div id="app"></div>
<style>
#app {
position: absolute;
width:20px;
height:20px;
background: red;
}
.app-to-end {
top: 100px;
}
.animate-on-transforms {
transition: all 2s;
}
</style>
复制代码
在React中是什么用的?
微信app里聊天界面点击预览图片时,图片从对话框到全屏预览的这个过程,用了一个过渡的动画,呈现出图片从小图到大图和从大图恢复到小图的全过程。代码仓库
First
<ul className="pic-list">
{
listData.map((item, index) => (
<li
key={index}
className="pic-item"
onClick={this.previewItem.bind(this, 1, item)} // 给每一个item绑定处理函数
title="点击预览">
<img src={item.bgPic} alt="" className="pic" />
</li>
))
}
</ul>
复制代码
在previewItem函数中的处理逻辑:计算初始态
previewItem (status, previewImgInfo = null, e) {
previewVisibleStatus = status;
if (previewVisibleStatus === 1) {
// 计算初始态
const currentPreviewEle = e.target;
rectInfo = currentPreviewEle.getBoundingClientRect()
previewFirstRect[0] = rectInfo.left
previewFirstRect[1] = rectInfo.top
this.setState({
previewImgInfo,
previewStatus: 1
})
} else {
this.setState({
previewStatus: 1
})
}
}
复制代码
此时会触发一次render()函数
{
(previewVisibleStatus === 1 || previewVisibleStatus === 2) ? (
<>
<div
className="preview-box"
onClick={this.previewItem.bind(this, 2)}
style={{
opacity: previewStatus === 3 && previewVisibleStatus !== 2 ? .65 : 0
}}
></div>
<img
ref={this.previewRef}
src={previewImgInfo.bgPic} // 图片URL
style={{
// 此时previewStatus是1,transform属性为translate3d(0, 0, 0) scale(1)
transform: previewStatus === 2 || previewVisibleStatus === 2
? `xxxxxx` : 'translate3d(0, 0, 0) scale(1)',
transformOrigin: '0 0'
}}
onClick={this.previewItem.bind(this, 2)}
onTransitionEnd={this.transEnd.bind(this)}
alt="" />
</>
) : null
}
复制代码
Last
接着会触发componentDidUpdate生命周期函数,在该生命周期函数中主要是执行updatePreviewStatus函数。
updatePreviewStatus () {
if (this.state.previewStatus === 1) {
// 计算终态
if (previewVisibleStatus === 1) {
const lastRectInfo = this.previewRef.current.getBoundingClientRect()
previewLastRect[0] = lastRectInfo.left
previewLastRect[1] = lastRectInfo.top
scaleValue = rectInfo.width / lastRectInfo.width
}
this.setState({
previewStatus: 2
})
} else if (this.state.previewStatus === 2) {
// Play
setTimeout(() => {
this.setState({
previewStatus: 3
})
}, 0)
}
}
复制代码
Invert
再次触发render()函数
{
(previewVisibleStatus === 1 || previewVisibleStatus === 2) ? (
<>
<div
className="preview-box"
onClick={this.previewItem.bind(this, 2)}
style={{
opacity: previewStatus === 3 && previewVisibleStatus !== 2 ? .65 : 0
}}
></div>
<img
ref={this.previewRef}
src={previewImgInfo.bgPic} // 图片URL
style={{
// 此时previewStatus是2,transform属性为下面这一大坨
transform: previewStatus === 2 || previewVisibleStatus === 2
? `translate3d(${previewFirstRect[0] - previewLastRect[0]}px, ${previewFirstRect[1] - previewLastRect[1]}px, 0) scale(${scaleValue})`
: 'translate3d(0, 0, 0) scale(1)',
transformOrigin: '0 0'
}}
onClick={this.previewItem.bind(this, 2)}
onTransitionEnd={this.transEnd.bind(this)}
alt="" />
</>
) : null
}
复制代码
此时,transform属性是
translate3d(${previewFirstRect[0] - previewLastRect[0]}px, ${previewFirstRect[1] - previewLastRect[1]}px, 0) scale(${scaleValue})
复制代码
也就是说第二次执行render函数的时候,图片在(translate3d + scale)的作用下缩放回了初始位置。
Play
再次触发componentDidUpdate生命周期函数,在该生命周期函数中主要是执行updatePreviewStatus函数。
updatePreviewStatus () {
if (this.state.previewStatus === 1) {
// xxxxx
} else if (this.state.previewStatus === 2) {
// Play
setTimeout(() => {
this.setState({
previewStatus: 3
})
}, 0)
}
}
复制代码
触发render()函数
<>
<div
className="preview-box"
onClick={this.previewItem.bind(this, 2)}
style={{
opacity: previewStatus === 3 && previewVisibleStatus !== 2 ? .65 : 0
}}
></div>
<img
ref={this.previewRef}
className={`img${(previewStatus === 3 && previewVisibleStatus === 1) || previewVisibleStatus === 2 ? ' active' : ''}`}
src={previewImgInfo.bgPic}
onClick={this.previewItem.bind(this, 2)}
onTransitionEnd={this.transEnd.bind(this)}
alt="" />
</>
复制代码
为图片添加 transition属性,并移除相关 transform属性,即可启动动画。
css:
.img.active {
transition: all .36s ease-in-out;
}
复制代码
至于放大后的图片恢复到小图这一个阶段,可以看成是另外一个 FLIP动画,继续套用即可,只不过这个动画就是上一个放大动画的逆向,所需的尺寸和位置信息都已经拿到了,可以省去调用 getBoundingClientRect的过程。
为什么要用FLIP?
有些人可能比较疑惑,如果想要实现动画的话,直接 transform不就好了,为什么要多此一举搞个 FLIP的概念出来?
对于一些动画,你明确的知道它的初始态(First)和结束态(Last),比如你就想让一个元素从 left:10px;移动到 left:100px;,那么你直接 transform就好了,根本没必要 FLIP,用了反而多此一举;
但除此之外,还有一部分你无法明确的初始态(First)或结束态(Last)的动画。这样情况下使用FLIP就会比较爽快了。但是你可能又会说,我即时不知道结束态,但是可以使用浏览器 API进行测量啊。
不好意思,这正是 FLIP要做的事情之一,你还是在无意识地情况下用到了这个东西,只不过相对于被前人总结并优化后的 FLIP来说,你的整体用法可能更零散更不规范一些。