船新的JS动画 - FLIP

264 阅读3分钟

页面写久了总会有花里胡哨的动画需求,vue的文档里有很清晰的动画文档进入/离开 & 列表过渡,在框架中要实现过渡动画通常我们会使用到和标签来完成,框架提供了完善的过渡css和js钩子。看的过程中我发现了一个很有意思的东西,在列表的排序过渡中提到了flip这种动画方式,从而达到不需要单独控制每一个结点而完成动画的效果。

方便当然是基于要完成指定动画效果的基础上的。当添加和移除元素的时候,周围的元素会瞬间移动到他们的新布局的位置,而不是平滑的过渡,而FLIP能帮助我们解决这个问题。

首先我们先了解一下FLIP到底是啥,这实际上是完成动画的四个步骤(FLIP Your Animations):

  • First: the initial state of the element(s) involved in the transition.
  • Last: the final state of the element(s).
  • Invert: here’s the fun bit. You figure out from the first and last how the element has changed.
  • Play: switch on transitions for any of the properties you changed

 

接下来一起来实现一下文档中列表中添加元素,周围的元素平滑移动到他们的新布局的位置(以下部分使用vue实现并省略数据更新绑定页面与布局的过程)

首先先定义一个简单的每行四列的布局:

<template>
 <div class="grid-group">
      <div v-for="(item, index) in options" :key="index">
        <div
          ref="grid-item"
          :id="item.id"
          class="grid-item"
          :style="{ backgroundColor: item.value }"
        />
      </div>
  </div>
</template>
const options = [
 { id: 0, value: 'red' },
 { id: 1, value: 'green' },
 { id: 2, value: 'blue' },
]

于此我们已经定义好了dom元素的初始位置。根据FLIP的定义,接下来需要获取到这些节点的当前位置信息,添加新元素,并获取移动的目标位置,然后趁着dom改变了但是浏览器还没有渲染的时间,将这些元素通过css移动到之前的位置上,最后通过animation api绑定动画并播放就完成了全过程。

好了,准备过程over至少已经了解了全过程,那么一步一步来

const prevDoms = this.$refs['grid-item']

通过vue的ref能很方便的获取到绑定到的节点信息,在模版语句中绑定同名到ref在获取的时候能拿到一个获取到所有同名节点的array,位置信息就藏在里面,而通过getBoundingClientRect我们能很方便的拿到,之后还要获取一下改变后的节点位置所以在这我们就直接抽象成一个函数,返回值是由dom节点id作为键值的对象:

const _getRacts = e => {
  const target = {}
  e.map(item => {
    const ract = item.getBoundingClientRect()
    const { left, top } = ract
    target[item.id] = { left, top, dom: item }
 })
  return target
}

然后就是朴实无华的更新数据,当然在vue的基础之上他完成了大部分工作,我们的关注点就直接聚焦于数据的变化就好了(当然id我们就先随意生成一个)

const _getUuid = () =>
  Number(
    Math.random()
     .toString()
     .substr(3, length) + Date.now()
 ).toString(36)
this.options.unshift({ id: _getUuid(), value: _getRandomColor() })

这一步就是重头戏了,当options改变的时候dom不会立即改变,而是会等待宏任务做完之后把渲染的步骤移入microtask中,通过vue提供的nextTick api能使得我们在下一个event loop中获取到更新后的dom节点,此时新加入的元素已经被塞入,周围元素已经被移动到了目标位置,dom结构改变了但是浏览器还没有渲染,我们就能获取到元素的最终位置信息:

this.$nextTick(() => {
  const currPositions = _getRacts(prevDoms)
})

如今已经得到了起始位置和目标位置,但是如果到此为止周围元素依旧是瞬间移动而没有动画,所以需要通过transform将每个dom元素位置倒置从而放回原位(由于最后要绑定动画然后通过animation api播放出来,所以直接通过invert记录一下原始位置):

Object.keys(prevPositions).forEach(src => {
  const curr = currPositions[src]
  const prev = prevPositions[src]
  const invert = {
    left: prev.left - curr.left,
    top: prev.top - curr.top
 }
})

最后就是keyframes定义关键帧和动画化的配置并播放就大功告成!

const keyframes = [
 { transform: `translate(${invert.left}px, ${invert.top}px)` },
 { transform: `translate(0)` }
]
 
curr.dom.animate(keyframes, {
  duration: 300,
  easing: 'cubic-bezier(0,0,0.32,1)'
})

实际上实际业务中会发现这种需求实际上并不少,比如排序,比如洗牌等等,有动画会让页面友好很多。当然开发的时候没有必要去手写FLIP 的操作,无论是vue还是react框架都提供了现成好用的组件,但是原生写总是有种莫名的快乐不是么?万一啥时候用到了呢!

 

参考

前端动画必知必会:React 和 Vue 都在用的 FLIP 思想实战

进入/离开 & 列表过渡

FLIP Your Animations

Animation.Animation()