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

时间: 2020-06-18阅读: 141标签: 动画

前言

vue 的官网中的过渡动画章节中,可以看到一个很酷炫的动画效果


乍一看,让我们手写出这个逻辑应该是非常复杂的,先看看本文最后要实现的效果吧,和这个案例是非常类似的。


预览


 

分析需求

拿到了这个需求,第一直觉是怎么做?假设第一行第一个图片移动到了第二行第三列,是不是要计算出第一行的高度,再计算出第二行前两个元素的宽度,然后从初始的坐标点通过 css 或者一些动画 API 移动过去?这样做是可以,但是在图片不定高不定宽,并且一次要移动很多图片情况下,这个计算方法就非常复杂了。并且这种情况下,图片的坐标都需要我们手动管理,非常不利于维护和扩展。

换种思路,能不能直接很自然的把 DOM 元素通过原生 API 添加到 DOM 树中,然后让浏览器帮我们好这个终点值,最后我们再动画位移过去?

在文档里我们发现一个名词:FLIP,这给了我们一个线索,是不是用这个玩意就可以写出这个动画呢?

答案是肯定的

FLIP

FLIP 究竟是什么东西呢?先看下它的定义:

First

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

Last

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

Invert

这一步比较关键,假设我们图片的初始位置是 左: 0, 上:0,元素动画后的最终位置是 左:100, 上100,那么很明显这个元素是向右下角运动了 100px。

但是,此时我们不按照常规思维去先计算它的最终位置,然后再命令元素从 0, 0 运动到 100, 100,而是先让元素自己移动过去(比如在 vue 中用数据来驱动,在数组前面追加几个图片,之前的图片就自己移动到下面去了)。

这里有一个关键的知识点要注意了。

DOM 元素属性的改变(比如 left、right、 transform 等等),会被集中起来延迟到浏览器的下一帧统一渲染,所以我们可以得到一个这样的中间时间点:DOM 状态(位置信息)改变了,而浏览器还没渲染。

有了这个前置条件,我们就可以保证先让 vue 去操作 DOM 变更,此时浏览器还未渲染,我们已经能得到 DOM 状态变更后的位置了。

说的具体点,假设我们的图片是一行两个排列,图片数组初始化的状态是 [img1, img2,此时我们往数组头部追加两个元素 [img3, img4, img1, img2],那么 img1 和 img2 就自然而然的被挤到下一行去了。

假设 img1 的初始位置是 0, 0,被数据驱动导致的 DOM 改变挤下去后的位置是 100, 100,那么此时浏览器还没有渲染,我们可以在这个时间点把 img1.style.transform = translate(-100px, -100px),让它 先 Invert 倒置回位移前的位置。

Play

倒置了以后,想要让它做动画就很简单了,再让它回到 0, 0 的位置即可,本文会采用最新的 Web Animation API 来实现最后的 Play。


实现

首先图片渲染很简单,就让图片通过简单的排成 4 列即可:

.wrap {
  display: flex;
  flex-wrap: wrap;
}

.img {
  width: 25%;
}

<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 预加载这些图片,防止渲染一堆空白图片到屏幕上。

然后定义一个计算一组 DOM 元素位置的函数 getRects,利用 getBoundingClientRect 可以获得最新的位置信息,这个方法在接下来获取图片元素旧位置和新位置时都要使用。

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 这个 API,这个 API 把你传入的回调函数放进了 microTask 队列,正如上文提到的事件循环的文章里所说,microTask队列的执行一定发生在浏览器重新渲染前。

由于先调用了 this.imgs = newData.concat(this.imgs) 这段代码,触发了 vue 的响应式依赖更新,此时 vue 内部会把本次 DOM 更新的渲染函数先放到 microTask队列中,此时的队列是[changeDOM]。

调用了 nextTick(callback) 后,这个callback函数也会被追加到队列中,此时的队列是 [changeDOM, callback]。

这下聪明的你肯定就明白了,为什么 nextTick的回调函数里一定能获取到最新的 DOM 元素,此时新加入图片的 DOM 已经在 DOM 树中,但是屏幕还没有发生绘制,调用 getBoundingClientRect 可以触发「强制同步布局」,此后的代码里就可以获取到 DOM 在屏幕预计出现的准确位置。(注意,位置信息是最新的,但是屏幕上并没有发生绘制)

由于我们之前保存了图片元素节点的数组 prevImgs,所以在 nextTick 里对这些旧图片再调用一次 getRect 方法,获取到的就是旧图片的最新位置了。

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)
    })
  })
},

