《不怎麼樣的造輪子—FLIP動畫》—新春限定

一直很好奇坊間的一些vue/react ui 庫的按鈕彈窗的那些動畫是怎麼作到從按鈕的方向彈射出來的,效果很讓人驚歎,可是一直沒去深究。後來無心中看到一種動畫效果——叫FLIP,發現能完美實現前面的那些高大上的效果,遂去翻書,這個東西一開始看看得一頭霧水,不知所云。後面本身小寫了一些demo,漸漸地體會到其中的奧義。因而記錄下來。css

項目源碼Git地址html

原理

FLIP 是一套動畫思想和執行的流程規則,它表達的含義是:First,Last,Invert,Play。vue

  • first——元素即將開始過渡動畫以前的初始狀態,即位置、尺寸信息
  • last——元素的最終狀態
  • invert—— 計算出初始狀態和最終狀態的變化量,像寬度,高度,透明度這些。而後把這些狀態量統統反轉,並使用transform的對應屬性應用到元素上
  • play——開啓動畫,把動畫的結束狀態設置爲移除掉咱們在invert中設置了的transform的屬性,和還原opacity屬性。

看不懂文字?不要緊,demo已經在路上了...node

原生js使用FLIP

如下demo實現了按鈕彈窗這麼個小功能。演示地址(使用chrome)react

<!--html-->
<div class="dialog-btn"></div>
<div class="dialog-wrapper">
  <div class="dialog -large">
    <h1>hello world!</h1>
  </div>
</div>
複製代碼
// js
const wrapper = document.querySelector('.dialog-wrapper')
const dialog = wrapper.querySelector('.dialog')
const dialogBtn = document.querySelector('.dialog-btn')

dialogBtn.addEventListener('click', () => {
  // first——獲取運動元素的初始狀態
  const first = dialogBtn.getBoundingClientRect()
  
  // last——觸發運動元素到達最終狀態
  wrapper.style.display = 'block'
  const last = dialog.getBoundingClientRect()
  
  // invert——計算運動元素的變化量
  const dx = first.x - last.x   // x軸位移變化量
  const dy = first.y - last.y   // y軸位移變化量
  const dw = first.width / last.width   // 寬度變化量
  const dh = first.height / last.height // 高度變化量
  
  // play——觸發運動元素開始運動
  dialog.animate(
    [
      {
        transform: ` translate(${dx}px, ${dy}px) scale(${dw}, ${dh}) `,
        opacity: 0.6
      },
      {
        transform: ` translate(0, 0) scale(1, 1) `,
        opacity: 1
      }
    ],
    {
      duration: 6000,
      easing: 'cubic-bezier(0.2, 0, 0.2, 1)'
    }
  )

}, false)
複製代碼

實現效果:css3

把上面的代碼總結一下:git

  1. 運動元素——模態框的初始尺寸獲取的是按鈕的尺寸,不必定是彈窗本來的css尺寸。能夠看得出flip真的是能夠隨心所欲,設置了什麼初始狀態,動畫的初始狀態就是什麼。
  2. 獲取last狀態,就是直接經過css把模態框顯示出來,經過getBoundingClientRect來獲取位置尺寸信息。
  3. invert階段,由於在第二步中,元素當前已是最終狀態,而咱們作動畫的需求本來是讓元素從初始狀態動畫過渡到最終狀態,因此咱們指望的是在第三步中把元素還原回初始狀態,再在第下一步中觸發動畫。用 first - last ,得出差值,把差值應用到元素上,來讓元素回到初始狀態。舉個🌰,初始狀態left爲20px,最終狀態left爲100px 。x軸位移變化量爲20-100=-80px,此時咱們設置transform: translateX(-80px)就可以讓元素回到初始狀態了。
  4. 觸發補間動畫,設置終態爲transform: translateX(0),由css3管理動畫執行。

提出疑問

  • 一套F-L-I-P流程作動畫帶來什麼優點?github

    答: 說白了就是css3自己的優點。一是使用了瀏覽器自己的功能,只須要肯定動畫的開始和結束節點的位置尺寸信息,由渲染引擎自動完成補間動畫。二是transformopacity屬性自己就能觸發gpu渲染,動畫性能至關贊。若是本身操做js+dom,控制動畫的工做量會至關大,並且通常還須要引入第三方庫。chrome

  • invert反轉的意圖是什麼?瀏覽器

    答:用flip方案作動畫時,會加入一些transform屬性,這些是額外加入的屬性,會影響到咱們本來的css佈局,經過反轉操做,最後把transform置爲none,那麼transform相關屬性只會在過渡動畫的生命週期裏存在,動畫結束時再也不影響原來的dom的css佈局。

