FLIP實(shí)現(xiàn)animation動(dòng)畫

本文代碼均放在git倉(cāng)庫(kù)

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來(lái)處理;
  • 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來(lái)改變?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;
// 使元素看起來(lái)好像在起始點(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ù)到小圖的全過程。代碼倉(cāng)庫(kù)

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的概念出來(lái)?

對(duì)于一些動(dòng)畫,你明確的知道它的初始態(tài)(First)和結(jié)束態(tài)(Last),比如你就想讓一個(gè)元素從 left:10px;移動(dòng)到 left:100px;,那么你直接 transform就好了,根本沒必要 FLIP,用了反而多此一舉;

但除此之外,還有一部分你無(wú)法明確的初始態(tài)(First)或結(jié)束態(tài)(Last)的動(dòng)畫。這樣情況下使用FLIP就會(huì)比較爽快了。但是你可能又會(huì)說,我即時(shí)不知道結(jié)束態(tài),但是可以使用瀏覽器 API進(jìn)行測(cè)量啊。

不好意思,這正是 FLIP要做的事情之一,你還是在無(wú)意識(shí)地情況下用到了這個(gè)東西,只不過相對(duì)于被前人總結(jié)并優(yōu)化后的 FLIP來(lái)說,你的整體用法可能更零散更不規(guī)范一些。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • CSS參考手冊(cè) 一、初識(shí)CSS3 1.1 CSS是什么 CSS3在CSS2.1的基礎(chǔ)上增加了很多強(qiáng)大的新功能。目前...
    沒汁帥閱讀 4,265評(píng)論 1 13
  • 1 CALayer IOS SDK詳解之CALayer(一) http://doc.okbase.net/Hell...
    Kevin_Junbaozi閱讀 5,331評(píng)論 3 23
  • 一:在制作一個(gè)Web應(yīng)用或Web站點(diǎn)的過程中,你是如何考慮他的UI、安全性、高性能、SEO、可維護(hù)性以及技術(shù)因素的...
    Arno_z閱讀 1,354評(píng)論 0 1
  • 一、CSS入門 1、css選擇器 選擇器的作用是“用于確定(選定)要進(jìn)行樣式設(shè)定的標(biāo)簽(元素)”。 有若干種形式的...
    寵辱不驚丶?xì)q月靜好閱讀 1,708評(píng)論 0 6
  • 通過jQuery,您可以選?。ú樵儯琿uery)HTML元素,并對(duì)它們執(zhí)行“操作”(actions)。 jQuer...
    枇杷樹8824閱讀 713評(píng)論 0 3

友情鏈接更多精彩內(nèi)容