本文代碼均放在git倉庫
FLIP是什么
首先FLIP并不是一項(xiàng)新技術(shù),可以把它理解為一種實(shí)現(xiàn)動(dòng)畫的新的理念或者新的方法盐欺。
FLIP是 First赁豆、Last、Invert和 Play四個(gè)單詞首字母的縮寫冗美。
- First魔种,指的是在任何事情發(fā)生之前(過渡之前),記錄當(dāng)前元素的位置和尺寸粉洼,即動(dòng)畫開始之前那一刻元素的位置和尺寸信息节预,可以使用 getBoundingClientRect()這個(gè) API來處理;
- Last:執(zhí)行一段代碼属韧,讓元素發(fā)生相應(yīng)的變化安拟,并記錄元素在動(dòng)畫最后狀態(tài)的位置和尺寸,即動(dòng)畫結(jié)束之后那一刻元素的位置和尺寸信息宵喂;
- Invert:計(jì)算元素第一個(gè)位置(First)和最后一個(gè)位置(Last)之間的位置或者尺寸變化糠赦,然后使用這些數(shù)字做一定的計(jì)算,讓元素進(jìn)行移動(dòng)(通過 transform來改變?cè)氐奈恢煤统叽纾┕兀瑥亩鴦?chuàng)建它位于初始位置的一個(gè)錯(cuò)覺拙泽。即讓元素處于動(dòng)畫的結(jié)束狀態(tài),然后使用 transform屬性將元素反轉(zhuǎn)回動(dòng)畫的開始狀態(tài)(這個(gè)狀態(tài)的信息在 First步驟就拿到了)裸燎;
- Play:將元素反轉(zhuǎn)(假裝在first位置)顾瞻,我們可以把 transform設(shè)置為 none,因?yàn)槭チ?transform的約束顺少,所以元素肯定會(huì)往本該在的位置(即動(dòng)畫結(jié)束時(shí)的那個(gè)狀態(tài))進(jìn)行移動(dòng)朋其,也就是last的位置王浴,如果給元素加上 transition的屬性,那么這個(gè)過程自然也就是以一種動(dòng)畫的形式發(fā)生了梅猿。
舉個(gè)??
var app = document.getElementById('app');
var first = app.getBoundingClientRect(); // 初始態(tài)
app.classList.add('app-to-end');
var last = app.getBoundingClientRect(); // 終態(tài)
var invert = first.top - last.top;
// 使元素看起來好像在起始點(diǎn)
app.style.transform = `translateY(${invert}px)`;
requestAnimationFrame(function() {
// 啟用tansition氓辣,并移除翻轉(zhuǎn)的改變
app.classList.add('animate-on-transforms');
app.style.transform = '';
// 此時(shí),方塊就會(huì)從假裝在起始點(diǎn)開始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里聊天界面點(diǎn)擊預(yù)覽圖片時(shí)钞啸,圖片從對(duì)話框到全屏預(yù)覽的這個(gè)過程,用了一個(gè)過渡的動(dòng)畫喇潘,呈現(xiàn)出圖片從小圖到大圖和從大圖恢復(fù)到小圖的全過程体斩。代碼倉庫
First
<ul className="pic-list">
{
listData.map((item, index) => (
<li
key={index}
className="pic-item"
onClick={this.previewItem.bind(this, 1, item)} // 給每一個(gè)item綁定處理函數(shù)
title="點(diǎn)擊預(yù)覽">
<img src={item.bgPic} alt="" className="pic" />
</li>
))
}
</ul>
在previewItem函數(shù)中的處理邏輯:計(jì)算初始態(tài)
previewItem (status, previewImgInfo = null, e) {
previewVisibleStatus = status;
if (previewVisibleStatus === 1) {
// 計(jì)算初始態(tài)
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
})
}
}
此時(shí)會(huì)觸發(fā)一次render()函數(shù)
{
(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={{
// 此時(shí)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
接著會(huì)觸發(fā)componentDidUpdate生命周期函數(shù)颖低,在該生命周期函數(shù)中主要是執(zhí)行updatePreviewStatus函數(shù)絮吵。
updatePreviewStatus () {
if (this.state.previewStatus === 1) {
// 計(jì)算終態(tài)
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
再次觸發(fā)render()函數(shù)
{
(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={{
// 此時(shí)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
}
此時(shí)忱屑,transform屬性是
translate3d(${previewFirstRect[0] - previewLastRect[0]}px, ${previewFirstRect[1] - previewLastRect[1]}px, 0) scale(${scaleValue})
也就是說第二次執(zhí)行render函數(shù)的時(shí)候蹬敲,圖片在(translate3d + scale)的作用下縮放回了初始位置。
Play
再次觸發(fā)componentDidUpdate生命周期函數(shù)莺戒,在該生命周期函數(shù)中主要是執(zhí)行updatePreviewStatus函數(shù)伴嗡。
updatePreviewStatus () {
if (this.state.previewStatus === 1) {
// xxxxx
} else if (this.state.previewStatus === 2) {
// Play
setTimeout(() => {
this.setState({
previewStatus: 3
})
}, 0)
}
}
觸發(fā)render()函數(shù)
<>
<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屬性,并移除相關(guān) transform屬性从铲,即可啟動(dòng)動(dòng)畫瘪校。
css:
.img.active {
transition: all .36s ease-in-out;
}
至于放大后的圖片恢復(fù)到小圖這一個(gè)階段,可以看成是另外一個(gè) FLIP動(dòng)畫名段,繼續(xù)套用即可阱扬,只不過這個(gè)動(dòng)畫就是上一個(gè)放大動(dòng)畫的逆向,所需的尺寸和位置信息都已經(jīng)拿到了吉嫩,可以省去調(diào)用 getBoundingClientRect的過程价认。
為什么要用FLIP?
有些人可能比較疑惑自娩,如果想要實(shí)現(xiàn)動(dòng)畫的話用踩,直接 transform不就好了,為什么要多此一舉搞個(gè) FLIP的概念出來忙迁?
對(duì)于一些動(dòng)畫脐彩,你明確的知道它的初始態(tài)(First)和結(jié)束態(tài)(Last),比如你就想讓一個(gè)元素從 left:10px;移動(dòng)到 left:100px;姊扔,那么你直接 transform就好了惠奸,根本沒必要 FLIP,用了反而多此一舉恰梢;
但除此之外佛南,還有一部分你無法明確的初始態(tài)(First)或結(jié)束態(tài)(Last)的動(dòng)畫梗掰。這樣情況下使用FLIP就會(huì)比較爽快了。但是你可能又會(huì)說嗅回,我即時(shí)不知道結(jié)束態(tài)及穗,但是可以使用瀏覽器 API進(jìn)行測(cè)量啊。
不好意思绵载,這正是 FLIP要做的事情之一埂陆,你還是在無意識(shí)地情況下用到了這個(gè)東西,只不過相對(duì)于被前人總結(jié)并優(yōu)化后的 FLIP來說娃豹,你的整體用法可能更零散更不規(guī)范一些焚虱。