一般在开发的过程中,很多时候都会有动画夹杂里面,不管是交互上还是体验,对使用者来说都是不错的,但是当我们真正的去写动画的时候又发现并不是那么简单的,涉及到的css 和 js 的计算等一些处理包括边界条件的处理都是比较麻烦,做出来了很多时候也会不尽如人意。有没有简单的不需要很复杂的实现动画的效果却又不是很麻烦的呢?可以借鉴使用一下现在很流行,至少前端也有必要去了解的动画 Flip 模式。

什么是Flip动画

其实可以拆解出来说,四个单词四个意思。

  • F:表示 First 一个动画元素的最开始的位置。
  • L:表示 Last 一个动画元素的最后结束的位置。
  • I:表示 Invert 一个元素前后位置的翻转,或者可以说是前后位置的交换。
  • P:表示 Play 元素最后完成的过渡动画执行,换句话说,当元素交换之后,两个位置的坐标差值进行 transform 过渡。

简单的四个步骤,思路简单明了,换成代码就是下面的;

// 获取元素开始的位置
var first = el.getBoundingClientRect();

// 中间元素有位置变化的操作
// ...

// 获取元素变化完成后的位置
var last = el.getBoundingClientRect();

// 两个坐标位置差值
var invert = first.top - last.top;

// 趁浏览器还没来的及渲染,设置其位移距离
el.style.transform = `translateY(${invert}px)`;

// 然后再执行动画
requestAnimationFrame(function() {
  el.style.transform = '';
});

如果还是没思路的话,可以接下来看几个小栗子? 来直观的感受下flip动画的魅力。

随机的滑块

做一个随机运动的滑块效果,在一个区域内,通过点击操作使滑块随机位置摆放。可以看一下简单的代码;

<div class="content">
  <div class="box"></div>
</div>
<script>
    function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min)
  }
  function btnClick() {
    const box = document.querySelector('.box')

    const top = getRandomInt(0, 350)
    const left = getRandomInt(0, 350)

    box.style.top = `${top}px`
    box.style.left = `${left}px`
  }    
</script>

样式代码太简单了就不写,可以看到如下的界面点击效果;

3.gif

看起来还不错,至少可以随机的改变位置,但是变化的时候有点生硬不自然,因为这里的动画啥都没写,如果要加上强行加上 transition 过渡也不是不行,但是这次说的不是这个 css 就能处理的问题,可以用 Flip 的思路,通过js写一个动画效果。

可以稍微的改一下上面的点击函数;

function btnClick() {
  const box = document.querySelector('.box')
  // 获取元素的起始位置
  const rect1 = box.getBoundingClientRect()

  const top = getRandomInt(0, 350)
  const left = getRandomInt(0, 350)

  box.style.top = `${top}px`
  box.style.left = `${left}px`

  // 获取元素结束后的位置
  const rect2 = box.getBoundingClientRect()

  // 计算两个坐标位置差值
  const diffLeft = rect1.left - rect2.left
  const diffTop = rect1.top - rect2.top

  // 趁浏览器还没来的及渲染,设置其位移距离
  box.style.transform = `translate(${diffLeft}px, ${diffTop}px)`

  // 执行动画
  requestAnimationFrame(update)

  const start = new Date().getTime()
  function update() {
    let time = (new Date().getTime() - start) / 300
    time = Math.min(1, Math.max(0, time))

    const l = diffLeft * (1 - time)
    const t = diffTop * (1 - time)

    box.style.transform = `translate(${l}px, ${t}px)`

    if (time < 1) {
      requestAnimationFrame(update)
    } else {
      box.style.transform = null
    }
  }
}

3.gif

重新刷新一下代码再点击之后就可以看到滑块的过渡效果了,暂时没有借用 csstransition 过渡效果。 明白人肯定会说,用 css 实现,为啥还要用 js 去做动画呢?

答案肯定有必要去做的,在不同的场景下,多个元素的变化的时候,使用js 操作会好很多,举几个很常见的场景。

  1. 列表的拖拽排序动画
  2. 列表的增删动画
  3. 预览图片的放大缩小

等,操作列表类的都合适,再说一个 vue 里面的 transition-group 过渡动画效果也是借用这个思路写的。说了废话下面就来干货,做一个拖拽的列表。

说道拖拽列表的操作,很多情况下不会如想象的一般那么美好,像大部分列表都是不固定的高度的,在操作js时都要去计算当前项的高度和在列表的位置高度等去计算,还要去设置定位操作,过程可谓是一波三折,如果这样的麻烦话,一般来说都是借助第三方工具操作,但是很简单的项目都要引入大的文件的插件话,有点杀鸡焉用牛刀的意思了,不妨动手写个小几十行的插件即可。屁话不多说,开始写个拖拽的列表。

拖拽的列表