如何在MVVM框架上使用FLIP

你們都知道MVVM框架是數據驅動,對dom的操做通常沒jQuery這種方便。而2020年了,MVVM地位舉足輕重,如何在MVVM框架上集成也是一個大的課題,下面會以react爲案例來探討一下

肯定 F-L-I-P

  • F —— 如何獲取 first 的位置?dom更新前的位置尺寸信息通常能夠在 componentWillReceivePropscomponentWillUpdate這些生命週期函數裏獲取
  • L —— 如何獲取 last 的位置?對應的是dom更新後的鉤子函數裏,即componentDidMountcomponentDidUpdatesetStat的回調
  • I —— 經過 FL 來計算差值。dom屢次變化時,上一次的last狀態就是下一次的first狀態了。因此須要記錄每一次的dom變更,須要維護一個狀態管理器。
  • P —— 調用css3補間動畫,參數來源於 I 步驟所得

實現按鈕彈窗

不妨以上一個例子——按鈕彈窗場景來小試牛刀 在線預覽。從複用的角度來看,咱們但願把動畫的邏輯封裝成單獨的一個Component,這裏咱們定義爲了Flipper,相關的flip邏輯將會放在這裏面去。

根據flip法則:

  • first —— 按鈕的位置 firstRect,由父元素傳遞給Flipper
    class App extends Component {
    
      state = {
        showDialog: false,
        firstRect: {},
      }
    
      render () {
        return (
          <div> <button ref={el => this.btnRef = el} onClick={this.onClick} >open dialog</button> { this.state.showDialog ? ( <div className="dialog-wrapper"> <Flipper duration={1000} firstRect={firstRect} > <Dialog key="dialog" close={this.close.bind(this)} /> </Flipper> </div> ) : null } </div> ) } componentDidMount () { <!--獲取按鈕的尺寸信息--> this.setState({ firstRect: this.btnRef.getBoundingClientRect() }) } } 複製代碼
  • last / invert / play —— 在這個例子中,作的是一個元素由無到有的進入的動畫行爲,在componentDidMount裏,模態框插入到了document,此時就是模態框的last狀態。在React Component中獲取實際的dom結構,須要借用到一些React提供的頂層API。
    class Flipper extends Component {
    
      state = {
        showDialog: false,
        firstRect: {},
      }
    
      render () {
        return (
          <> <!--這裏須要爲節點添加ref信息,以供在js裏獲取模態框dom服務。因此使用React.cloneElement加工this.props.children--> <!--{ this.props.children }--> { React.Children.map(this.props.children, node => { return React.cloneElement(node, { ref: node.key }); }) } </> ) } componentDidMount () { <!--開始f-l-i-p--> this.doFlip() } doFlip () { <!--first信息--> let first = this.props.firstRect if (!first) return; <!--last信息,this.props.children 表明<Dialog />組件,經過ReactDOM.findDOMNode獲取dom節點 --> const dom = ReactDOM.findDOMNode(this.refs[this.props.children.key]) const last = dom.getBoundingClientRect() <!--inver信息--> const diffX = first.x - last.x const diffY = first.y - last.y if (!diffX && !diffY) return; <!--觸發play--> const task = dom.animate( [ { opacity: 0, transform: `translate(${diffX}px, ${diffY}px)` }, { opacity: 1, transform: `translate(0, 0)` } ], { duration: +this.props.duration, easing: 'ease' } ) task.onfinish = () => { <!--補間動畫結束--> } } } 複製代碼

總結:MVVM框架表明的是一種數據驅動的思想,可是咱們作flip動畫時,是直接操做dom的,並無把css的相關屬性做爲state去管理。

實現列表的增刪移位

實現效果:在線預覽

