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

    |     2020年6月4日   |   web前端技术, 动画   |     0 条评论   |    836

Vue 官网过渡动画里有一段很炫的效果。乍一看手写逻辑会很复杂,文档里出现了 FLIP。顺着这条线能在 Aerotwist 的 flip-your-animations 上找到思路,再一步步做出类似效果。图片素材引自知乎《有个漂亮女朋友是种怎样的体验?》,侵删。

flip1

本文最终效果:

flip4

预览:http://sl1673495.gitee.io/flip-animation

字母 含义 在本文里
First 动画前的初始状态(位置、透明度等) getBoundingClientRect 记下旧图 left/top
Last 动画后的最终状态 改数据后 DOM 已变、浏览器尚未绘制
Invert 先把元素“倒”回动画前的视觉位置 translate 回偏移量
Play 再播到真正的新位置 Web Animation API 的 animate

一、FLIP 四步

First

即将做动画的元素的初始状态(位置、透明度等)。

Last

即将做动画的元素的最终状态。

Invert

假设图片初始 左: 0, 上:0,最终 左:100, 上100,等于向右下走了 100px。不要先算终点再命令它从 0,0 走到 100,100,而是先让元素自己移动过去(Vue 里用数据驱动,数组前面追加几张图,旧图就被挤下去)。

关键点:DOM 属性变化(left、right、transform 等)会被攒到下一帧统一渲染。于是存在一个时间点:DOM 状态已经变了,浏览器还没画。可以先让 Vue 改 DOM,此时已能读到变更后的位置。

一行两张图,初始 [img1, img2],头部追加后变成 [img3, img4, img1, img2],img1/img2 被挤到下一行。img1 从 0,0 到 100,100,浏览器还没画,先 img1.style.transform = translate(-100px, -100px),把它 Invert 回位移前。

Play

倒置后再让它回到 0, 0 即可。本文用 Web Animation API 做最后的 Play(MDN:Web Animation)。

二、实现

图片用 column-count: 4 排成 4 列:

.wrap {
  display: flex;
  flex-wrap: wrap;
  column-count: 4;
}

<div v-else class="wrap">
  <div class="img-wrap" v-for="src in imgs" :key="src">
    <img ref="imgs" class="img" :src="src" />
  </div>
</div>

关键是往 imgs 追加元素后,做流畅的路径动画。先实现 add:

async add() {
  const newData = this.getSister()
  await preload(newData)
}

随机取几张图,new Image 预加载防止空白,再算旧图位置:

function getRects(doms) {
  return doms.map((dom) => {
    const rect = dom.getBoundingClientRect()
    const { left, top } = rect
    return { left, top }
  })
}

// 当前已有的图片
const prevImgs = this.$refs.imgs.slice()
const prevPositions = getRects(prevImgs)

记下旧位置后追加:

this.imgs = newData.concat(this.imgs)

Vue 异步渲染,改 imgs 不会立刻动 DOM,要用 nextTick:回调进 microTask。microTask 一定发生在浏览器重绘前。

先执行 this.imgs = newData.concat(this.imgs),触发依赖更新,Vue 把本次 DOM 更新推进 microTask,队列是 [changeDOM]。再 nextTick(callback),变成 [changeDOM, callback]。所以 nextTick 里一定能拿到最新 DOM。

保存过 prevImgs,nextTick 里再 getRects 就是旧图片的新位置:

async add() {
  // 最新 DOM 状态
  this.$nextTick(() => {
    // 再调用同样的方法获取最新的元素位置
    const currentPositions = getRects(prevImgs)
},

有了新旧位置就可以 Invert 再 Play:

prevImgs.forEach((imgRef, imgIndex) => {
  const currentPosition = currentPositions[imgIndex]
  const prevPosition = prevPositions[imgIndex]

  // 倒置后的位置,虽然图片移动到最新位置了,但你先给我回去,等着我来让你做动画。
  const invert = {
    left: prevPosition.left - currentPosition.left,
    top: prevPosition.top - currentPosition.top,
  }

  const keyframes = [
    // 初始位置是倒置后的位置
    {
      transform: `translate(${invert.left}px, ${invert.top}px)`,
    },
    // 图片更新后本来应该在的位置
    { transform: "translate(0)" },
  ]

  const options = {
    duration: 300,
    easing: "cubic-bezier(0,0,0.32,1)",
  }

  const animation = imgRef.animate(keyframes, options)
})

完整实现:

async add() {
  const newData = this.getSister()
  await preload(newData)

  const prevImgs = this.$refs.imgs.slice()
  const prevPositions = getRects(prevImgs)

  this.imgs = newData.concat(this.imgs)

  this.$nextTick(() => {
    const currentPositions = getRects(prevImgs)

    prevImgs.forEach((imgRef, imgIndex) => {
      const currentPosition = currentPositions[imgIndex]
      const prevPosition = prevPositions[imgIndex]

      const invert = {
        left: prevPosition.left - currentPosition.left,
        top: prevPosition.top - currentPosition.top,
      }

      const keyframes = [
        {
          transform: `translate(${invert.left}px, ${invert.top}px)`,
        },
        { transform: "translate(0)" },
      ]

      const options = {
        duration: 300,
        easing: "cubic-bezier(0,0,0.32,1)",
      }

      const animation = imgRef.animate(keyframes, options)
    })
  })
},

三、源码与适用面

https://github.com/sl1673495/flip-animation

FLIP 不只做位移,透明度、宽高一样能做。电商里点商品图,从图原来的位置放大成整页,就是这类思路。

只要知道动画前、动画后的状态,就能流畅过渡,而且此时 DOM 是干净的——不是硬编码强迫它从 0,0 走到 100,100。

一句话总结:FLIP 是先让 DOM 走到终点,再在绘制前用 transform 倒回去,最后用动画“假装”走过去,React 和 Vue 的列表过渡都是这套。

转载请注明来源:前端动画必知必会:React 和 Vue 都在用的 FLIP 思想实战。
本文链接地址:https://ai.zhousir.top/?p=2986
回复 取消