一直很好奇坊间的一些
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。