基于Javascript和CSS的FLIP动画思路

1,000 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

简介

首先,FLIP动画基础并不是一个插件或者是一个库、框架,它仅仅只是一个动画的实现思路而已,基于这种动画思路,可以再封装成一个插件等。该思路在一些有名框架也有用到,例如:

Vue里面的内置组件TransitionGroup里面就有用到FLIP技术。

image.png

思路

现在,来看一下实现FLIP动画的思路,首先得明白FLIP是4个单词:

FLIP代表FirstLast以及Invert还有Play四个单词的组合,意义如下:

  • First 转换中涉及的元素的初始状态。

  • Last 元素的最终状态。

  • Invert 把转换前的元素和转换后的元素状态进行交换。

  • Play 添加过渡,把Invert时交换的位置重置,这时因为过渡存在,动画就会产生。

实现

现在,就来看下如何实现一个简单的Flip动画,首先做一个简单的方块移动页面:

const box = document.querySelector('.box')
const btn = document.querySelector('.btn')
btn.addEventListener('click', () => {
  box.style.transition = ''
  let rect = null
  const w = window.innerWidth
  const h = window.innerHeight
  const rL = Math.random() * w
  const rT = Math.random() * h
  const l = rL > w - box.offsetWidth ? w - box.offsetWidth : rL
  const t = rT > h - box.offsetHeight ? h - box.offsetHeight : rT
  box.style.left = l + 'px'
  box.style.top = t + 'px'
})

flip01.gif

可以看到页面中每点击一次按钮时,方块就会到一个新的位置去,但是并没有动画就显得很生硬。现在,通过FLIP思路,让每一次方块移动都会有一个动画效果,实现如下:

const box = document.querySelector('.box')
const btn = document.querySelector('.btn')
btn.addEventListener('click', () => {
  let rect = null
  box.style.transition = ''
  const w = window.innerWidth
  const h = window.innerHeight
  
  rect = box.getBoundingClientRect()
  const startLeft = rect.left
  const startTop = rect.top
  
  const rL = Math.random() * w
  const rT = Math.random() * h
  const l = rL > w - box.offsetWidth ? w - box.offsetWidth : rL
  const t = rT > h - box.offsetHeight ? h - box.offsetHeight : rT
  box.style.left = l + 'px'
  box.style.top = t + 'px'
  
  rect = box.getBoundingClientRect()
  const endLeft = rect.left
  const endTop = rect.top
  box.style.transform = `translate(${startLeft - endLeft}px, ${startTop - endTop}px)`
  requestAnimationFrame(() => {
    box.style.transition = 'transform .5s'
    box.style.transform = 'translate(0, 0)'
  })
})

完成之后,元素就会出现动画效果,如下图:

flip02.gif

动的原理

其实让方块实现动的原理很简单,就是在元素移动到目标位置之后,通过设置transform又把元素移动到开始的位置(Invert),之后在requestAnimationFrametransform移除,就会有一个动画的产生。

因为这个过程很快,用户感知不到,肉眼就看着就只有开头到结尾的过渡动画。

问题
虽然上面通过FLIP动画思路实现了动画效果,但是实际上通过直接设置transition属性,也能得到上面的效果。
那么FLIP思路的有点又有什么呢?看看下面这个例子。

例子

接下来实现一个Demo例子:生成一堆方块,然后随机删除一个,代码如下:

const list = document.querySelector('.box-list')
const btn = document.querySelector('.btn')
btn.addEventListener('click', () => {
  const boxs = document.querySelectorAll('.box')
  const rIdx = Math.floor(Math.random() * boxs.length)
  list.removeChild(boxs[rIdx])
})

从下面效果图中,可以看到每个元素都设置了transition过渡效果得,可是动画删除之后移动是没有过渡效果产生的,如下图:

flip03.gif

此时,通过FLIP思路来实现上面的功能,代码简单改造一下:

const list = document.querySelector('.box-list')
const btn = document.querySelector('.btn')

const flip = async (el, callback) => {
  let rect = null
  el.style.transition = ''
  rect = el.getBoundingClientRect()
  const startLeft = rect.left
  const startTop = rect.top
  await callback()
  rect = el.getBoundingClientRect()
  const endLeft = rect.left
  const endTop = rect.top
  el.style.transform = `translate(${startLeft - endLeft}px, ${startTop - endTop}px)`
  requestAnimationFrame(() => {
    el.style.transition = 'all .5s'
    el.style.transform = 'translate(0, 0)'
  })
}

btn.addEventListener('click', () => {
  const boxs = document.querySelectorAll('.box')
  const rIdx = Math.floor(Math.random() * boxs.length)
  boxs.forEach(box => flip(box, () => new Promise(resolve => setTimeout(() => resolve()))))
  list.removeChild(boxs[rIdx])
})

此时,再看看改造之后的效果图:

flip04.gif

上面的效果图,通过纯JavaScript也能实现,但是个人觉得比起FLIP思路来说不够优雅。

注意

使用FLIP思路完成动画有一些注意事项:

  • invert时不要超过100ms时间,可以使用requestAnimationFrame来完成;
  • transform时有可能会导致元素失真;

以上,就是基于JavascriptCSSFLIP动画实现了。