H5、vue加入购物车动画小球

1,004 阅读5分钟

前言

  1. 主要涉及vue中的列表过渡transition的钩子
  2. 小球的掉落购物车的轨道即 曲线运动——贝塞尔曲线可以根据手动规划小球运动路径给transition: all 0.7s cubic-bezier(0.49, -0.29, 0.75, 0.41);的动画过渡效果赋值,到达效果

image.png

实现代码

ball.vue组件
<template>
  <view class="demo">
    <!-- <view class="addBtn" @click="addGoods"></view> -->
    <!-- <view class="cart">
      这是购物车
    </view> -->
	<view v-for="(ball, index) of balls" :key="index">
	  <transition
	    name="drop"
	    @before-enter="beforeEnter"
	    @enter="enter"
	    @after-enter="afterEnter"
	  >
	    <view class="ball" v-show="ball.show">
	      <!--这里为了做两个维度的动画,因此需要多包一层,外层做Y轴,内层做X轴动画-->
	      <view class="inner inner-hook" >+</view>
	    </view>
	  </transition>
	</view>
  </view>
</template>
<script>
export default {
  data() {
    return {
      balls: [
        // 这里定义了多个ball,是因为可能同时有多个小球在动画中(快速点击多次或者多个商品)
        {
          show: false
        },
        {
          show: false
        },
        {
          show: false
        },
        {
          show: false
        },
        {
          show: false
        }
      ],
      dropBalls: [] // 在动画中的小球集合
    };
  },
  methods: {
    addGoods(e) {
      let el = e.target;
      this.balls.forEach(v => {
        if (!v.show) {
          v.show = true; // 当切换元素的display:block/none时,会触发vue的动画
          v.el = el; // 将触发点击事件的“+”号保定道小球对象上,方便获取动画初始时的位置
          this.dropBalls.push(v); // 取一个小球加入动画队列
          return;
        }
      });
    },
    beforeEnter(el) {
      let count = this.dropBalls.length;
      while (count--) {
        // 将动画队列中的小球,依次处理
        let ball = this.dropBalls[count];
		// console.log(ball.el)
        if (ball.show) {
            // let rect = ball.el;
           let rect = ball.el.getBoundingClientRect(); //拿到点击的“+”号的位置,这里不直接取值(我是用的绝对定位,当然可以直接取值)的原因是,商品列表中每个加号的位置是不固定的,如果上下滑动了,这个位置就不确定
          let x = rect.offsetLeft - 45; // 需要偏移的x向距离
          let y = -(window.innerHeight - rect.offsetTop - 60); // 需要偏移的y向距离
          el.style.display = ""; // 当前状态下,display值为none,将其置空。

          // 这里需要注意了,小球飞入的动画分两个维度,X轴和Y轴,因此
          el.style.webkitTransform = `translate3d(0px, ${y}px, 0px)`; // 首先将“ball”Y向移动至“+”好位置
          el.style.transform = `translate3d(0px, ${y}px, 0px)`;
          // 接着将“inner-hook”X向移动至“+”号处,其实此时外层“ball”的X位置没有动,但因为背景色等等样式只应用于“inner-hook”上,因此,视觉效果上,这个小球是移动到了“+”号的位置
          let inner = el.getElementsByClassName("inner-hook")[0];
          inner.style.webkitTransform = `translate3d(${x}px, 0, 0)`;
          inner.style.transform = `translate3d(${x}px, 0, 0)`;
        }
      }
    },
    enter(el) {
      /* eslint-disable no-unused-vars */

      let rf = el.offsetHeight; // 主动触发浏览器重绘
      this.$nextTick(() => {
        el.style.webkitTransform = "translate3d(0, 0, 0)"; //接着将小球位置置为初始值,但css中设置了transition .8s,因此,动画效果就出来了
        el.style.transform = "translate3d(0, 0, 0)";
        let inner = el.getElementsByClassName("inner-hook")[0];
        inner.style.webkitTransform = "translate3d(0, 0, 0)";
        el.style.transform = "translate3d(0, 0, 0)";
      });
    },
    afterEnter(el) {
      let ball = this.dropBalls.shift(); //结束后,将这个活动中的小球删除
      if (ball) {
        ball.show = false;
        el.style.display = "none"; // 并且将其设为不可见
      }
    }
  }
};
</script>
<style lang="scss" scoped>
.demo{
	.cart {
		  position: absolute;
		  left: 30px;
		  bottom: 30px;
		  width: 100px;
		  text-align: center;
		  height: 50px;
		  line-height: 50px;
		  border: 1px solid #ccc;
	}
	
	.addBtn{
		position: absolute;
		top: 100px;
		right: 50px;
		width: 20px;
		height: 20px;
		background: blue;
		border-radius: 50%;
	}
	 
	.ball{
		position: fixed;
		z-index: 9999;
		left: 40px;
		bottom: 45px;
		
		&.drop-enter-active{
			transition: all 0.7s cubic-bezier(0.49, -0.29, 0.75, 0.41);
		}
		  
		.inner{
			width: 15px;
			height: 15px;
			border-radius: 50%;
			background: #FF4343;
			text-align: center;
			font-size: 11px;
			line-height: 12px;
			color: white;
			transition: all 0.8s;
		}
	}
}
  
