教你怎麼更優雅的去實現交互過渡動效

前言

對咱們前端來講,用戶體驗是咱們在開發面向客戶的應用時必須作好的一點,怎樣實現良好的交互過渡特效也是咱們須要掌握的一個技術。筆者用過vuereact,這兩個框架都爲咱們開發應用時處理UI的過渡動畫提供了組件,咱們不須要關心底層的實現,只須要簡單配置組件的api就能解決絕大部分的過渡場景。可是我最近在開發一個組件時,須要一些特別的過渡動畫,這些組件功能就有點侷限,因此我學習實踐了一下上述組件使用到的過渡技術,FLIPcss

爲啥要用FLIP

舉個例子,假如如今頁面上存在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實現原理

FLIP實際上是四個單詞的縮寫,FirstLastInvertPlay。我不太喜歡在掘文裏寫具體的概念,你們來看博客都是爲了學技術,具體概念啥的有興趣的就自行去了解一下吧,我在這裏主要介紹一下FLIP的實現的思路。前端框架

FLIP實際上是一個反向的實現過渡思路,通常的過渡思路是我須要把一個元素從A點移動到B點,我就須要一點點修改這個元素的transform,讓它到達B點。而FLIP是直接讓這個元素到達B點的位置,而後計算A點和B點的座標的距離,設置transform,讓它從A點移動到translate(0px, 0px),也就是自身的位置。markdown

FLIP的大致流程是app

  • 1.記錄原來的dom節點的座標
  • 2.數據更改,修改dom頁面(新增,刪除,從新排序)
  • 3.記錄新的dom點的座標
  • 4.根據新和老的座標,得出兩個座標之間的距離
  • 5.給已經從新渲染後的dom添加初始樣式,讓它有相似過渡的動畫

預覽

我仿照vue官網的過渡組件中的例子,寫了一個示例的頁面。由於FLIP只是一種實現動畫的思想,跟框架無關,因此我在demo頁面中是使用的原生js框架

123.gif

DEMO頁面

實現細節

這裏介紹我寫的這個示例頁面的實現過程以及細節。首先是佈局,佈局很簡單,就是一個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);
};
複製代碼

這裏有一個比較關鍵的點,若是在vuereact中使用須要注意,就是頁面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來更方便跟好的幫助你解決問題。

感謝

若是本文對你有所幫助,請幫忙點個贊,感謝你們!

相關文章
相關標籤/搜索