《不怎么样的造轮子—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,有什么办法?答案固然是结束当前动画,再从新设置动画。但是这个在各类场景下实践起来并没那么容易。因此在用户能够本身随意频繁触发重置动画的场景下,很差处理。
  • 其实我这个实现也还不完美,没法在任何场景下使用,只是提供一种思路罢了,有想法能够多交流嘞。

参考

相关文章
相关标签/搜索