原本覺得按彈窗的邏輯補充一下就好了,然而寫着寫着就發現問題並無想象中的那麼簡單。梳理出的問題以下:

  • 列表存在多個動畫元素,如何保存未知數量的動畫元素的狀態,如何對元素進行標識

    進行動畫的元素,都須要綁定一個key props做爲惟一標識,一來聽從React的使用規則,保證在動畫過程當中,只要保證key沒被清除,dom就不被從新生成,避免動畫信息丟失;二來咱們須要爲每一個運動元素添加ref屬性,藉助key的值來設置較爲方便。所以經過key來保存和索引元素信息。cacheRect也能夠放在componentWillReceiveProps中調用,但只在props變動時觸發,能夠根據編碼實際狀況選擇。

    cacheRect () {
      this.state.cloneChildren.forEach(node => {
        this.cacheRectData[node.key] = ReactDOM.findDOMNode(this.refs[node.key]).getBoundingClientRect()
      })
    }
    componentWillUpdate () {
      <!--state和props更新時都會觸發-->
      this.cacheRect()
    }
    
    複製代碼
  • 元素存活週期比較長,不只有進入動畫,離開動畫,還有因元素彼此之間相對位置變化而產生換位等動畫,如何管理一個元素從進入-移動-離開整個生命週期期間的位置尺寸信息

    flip的難點就是firstlast的獲取:

    • 進入動畫,f是個性化設置的,l是在插入文檔後經過getBoundingClientRect獲取,再去執行動畫;
    • 移動動畫,表明dom一直在文檔結構中,可能有屢次的flip動畫,因此每次flip開始前要拿最新的f信息,f是dom更新前的狀態,通常可在componentWillReceiveProps,componentWillUpdate中獲取,l是更新後的狀態,在componentDidUpdate中獲取,再根據先前記錄的first信息計算動畫參數

      重點注意:上面的說法針對的是不在執行動畫過程當中的元素。對於運動中的元素,因爲元素`key`不變,沒法經過從新渲染它的最終狀態,而 getBoundingClientRect 獲取的始終是當下的狀態,因此須要經過 offsetTop、offsetLeft來計算,得出的值是忽略了transform相關值的影響的

    • 離開動畫,f也是在dom更新前拿到最新的數據,l也是個性化設置的。

    訣竅 :總的來講,通常利用getBoundingClientRect來獲取first last信息。在要執行補間動畫的地方,若是當下能獲取first的狀態,就要在以前保存好last的狀態。若是當下能獲取last的狀態,就要在以前保存好first的狀態,若是getBoundingClientRect不知足要求,就想一想其餘計算辦法。

  • 元素被刪除,dom立刻消失了,消失的dom我沒法作動畫

    是的,數據驅動下,數據一更新,dom當即更新。因此咱們須要設置一個子組件的this.state.children去代理父組件this.props.children,把props.children中刪除的dom先繼續放在state.children中,補間動畫完成後再真正從文檔中刪除。

  • 在列表中,操做過快時會出現一個問題,一個運動元素的當前動畫還沒完成,last狀態就變了,列表裏其餘數據的增/刪/換位均可能引起這個結果。面對這種狀況,須要及時修正運動軌跡,須要重置flip動畫,才能保證動畫的連貫性。

    這基本就是在MVVM中實踐flip的重點難點,也仍是firstlast的獲取和設置問題。

    例如,刪除元素後在執行動畫的那一段時間裏,其實咱們須要保留該元素,這會致使元素佔位,後面的兄弟元素沒法取締它的位置,因此咱們要把待刪除元素設置成position: absolute及設置合理的lefttop值,讓其餘元素能合理地過渡。另外,在離開動畫進行中的元素不該該被屢次觸發刪除的補間動畫,須要提供狀態進行判斷條件。

    訣竅 :多作運動分解,拆分紅x軸、y軸方向上的分析會清晰簡單一些,跟學物理同樣。

FLIP 使用注意事項

  • 動畫元素自己不能有transform屬性,由於會帶來衝突。
  • 因爲使用的原理仍是基於transform,因此應用場景的邊界也是沒法超過css3的,具體來講,就是位移縮放opacity
  • 動畫衝突問題,一個在animation的元素,若是你要再次修改它的animation,有什麼辦法?答案固然是結束當前動畫,再從新設置動畫。但是這個在各類場景下實踐起來並沒那麼容易。因此在用戶能夠本身隨意頻繁觸發重置動畫的場景下,很差處理。
  • 其實我這個實現也還不完美,沒法在任何場景下使用,只是提供一種思路罷了,有想法能夠多交流嘞。

參考

相關文章
相關標籤/搜索