乱序

现在我们想要实现官网 demo 中的 shuffle 效果,有了追加图片逻辑的铺垫,是不是已经觉得思路如泉涌了?没错,即使图片被打乱的再厉害,只要我们有「图片开始时的位置」和「图片结束时的位置」,那就可以轻松做到路径动画。

现在我们需要做的是把动画的逻辑抽离出来,我们分析一下整条链路:

保存旧位置 -> 改变数据驱动视图更新 -> 获得新位置 -> 利用 FLIP 做动画

其实外部只需要传入一个 update 方法告诉我们如何去更新图片数组,就可以把这个逻辑完全抽象到一个函数里去。

scheduleAnimation(update) {
  // 获取旧图片的位置
  const prevImgs = this.$refs.imgs.slice()
  const prevSrcRectMap = createSrcRectMap(prevImgs)
  // 更新数据
  update()
  // DOM更新后
  this.$nextTick(() => {
    const currentSrcRectMap = createSrcRectMap(prevImgs)
    Object.keys(prevSrcRectMap).forEach((src) => {
      const currentRect = currentSrcRectMap[src]
      const prevRect = prevSrcRectMap[src]

      const invert = {
        left: prevRect.left - currentRect.left,
        top: prevRect.top - currentRect.top,
      }

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

      const animation = currentRect.img.animate(keyframes, options)
    })
  })
}

那么追加图片和乱序的函数就变得非常简单了:

// 追加图片
async add() {
  const newData = this.getSister()
  await preload(newData)
  this.scheduleAnimation(() => {
    this.imgs = newData.concat(this.imgs)
  })
},
// 乱序图片
shuffle() {
  this.scheduleAnimation(() => {
    this.imgs = shuffle(this.imgs)
  })
}

总结

FLIP

FLIP 不光可以做位置变化的动画,对于透明度、宽高等等也一样可以很轻松的实现。

比如电商平台中经常会出现一个动画,点击一张商品图片后,商品从它本来的位置慢慢的放大成了一张完整的页面。

FLIP的思路掌握后,只要你知道元素动画前的状态和元素动画后的状态,你都可以轻松的通过「倒置状态」后,让它们做一个流畅的动画后到达目的地,并且此时的 DOM 状态是很干净的,而不是通过大量计算的方式强迫它从 0, 0 位移到 100, 100,并且让 DOM 样式上留下 transform: translate(100px, 100px) 类似的字样。

Web Animation

利用 Web Animation API 可以让我们用 JavaScript 更加直观的描述我们需要元素去做的动画,想象一下这个需求如果用 css 来做,我们大概会这样去完成这个需求:

const currentImgStyle = currentRect.img.style
currentImgStyle.transform = `translate(${invert.left}px, ${invert.top}px)`
currentImgStyle.transitionDuration = "0s"

this._reflow = document.body.offsetHeight

currentRect.img.classList.add("move")

currentImgStyle.transform = currentRect.img.style.transitionDuration = ""

currentRect.img.addEventListener("transitionend", () => {
  currentRect.img.classList.remove("move")
})

