vue 动画入门之小球飞入购物车

3,578 阅读3分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

商品飞入购物车动画,是购物类 APP 常见的效果,比如美团外卖、饿了么外卖等,飞入的动画效果可以有多种方式来实现。

1、技术方案

第一种方案,基于 css3 有两种方法可以实现同样的效果:

  1. 利用 animation 动画
  2. 利用 transform 和 transition

第二种方案,javascript 中也有运动函数的概念:

  1. 小伙伴可以参考这篇文章

  2. 张鑫旭大神有一篇博客,讲的也是 javascript 抛物线运动,写的非常详细:

    JavaScript与元素间的抛物线轨迹运动

第三种方案,vue 中,提供了用于过渡的组件:transition

一般说来,在 vue 中我更倾向于结合 css3 来实现动画效果。

2、直线运动

transformtransition ,只能控制一个物体做直线运动。比如下面的代码:

  <style>
    #ball{
      width: 10px;
      height: 10px;
      border-radius: 50%;
      background: red;
      transition: all 1s linear;
    }
  </style>
  <body>
    <div id="ball"></div>
  </body>
  <script>
     setTimeout(function(){
      ball.style.transform= "translate(800px, 500px)";
     },1000)
  </script>

这段代码的效果是让一个小球从坐标[0,0]飞向坐标[800,500],飞行的时间为 1s

在这里插入图片描述

如果我们想要实现一条类似抛物线的路径,得先来认识一下贝塞尔曲线了。

3、贝塞尔曲线(cubic-bezier)

这里有一篇关于贝塞尔曲线的博客,讲解的比较深刻:点我

当然了,单纯的从数学角度来理解贝塞尔曲线,有点大材小用。这是一个 CSS3 贝塞尔曲线工具、CSS3 贝塞尔曲线模板网站,可以用于调试曲线

我们只需要知道贝塞尔曲线在代码里是如何使用的,以及如何调试就可以了。

当然啦,其实你在以前可能已经接触过贝塞尔曲线了,如果你用过 jquery 的动画的话。

在 jquery 动画中设置的 timing-function,如 easy、easy-in、easy-in-out 等,其实就是写好的贝塞尔曲线。

本质上,贝塞尔曲线是用来控制动画速度的,简单来说,就是一段变化是匀速的?还是先快后慢?或者先慢后快呢?

它而并非是小球所走过的路径,实际上,即便使用了贝塞尔,一个小球的飞行轨迹依然是一条直线。

这是因为,物体的运动永远受到 x 轴和 y 轴的作用,而当两个轴上的运动模型受到同一个 transition 的控制,二者总会合并成一个固定的方向。

就好像中学物理中力的合成。

在这里插入图片描述

大家可以在这里贝塞尔曲线看到相关的效果,我就不再赘述。

根据上面的说法简单的总结下:

想让物体曲线运动,我们可以创建两个物体,一个负责 x轴上的动画,一个负责 y轴的动画,并且,动画的时长要完全一致,而 timing-function 不同步,这样两个物体的运动互相影响,就会形成曲线运动轨迹。

4、代码

我们来简单的做个案例。

首先,我们需要布局两个物体,一个在 x 轴运动,另一个则在 y 轴上运动。

<template>
  <div class="home">
    <div class="cart">cart</div>
    <transition
      @before-enter="beforeEnter"
      @enter="enter"
      @after-enter="afterEnter"
    >
      <div class="ball" v-show="ball.show">
        <div class="innerBall">
          <i class="iconfont icon-xingxing"></i>
        </div>
      </div>
    </transition>
    <div class="menu" @click="addToCart($event)">menu</div>
  </div>
</template>

这里我们写了三个东西,分别是

  • 购物车 cart
  • 一个用于点击的按钮 menu
  • 一个小球,这个小球由两个 div 构成,一个是外层的 ball,另外一个 div 在 ball 的内部,叫做 innerBall,并且使用了的 vue 的内置组件transition

接下来我们要定义一些简单的 css 样式。

<style lang="scss">
.cart,
.ball {
  position: fixed; right: 700px; top: 100px;
}
.cart {
  border: 1px solid blue; padding: 5px 12px;
}
.ball {
  width: 10px;  height: 10px; transition: all 1s cubic-bezier(0.49, -0.29, 1, 1);
}
.innerBall {
  width: 10px; height: 10px; transition: all 1s linear;
}
.menu {
  position: absolute; top: 500px; left: 50px; border: 1px solid blue; 
  padding: 5px 12px; cursor: pointer;
}
</style>

针对 ball 和 innerBall,我们使用 css3 的 transition添加上了过渡的效果,其中我们让 ball 的运动模型为贝塞尔曲线 cubic-bezier(0.49, -0.29, 1, 1),innerBall 的运动模型为直线运动 linear

vue 的 transition 组件,可以定义过渡期间的动画效果,这里的定义既可以使用 css 的类名,也能使用绑定事件的方式

其中,css 类名总共有6个,分别如下:

image.png

但这里我并不打算使用类名的方式,而是采用 js 事件,这里我先简单的陈述一下事件逻辑:

事件分为动画开始前,动画中和动画结束后,针对的绑定事件的名称就是before-enter,enter,after-enter

其中在动画开始前,我们需要:

  1. 获取 ball 和 innerBall
  2. 计算小球的位置到购物车的位置的距离 x、y
  3. 重置小球的位置为初始位置

在动画执行阶段,我们需要:

  1. 让 ball 沿着 x 轴运动
  2. 让 innerBall 沿着 y 轴运动
  3. 运动通过 transform: translate 来实现

在动画技术后,我们将小球隐藏,这样在动画开始之前重制小球的位置时,用户是不会看到的。

下面是具体的代码,我已经做好了注释

<script>
export default {
  name: 'home',
  data() {
    return {
      ball: {
        el: null,
        show: false,
      },
    }
  },
  methods: {
    beforeEnter(el) {
      let ele = this.ball.el //要添加购物车的商品
      let ract = ele.getBoundingClientRect() //商品的位置
      let elRight = this._getStyle(el, 'right') //购物车 right
      let elTop = this._getStyle(el, 'top') //购物车 top
      let x = window.innerWidth - ract.left - parseFloat(elRight) // 计算小球移动的X轴的距离
      let y = ract.top - parseFloat(elTop)// 计算小球移动的y轴的距离

      el.style.display = ''
      el.style.transform = `translateY(${y}px)` //重置小球的x轴位置

      let innerBall = el.querySelector('.innerBall')
      innerBall.style.transform = `translateX(-${x}px)`//重置小球的y轴位置
    },
    enter(el, done) {
      this._offset = document.body.offsetHeight //激发重绘

      el.style.transform = `translate(0, 0)` //小球沿着y轴移动到购物车

      let innerBall = el.querySelector('.innerBall')
      innerBall.style.transform = `translate(0, 0)`//小球沿着x轴移动到购物车

      el.addEventListener('transitionend', done)
    },
    afterEnter(el) {
      this.ball.show = false
      el.style.display = 'none'
    },
    addToCart(event) {
      this.ball.el = event.target
      this.ball.show = true
    },
    _getStyle(el, attr) {
      return el.currentStyle
        ? el.currentStyle[attr]
        : getComputedStyle(el, false)[attr]
    },
  },
}
</script>

写好以后,我们启动本地服务在浏览器中看看效果吧:

在这里插入图片描述