说道到拖拽可是使用场景非常多的一个功能了,移动端算是避免不了,如要要做列表的排序,那肯定是拖拽来的方便。可以用以前的老方法 touch 的监听方法,也不要忘记用新的 html5 API 的 draggable 。只要在需要拖拽的元素上加一个 draggable="true" 的属性,就可以完成元素的拖拽监听了。下面完成后的效果展示;

5.gif

还算丝滑的,实现的代码也不多,就五十多行。

主要是用到了 ondragstartondragenterondragend,三个事件监听,最多就是在enter 的时候做的一些处理,其实跟单个的flip动画思路也是一样的,只不过多了一次循环的处理。

看一下代码实现;

<div class="container">
    <div class="item" draggable="true" style="height: 30px;">1</div>
    <div class="item" draggable="true">2</div>
    <div class="item" draggable="true">3</div>
    <div class="item" draggable="true">4</div>
    <div class="item" draggable="true" style="height: 60px;">5</div>
    <div class="item" draggable="true">6</div>
    <div class="item" draggable="true" style="height: 70px;">7</div>
    <div class="item" draggable="true">8</div>
    <div class="item" draggable="true">9</div>
</div>

Html 部分没什么好说的,就是模拟列表的不同大小高度设置,需要注意的是,里面的item 元素下的所有子元素最好要禁用掉 pointer-events 事件,可以在 css 上直接设置 pointer-events: none; 即可。这样做的目的是为了在拖拽的时候进入到子元素产生不必要处理。

JS 部分可以分开来说

<script>
  // 获取容器 dom 元素
  const container = document.querySelector('.container')
    let sourceNode = null
    container.ondragstart = (e) => {
    // 延迟加上移动的 class 类
        setTimeout(() => {
            e.target.classList.add('move')
      }, 0);
    // 保存正在拖拽的 dom 元素
      sourceNode = e.target.closest('.item')
    }
</script>

开始拖拽的事件简单,没啥好说的,主要是加上拖拽的样式和保存正在拖拽的元素,好让在下面拖拽到其他元素中做一个对比,因为加类名同步事件,导致拖出来的元素也是改变的,所以加一个异步的延迟。值得说的是,这里拖拽只针对了外部容器的监听,其实就是一个事件的委托,有兴趣可以了解一下。

container.ondragenter = (e) => {
  e.preventDefault()
  const currentNode = e.target.closest('.item')
  if (e.target === container || currentNode === sourceNode) {
    return
  }

  const children = Array.from(container.children)
  const currentIndex = children.indexOf(currentNode)
  const sourceIndex = children.indexOf(sourceNode)

  // 保存初始的位置数据
  children.forEach(child => {
    child.rect = child.getBoundingClientRect()
  })
  // 通过判断索引来区分是 往上移动 还是向下移动
  if (currentIndex > sourceIndex) {
    container.insertBefore(currentNode, sourceNode)
  } else {
    container.insertBefore(sourceNode, currentNode)
  }
  // 获取结束后 item 的位置
  children.forEach(child => {
    const rect = child.getBoundingClientRect()

    // 计算两个坐标位置差值
    const diffLeft = child.rect.left - rect.left
    const diffTop = child.rect.top - rect.top

    // 趁浏览器还没来的及渲染,设置其位移距离
    child.style.transform = `translate(${diffLeft}px, ${diffTop}px)`

    // 执行动画
    requestAnimationFrame(update)

    const start = new Date().getTime()
    function update() {
      let time = (new Date().getTime() - start) / 300
      time = Math.min(1, Math.max(0, time))

      const l = diffLeft * (1 - time)
      const t = diffTop * (1 - time)

      child.style.transform = `translate(${l}px, ${t}px)`

      if (time < 1) {
        requestAnimationFrame(update)
      } else {
        child.style.transform = null
      }
    }
  })
}

拖拽中处理的事情还是比较多,总体来说也并不复杂,需要考虑的主要有以下几点;

  1. 因为是委托事件,所以先要把自身和外部的父容器给过滤掉,保证不是自己的元素可以继续往下执行。
  2. 开始进入其他元素之前先保存上一个位置,换句话说就是保存当前的位置到 dom 自身。
  3. 然后通过判断索引来区分是 往上移动 还是向下移动,然后再使用 insertBefore 插入到对应的位置。
  4. 循环所有的元素来对比移动位置距离,并且赋值样式 transform ,这样元素看起来没有插入过去,或者说没有移动到指定位置,但实际上是 transform 的影响。
  5. 最后就是执行动画,使用的是 requestAnimationFrame 提高性能。

跟上面说的动画思路是一样的,提前计算好位置,然后再执行动画。

container.ondragend = (e) => {
  e.target.classList.remove('move')
}

最后一步就是拖拽后把元素的样式重置。

flip 动画思路其实可以给我们做到很多出乎意料的事情,如果想继续往下拓展的话可以做一个图片放大缩小的预览效果,拖拽九宫格排序,等等。