这是选择用比较原生的方式去控制 css 样式实现的 FLIP 动画,这段代码让我觉得不舒服的点在于:

  1. 需要通过 class 的增加和删除来和 css 来进行交互,整体流程不太符合直觉。
  2. 需要监听动画完成事件,并且做一些清理操作,容易遗漏。
  3. 需要利用 document.body.offsetHeight 这样的方式触发 强制同步布局,比较 hack 的知识点。
  4. 需要利用 this._reflow = document.body.offsetHeight 这样的方式向元素实例上增加一个没有意义的属性,防止被 Rollup 等打包工具 tree-shaking 误删。 比较 hack 的知识点 +1。

而利用 Web Animation API 的代码则变得非常符合直觉和易于维护:

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

const animation = currentRect.img.animate(keyframes, options)

关于兼容性问题,W3C 已经提供了 Web Animation API Polyfill,可以放心大胆的使用。

期待在不久的未来,我们可以抛弃旧的动画模式,迎接这种更新更好的 API。

来自:https://www.jianshu.com/p/28c5ed762791


站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

2.广告联盟: 整理了目前主流的广告联盟平台,如果你有流量,可以作为参考选择适合你的平台点击进入

链接: http://www.fly63.com/article/detial/9361

CSS 动画分类

CSS 动画主要分为CSS 动画分类和CSS 常用动画库简介与JS 动画;然后CSS 动画分类又分为过渡动画transition和关键帧动画keyframes,keyframes主要是从keyframes规则和keyframes属性来介绍

Flutter实现抽屉动画效果

这篇会深化View拖拽实例,利用Flutter Animation、插值器以及AnimatedBuilder教大家实现带动画的抽屉效果。通过构思,我们可以设想到实现抽屉的方式就是用Stack控件将两个Widget叠加显示,用GestureDetector监听手势滑动

css3 过渡和动画

在没有过渡属性的时候,当一个元素的属性值发生变化时,浏览器就会将个这个元素瞬间渲染成新属性值的样式。例如一个定位元素top:0,动态修改成top:100px,这个元素就瞬间跑到100px的位置,有时候我们为了达到某种视觉效果

前端开发常用css动画代码

常用的CSS动画效果,在实际开发中经常需要用到移动、大小、闪烁、渐显、渐隐等动画效果,这篇文章就整理了些常见的动画效果分享给大家,一遍收藏使用。

深入理解requestAnimationFrame的动画循环

requestAnimationFrame解决了浏览器不知道javascript动画什么时候开始、不知道最佳循环间隔时间的问题。它是跟着浏览器的绘制走的,如果浏览器绘制间隔是16.7ms

css动画

我们眼前所看到图像正在以每秒60次的频率刷新,由于刷新频率很高,因此你感觉不到它在刷新。而动画本质就是要让人眼看到图像被刷新而引起变化的视觉效果,这个变化要以连贯的、平滑的方式进行过渡。

Flutter 动画

Flutter动画中有4个比较重要的角色:Animation、Controller、Curve、Tween,先来了解一下这四个角色;Animation是Flutter动画库中的核心类,用于插入指导动画的值

JS动画与CSS动画

JS动画优点:过程控制能力强。可以对动画工程进行精准的控制,暂停、取消、开始、终止都可以。动画效果多、炫酷。有一些效果是CSS动画所不能实现的

设计师10大必备HTML动画工具

HTML 5已经成为Web开发者当中最流行的语言之一,“健壮的”语言有很大的生产优质万维网内容的能力。HTML 5在近三年的增长迅猛,新引入HTML 5的技术更加的优秀。HTML 5技术被现代浏览器所支持

css3动画添加间隔

因项目需要,需要在元素上实现动画效果,并且需要有动画间隔。坑爹的是animation-delay只有在第一次动画开始的时候才起效。在网上找了很多方法,最终的方法基本都是改动画规则

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!

文章投稿关于web前端网站点搜索站长推荐网站地图站长QQ:522607023

小程序专栏: 土味情话心理测试脑筋急转弯幽默笑话段子句子语录成语大全运营推广