</style>
调用ball.vue组件的父组件
<template>
	<view class="main">
		<ball ref="addGoods"></ball>
	</view>
</template>

<script>
import ball from '../ball/ball'
export default {
    components: {
            ball
    },
    method: {
            // 购物车添加
        handleAddCar(item, $event) {
// **$event**是指当前触发的是什么事件(鼠标事件,键盘事件等)
// $event.target**则指的是事件触发的目标,即哪一个元素触发了事件,这将直接获取该dom元素
// 动态获取到(每个商品加入按钮的位置信息)元素的获取其属性值;并传入到ball组件中          
            this.$refs.addGoods.addGoods($event)
            console.log($event)
        }
    }

</script>

<style lang="scss">
	@mixin img-width-height($width, $height) {
		height: $height;
		width: $width;
		object-fit: cover;
	}
        .footer-img{
			top: -20px;
			position: absolute;
			border-radius: 50%;
			border: 5px solid white;
			@include img-width-height(61px, 61px);
	}
</style>

详解
  • 生成一个动画小球的div,并且生成五个小球,五个是为了生成一定数量的小球来作为操作使用,按照小球动画的速度,一般来说五个也可以保证有足够的小球数量来运行动画

  • 动画的内容分别是外层和内层,外层控制动画小球的轨道和方向,内层控制动画小球的运行状态

  • 动画使用vue的js钩子实现

  • 因为小球动画只有一个方向(只执行单方向从上到下滚落),所以只用了before-enter,enter,after-enter

  • 用v-show控制小球的可见性,在动画执行期间可见,其余时候隐藏

  • 只要触发了drop事件,不止是drop事件里面的代码会执行,另外几个vue的js监听钩子也会一起按顺序执行

    • 触发了drop事件
    • beforeEnter开始执行
    • enter开始执行
    • afterEnter开始执行
  • drop事件的触发可以通过点击cartcontrol组件的添加小球按钮addCart事件触发使用$emit,也可以父组件this.$refs.shopcart.drop(target);直接触发

    • 这么做的目的是实现,在子组件cartcontrol点击之后,可以将该dom传给父组件goods然后再传给子组件shopcart,(因为目前他们之间的通道就是这样,shopcart子组件并没有导入cartcontrol子组件,所以没有直接通讯)这样就实现了多个组件之间的通讯,从而可以实现需求,例如这里就是实现点击子组件cartcontrol后添加一个动画,将小球滑落到另外一个组件shopcart
  • $emit是触发当前实例上的事件。附加参数都会传给监听器回调。

  • 关于transitionend

  • 关于drop方法,是实现每一个ball的show属性和el属性处理,并且点击一次会自动将一个小球放到dropBalls数组里面,放到里面就代表的是一个小球已经被开始执行动画,但是由于动画是异步的,所以先主动设置.

  • 关于getBoundingClientRect(位移的计算是从左上角开始)

    • 使用getBoundingClientRect获取到当前元素的坐标,然后需要位移的left减去元素的宽获取真正的最终位移x坐标
    • 使用getBoundingClientRect获取到当前元素的坐标,然后需要当前屏幕的高度减去元素的top再减去元素本身的高度获取到真正的最终位移y坐标,并且这个是负数,因为是从左上角往下的方向
  • 关于html重绘

animation的动画

第一、分解运动

上图的曲线运动进行分解
向右:匀速运动;
向上:加速运动;(因为向上的线越来越陡,意味着速度越来越快,所以在做加速运动)

第二、实现代码解释

有了对该运动的本质认识,那么实现起来就很易如反掌了;

**

@keyframes run-right-right {
  0% {
    left: 40px;
    transform: scale(0.7);
  }

  100% {
    left: 600px;
    transform: scale(0.45);
  }
}

——@keyframes 是css3的一个规则,用来定义一个运动的每个桢的位置,大小颜色等;

然后run-right-right就是这个动画特性的名字,很好理解;然后0%就是开始的时候的left值;transform: scale(0.7);变小0.7;时间到100%时 left值变大到600px的位置,然后变小到0.45;(这里的位置,大小都是相对于初始设置的ball类里面的值

**

animation: run-right-right 3s 0.4s 1 linear, run-right-top 3s 0.4s 1 cubic-bezier(.66,.1,1,.41);

——这段代码的意思是动画animation用run-right-right动画和 run-right-top动画,注意我们是同时引用的两个动画;
就是@keyframes 所定义的,然后我们又设置了一些参数,逐一解释:

  • 一.第一个参数就是引用的动画名字;
  • 二,动画持续时间3s;
  • 三、0.4s是延迟时间为0.4s,以run_top_right加到ball上面的时间为准,延后0.4s;
  • 四、1是动画的执行次数是1次;
  • 五、cubic-bezier(.66,.1,1,.41) 就是重要的贝塞尔曲线(cubic-bezier)

实际上是设置animation-timing-function的属性;就是设置运动速度的特性属性;

    1. linear,就是线性运动,也就是匀速运动;
  • 2 ease,默认。动画以低速开始,然后加快,在结束前变慢。
  • 3.ease-in,ease-out,ease-in-out,就很好记,ease就是慢的意思,ease-in就是慢速开始,就是做加速运动,ease-out就是减速运动,ease-in-out就是先加速后减速;当然我们也可以设置成cubic-bezier()值;