對咱們前端來講,用戶體驗是咱們在開發面向客戶的應用時必須作好的一點,怎樣實現良好的交互過渡特效也是咱們須要掌握的一個技術。筆者用過vue
和react
,這兩個框架都爲咱們開發應用時處理UI的過渡動畫提供了組件,咱們不須要關心底層的實現,只須要簡單配置組件的api
就能解決絕大部分的過渡場景。可是我最近在開發一個組件時,須要一些特別的過渡動畫,這些組件功能就有點侷限,因此我學習實踐了一下上述組件使用到的過渡技術,FLIP
。css
舉個例子,假如如今頁面上存在4個標籤[tag1, tag2, tag3, tag4]
,我如今須要把新增一個標籤tag5
,會變成[tag5, tag1, tag2, tag3, tag4]
,我想要讓一開始存在的4個標籤能夠有總體向右移動的過渡動畫,你有什麼思路?前端
若是是沒有學過FLIP
以前的我,我會考慮給這4個標籤都加一個過渡的css
,而後改變transform
讓這些標籤向右移動一段距離,或者使用js
去移動這些標籤向右移動。vue
這種方式存在幾個比較麻煩的問題,向右移動一段距離,若是每一個元素的寬高都一致,計算這個距離還好,可是若是每一個元素的寬高都不一致,那這個距離怎麼計算?若是一行排滿了,還須要換行,高度怎麼計算?還有一個問題,咱們如今用的前端框架都是使用數據來控制視圖,何時去把這個新的tag1
加入到數據中?react
先加數據,再控制標籤移動,頁面會從新渲染,標籤的位置會變化,這時候再控制標籤向右移動其實位置已經錯誤了。web
先移動,再加數據的話,若是是css控制的移動,按過渡時間來,常常會由於js
的定時不精準出現跳幀的現象。若是是js控制的移動,通常須要等待全部元素的移動回調完成再去加數據。api
FLIP
實際上是四個單詞的縮寫,First
,Last
,Invert
,Play
。我不太喜歡在掘文裏寫具體的概念,你們來看博客都是爲了學技術,具體概念啥的有興趣的就自行去了解一下吧,我在這裏主要介紹一下FLIP
的實現的思路。前端框架
FLIP
實際上是一個反向的實現過渡思路,通常的過渡思路是我須要把一個元素從A點移動到B點,我就須要一點點修改這個元素的transform
,讓它到達B點。而FLIP
是直接讓這個元素到達B點的位置,而後計算A點和B點的座標的距離,設置transform
,讓它從A點移動到translate(0px, 0px)
,也就是自身的位置。markdown
FLIP
的大致流程是app
我仿照vue
官網的過渡組件中的例子,寫了一個示例的頁面。由於FLIP
只是一種實現動畫的思想,跟框架無關,因此我在demo頁面中是使用的原生js
。框架
這裏介紹我寫的這個示例頁面的實現過程以及細節。首先是佈局,佈局很簡單,就是一個flex
容器。
.item_box {
display: flex;
flex-wrap: wrap;
...
}
.item {
...
}
// 數據列表,每次修改頁面元素,都會同步修改這個數據
const itemList = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
// 初始化,渲染item列表
const itemInit = () => {
const fragment = document.createDocumentFragment();
for (let i = 0; i < itemList.length; i ++) {
const dom = document.createElement('div');
// 這裏的後一個item是爲了後續找到相應的dom元素
dom.className = `item item${itemList[i]}`;
dom.innerHTML = itemList[i];
fragment.appendChild(dom);
}
document.querySelector('.item_box').appendChild(fragment);
};
itemInit();
複製代碼
咱們拿新增元素來舉例,先實現FLIP
的第一步,記錄原來的dom節點的座標
const getLeftOrTops = () => {
const rectList = [];
// 遍歷數據列表
for (let i = 0; i < itemList.length; i ++) {
// 計算這些節點的left和top數據
const { left, top } = document.querySelector(`.item${itemList[i]}`).getBoundingClientRect()
rectList.push({ left, top });
}
return rectList;
};
複製代碼
而後進行第二步,數據更改,修改dom頁面,以及第三步,記錄新的dom點的座標。
let count = 10;
// 新增dom節點的方法
const itemAdd = () => {
// 這是記錄原來的dom節點座標的方法
const oldRects = getLeftOrTops();
const curIndex = count++;
// 給數據列表新推入一個元素
itemList.unshift(curIndex);
// 在頁面中插入這個新節點,修改dom頁面
$box.insertBefore(createItem(curIndex), $box.childNodes[0]);
// 第三步,記錄新的dom點的座標
// 獲取新的數據列表中dom節點的座標
// 新加入的dom節點不須要添加過渡條件,其餘新舊dom節點須要計算新舊座標的差
const newRects = getLeftOrTops().slice(1);
};
複製代碼
這裏有一個比較關鍵的點,若是在vue
和react
中使用須要注意,就是頁面dom修改和獲取新的dom節點的座標的順序。若是在vue
中,修改dom頁面其實就是修改數據,vue
會把這個修改頁面dom的操做存入nextTick
,等到當前宏任務運行完,再去微任務中運行。因此,獲取新的dom節點的座標的方法,也必須寫在nextTick
中,保證會在dom元素修改完成以後再去獲取(獲取時頁面其實還未從新渲染,可是dom節點已經被修改)。在react
中推薦使用useLayoutEffect
在數據變動,dom修改以後去獲取新的dom節點的座標。
而後咱們繼續FLIP
的流程
const itemAdd = () => {
// 上面的內容省略
...
for (let i = 0; i < oldRects.length; i ++) {
// 根據新和老的座標,得出兩個座標之間的距離
const left = oldRects[i].left - newRects[i].left;
const top = oldRects[i].top - newRects[i].top;
const move = [
{ transform: `translate(${left}px, ${top}px)` },
{ transform: "translate(0)" },
];
const dom = document.querySelector(`.item${oldRects[i].key}`);
// 這裏使用web api的animate方法,讓元素移動
// animate的兼容可使用polyfill,或者使用其它的js動畫庫
// 給已經從新渲染後的dom添加初始樣式,讓它有相似過渡的動畫
dom && dom.animate(move, {
duration: 300,
easing: "cubic-bezier(0,0,0.4,1)",
});
}
};
複製代碼
就這樣,咱們就是完成了FLIP
的整個流程,你的新增元素已經有了好看的過渡動畫。
新增,刪除,從新排列的完整代碼都在上面的示例頁面中,你們有興趣能夠細看。
FLIP
只是一種實現過渡動畫的思路,不但在上文這種文檔流場景下可使用,它在絕對定位的佈局中依然能夠用。並且它是一種很是靈活的方法,並非只侷限於本文中的內容,在實現一些交互動效的時候,能夠多思考看看可否使用FLIP
來更方便跟好的幫助你解決問題。
若是本文對你有所幫助,請幫忙點個贊,感謝你們!