一直很好奇坊間的一些
vue/react ui
庫的按鈕彈窗的那些動畫是怎麼作到從按鈕的方向彈射出來的,效果很讓人驚歎,可是一直沒去深究。後來無心中看到一種動畫效果——叫FLIP
,發現能完美實現前面的那些高大上的效果,遂去翻書,這個東西一開始看看得一頭霧水,不知所云。後面本身小寫了一些demo,漸漸地體會到其中的奧義。因而記錄下來。css
項目源碼Git地址html
FLIP
是一套動畫思想和執行的流程規則,它表達的含義是:First,Last,Invert,Play。vue
transform
的對應屬性應用到元素上invert
中設置了的transform
的屬性,和還原opacity
屬性。看不懂文字?不要緊,demo已經在路上了...node
如下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
flip
真的是能夠隨心所欲,設置了什麼初始狀態,動畫的初始狀態就是什麼。getBoundingClientRect
來獲取位置尺寸信息。left爲20px
,最終狀態left爲100px
。x軸位移變化量爲20-100=-80px
,此時咱們設置transform: translateX(-80px)
就可以讓元素回到初始狀態了。transform: translateX(0)
,由css3管理動畫執行。一套F-L-I-P
流程作動畫帶來什麼優點?github
答: 說白了就是css3自己的優點。一是使用了瀏覽器自己的功能,只須要肯定動畫的開始和結束節點的位置尺寸信息,由渲染引擎自動完成補間動畫。二是transform
和opacity
屬性自己就能觸發gpu渲染,動畫性能至關贊。若是本身操做js+dom,控制動畫的工做量會至關大,並且通常還須要引入第三方庫。chrome
invert
反轉的意圖是什麼?瀏覽器
答:用flip
方案作動畫時,會加入一些transform
屬性,這些是額外加入的屬性,會影響到咱們本來的css佈局,經過反轉操做,最後把transform
置爲none
,那麼transform
相關屬性只會在過渡動畫的生命週期裏存在,動畫結束時再也不影響原來的dom的css佈局。
MVVM
框架上使用FLIP你們都知道
MVVM
框架是數據驅動,對dom
的操做通常沒jQuery
這種方便。而2020年了,MVVM
地位舉足輕重,如何在MVVM
框架上集成也是一個大的課題,下面會以react
爲案例來探討一下
componentWillReceiveProps
、 componentWillUpdate
這些生命週期函數裏獲取componentDidMount
、 componentDidUpdate
或setStat的回調
裏last
狀態就是下一次的first
狀態了。因此須要記錄每一次的dom變更,須要維護一個狀態管理器。不妨以上一個例子——按鈕彈窗場景來小試牛刀 在線預覽。從複用的角度來看,咱們但願把動畫的邏輯封裝成單獨的一個Component
,這裏咱們定義爲了Flipper
,相關的flip
邏輯將會放在這裏面去。
根據flip
法則:
firstRect
,由父元素傳遞給Flipperclass 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() }) } } 複製代碼
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
的難點就是first
和last
的獲取:
getBoundingClientRect
獲取,再去執行動畫;重點注意:上面的說法針對的是不在執行動畫過程當中的元素。對於運動中的元素,因爲元素`key`不變,沒法經過從新渲染它的最終狀態,而 getBoundingClientRect 獲取的始終是當下的狀態,因此須要經過 offsetTop、offsetLeft來計算,得出的值是忽略了transform相關值的影響的
訣竅 :總的來講,通常利用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
的重點難點,也仍是first
和last
的獲取和設置問題。
例如,刪除元素後在執行動畫的那一段時間裏,其實咱們須要保留該元素,這會致使元素佔位,後面的兄弟元素沒法取締它的位置,因此咱們要把待刪除元素設置成position: absolute
及設置合理的left
、top
值,讓其餘元素能合理地過渡。另外,在離開動畫進行中的元素不該該被屢次觸發刪除的補間動畫,須要提供狀態進行判斷條件。
訣竅 :多作運動分解,拆分紅x軸、y軸方向上的分析會清晰簡單一些,跟學物理同樣。
transform
屬性,由於會帶來衝突。transform
,因此應用場景的邊界也是沒法超過css3的,具體來講,就是位移、縮放、opacity。