Vue2.0实现高仿饿了么项目里的小球飞入动画

在学习Vue.js高仿饿了么项目的过程中,有一个小球飞入购物车的动画效果。项目是基于vue1.0的,如果是vue2.0的项目,该如何实现呢?自己也花时间研究了一会,从迷惑不解,各种尝试未果,到后来咬文嚼字研读vue 2.0官网关于过渡的章节,再到最终实现效果,心情十分愉悦,同时也算对vue2.0 transition 动画也有所体会和掌握。记录于此,分享大家!

先看下效果


在实现效果的过程中,我的体会有如下几点:

1:多种transition过渡动画,往往套路是内外两层来层或多层来实现,每一层实现不同的transition;

2:   样式部分,从不可见到可见,是enter 相关的样式:.xx-enter,.xx-enter-to,.xx-enter-active;

从可见到不可见,是leave相关的样式:.xx-leave, .xx-leave-to, .xx-leave-active;

其中..xx-enter/leave-active 写的套路是:transition: all .5s linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);

等模式,引用官网的中文翻译是:“这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数

linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier等选项 是属于CSS3 animation-timing-function 属性的值,看到这里随便也把w3cschool里几个和动画相关的属性也一起学习了下:

比如:transition属性是一个速记属性有四个属性:transition-property, transition-duration, transition-timing-function, and transition-delay

其中cubic-bezier 可以查看下:http://cubic-bezier.com/ 从中拖拽自己想要的过渡曲线函数;

3:既然是过渡,一定有开始的状态和结束的状态,在vue.js中即可以通过transition 的js 钩子函数写类似 object.style.transform的方式 也可以通过vue.js自动添加的css样式来规定,例如这里的小球飞入动画,不参考视频里的js的实现方式,用css来完成就是下面的写法:

template结构:

<div class="ball-wrapper">

      <transition-group name="drop" tag="div">
        <div class="ball" v-for="(ball,index) in balls" v-show="ball.show" :key="index">
          <div class="inner inner-hook"></div>
         </div>
      </transition-group>

</div>

stylus样式写法:

.ball-wrapper
.ball
position fixed
left 32px
bottom 22px
z-index 200
background-color red
.inner
width 15px
height 15px
border-radius 50%
background-color #00A0DC
transition all 1s linear
&.drop-enter-active
transition all 1s cubic-bezier( 0.49, -0.29, 0.75, 0.41)
&.drop-enter
transform translate3d( 0, -400px, 0)
.inner
transform translate3d( 300px, 0, 0)
&.drop-enter-to
transform translate3d( 0, 0, 0)
.inner
transform translate3d( 0, 0, 0)


上面代码中drop-enter规定了动画开始的位置,把小球移动到 (x=300px,y=-400px)的位置;.drop-enter-to规定了动画结束时的状态,位置是回到自己的原点,然后水平使用 linear曲线1s内完成,垂直使用 曲线函数cubic-bezier(0.49, -0.29, 0.75, 0.41) 来完成;

4:wrapper和inner 两层动画实际上各自独立进行,同时又因为父容器包裹inner ,所以inner的垂直变化受父容器的曲线函数影响,产生小球的抛物线效果,从background-color:red上 可以很明显看出;

5:知道了这个原理又实际看到了效果,再对照视频的做法,用js实现就很容易理解和实现了。

js部分的关键代码如下,这里没有考虑小球用完5个结束的情形

beforeEnter (el, done) {
let count = this.balls.length;
while (count--) {
let ball = this.balls[count];
if (ball.show) {
let rect = ball.el.getBoundingClientRect();
let x = rect.left - 32;
let y = -(window.innerHeight - rect.top - 22);
// el.style.display = '';
el.style.transform = `translate3d(0, ${y } px,0`;
el.style.webkitTransform = `translate3d(0, ${y } px,0`;
let inner = el.getElementsByClassName( 'inner-hook')[ 0];
inner.style.webkitTransform = `translate3d( ${x } px,0,0)`;
inner.style.transform = `translate3d( ${x } px,0,0)`;
// console.log(el);
}
}
},
dropEnter (el, done) {
/* eslint-disable no-unused-vars */
/* 触发浏览器重绘; */
let rf = el.offsetHeight;
this.$nextTick(() => {
el.style.webkitTransform = 'translate3d(0, 0, 0)';
el.style.transform = 'translate3d(0, 0, 0)';
let inner = el.getElementsByClassName( 'inner-hook')[ 0];
inner.style.webkitTransform = 'translate3d(0, 0, 0)';
inner.style.transform = 'translate3d(0, 0, 0)';
el.addEventListener( 'transitionend', done);
// done();
});
console.log(el);
// done();
}

6:dropEnter中 el.addEventListener('transitionend', done); 这句如果没有,则不会触发transition 的after-enter 事件,导致小球状态不能被还原;但如果 是直接 done(); 则会看不到过渡的动画效果;done做了什么,未做研究,可能需要看vue.js的源代码了!

7:触发浏览器重绘 发现注释也是没有差别;因为重绘,可能需要等待DOM完全加载完成,所以这里用到了this.$nextTick .

完整的代码:

< template >
< div class= "shopchart-wrapper" >
< div class= "content-left" >
< div class= "logo-wrapper" >
< div class= "logo" :class="{ 'hightlight': this.totalCount> 0}" >
< i class= "icon-shopping_cart" :class="{ 'hightlight': this.totalCount > 0}" ></ i >
</ div >
< div v-show=" this.totalCount > 0" class= "number" >{{totalCount}} </ div >
</ div >
< div class= "price" :class="{ 'highlight': this.totalPrice > 0}" >&yen;{{totalPrice}}元 </ div >
< div class= "desc" >另需配送费{{deliveryPrice}}元 </ div >
</ div >
< div class= "content-right" >
< div class= "pay" :class="payableStyle" >{{paydesc}} </ div >
</ div >
< div class= "ball-wrapper" >
< transition-group name= "drop" tag= "div"
v-on:before-enter="beforeEnter"
v-on:enter="dropEnter"
v-on:after-enter="afterEnter" >
< div class= "ball" v-for="(ball,index) in balls" v-show="ball.show" :key="index" >
< div class= "inner inner-hook" >
</ div >
</ div >
</ transition-group >
</ div >
</ div >
</ template >

< script >
export default {
props: {
'selectedFoods': {
type: Array,
default () {
return [];
}
},
'delivery-price': {
type: Number,
default: 0
},
'min-price': {
type: Number,
default: 0
}
},
data () {
return {
balls: [
{
show: false, el: null
},
{
show: false, el: null
},
{
show: false, el: null
},
{
show: false, el: null
},
{
show: false, el: null
}
],
droppedBalls: []
};
},
computed: {
totalPrice () {
let _totalPrice = 0.0;
this.selectedFoods.forEach((f) => {
// console.log(this.selectedFoods.length);
_totalPrice += f.price * f.count;
});
// console.log(_totalPrice);
return _totalPrice;
},
totalCount () {
let _totalCount = 0;
this.selectedFoods.forEach((f) => {
_totalCount += f.count;
});
return _totalCount;
},
paydesc () {
if ( this.totalPrice === 0) {
return `¥ ${ this.minPrice } 元起送`;
}
let _leftPrice = this.minPrice - this.totalPrice;
if ( this.totalPrice < this.minPrice) {
return `还差¥ ${_leftPrice } 元起送`;
} else {
return '去结算';
}
},
payableStyle () {
return {
'payable': this.totalPrice >= this.minPrice,
'not-enough': this.totalPrice > 0 && this.totalPrice < this.minPrice
};
}
},
methods: {
dropMove (el) {
for ( var i = 0; i < this.balls.length; i++) {
let b = this.balls[i];
if (!b.show) {
b.show = true;
b.el = el;
this.droppedBalls.push(b);
return;
}
}
},
beforeEnter (el, done) {
let count = this.balls.length;
while (count--) {
let ball = this.balls[count];
if (ball.show) {
let rect = ball.el.getBoundingClientRect();
let x = rect.left - 32;
let y = -(window.innerHeight - rect.top - 22);
el.style.display = '';
el.style.transform = `translate3d(0, ${y } px,0`;
el.style.webkitTransform = `translate3d(0, ${y } px,0`;
let inner = el.getElementsByClassName( 'inner-hook')[ 0];
inner.style.webkitTransform = `translate3d( ${x } px,0,0)`;
inner.style.transform = `translate3d( ${x } px,0,0)`;
// console.log(el);
}
}
},
dropEnter (el, done) {
/* eslint-disable no-unused-vars */
/* 触发浏览器重绘; */
let rf = el.offsetHeight;
this.$nextTick(() => {
el.style.webkitTransform = 'translate3d(0, 0, 0)';
el.style.transform = 'translate3d(0, 0, 0)';
let inner = el.getElementsByClassName( 'inner-hook')[ 0];
inner.style.webkitTransform = 'translate3d(0, 0, 0)';
inner.style.transform = 'translate3d(0, 0, 0)';
el.addEventListener( 'transitionend', done);
// done();
});
// console.log(el);
// done();
},
afterEnter (el) {
el.style.display = 'none';
let ball = this.droppedBalls.shift();
ball.show = false;
ball.el = null;
console.log(el);
}
}
};
</ script >

<!-- Add "scoped" attribute to limit CSS to this component only -->
< style lang= 'stylus' rel= 'stylesheet/stylus' scoped >
.shopchart-wrapper
position fixed
left 0
bottom 0
z-index 10
height 48px
width 100%
display flex
font-size 0
background-color #141d27
.content-left
flex 1
.logo-wrapper
display inline-block
vertical-align top
position relative
top -10px
margin 0 6px
padding 6px
width 56px
height 56px
box-sizing border-box
border-radius 50%
background #141d27
.logo
width 100%
height 100%
border-radius 50%
background #2b343c
text-align center
.icon-shopping_cart
line-height 44px
font-size 24px
color #80858a
&.hightlight
color #fff
&.hightlight
background rgb( 0, 160, 220)
.number
position absolute;
top 0
right 0
width 24px
height 16px
line-height 16px
// margin-top -5px
font-size 9px
font-weight 700
color white
text-align center
border-radius 16px
background-color rgb( 240, 20, 20)
box-shadow 0 4px 8px 0 rgba( 0, 0, 0, 0.5)
.price
display inline-block
vertical-align top
padding-right 12px
line-height 24px
margin-top 12px
border-right 1px solid rgba( 255, 255, 255, 0.1)
font-size 14px
font-weight 700px
color rgba( 255, 255, 255, 0.4)
&.highlight
color #fff
.desc
display inline-block
vertical-align top
margin 12px 0 0 12px
line-height 24px
font-size 10px
color rgba( 255, 255, 255, 0.4)
.content-right
flex 0 0 105px
width 105px
.pay
height 48px
width 100%
line-height 48px
text-align center
font-size 12px
font-weight 700
color rgba( 255, 255, 255, 0.4)
background-color #2b333b
&.payable
color #fff
background-color #00b43c
&.not-enough
color gray
background-color #2b333b
.ball-wrapper
.ball
position fixed
left 32px
bottom 22px
z-index 200
// background-color red
.inner
width 15px
height 15px
border-radius 50%
background-color #00A0DC
transition all 1s linear
&.drop-enter-active
transition all 1s cubic-bezier( 0.49, -0.29, 0.75, 0.41)
// &.drop-enter
// transform translate3d(0, -400px, 0)
// .inner
// transform translate3d(300px, 0, 0)
// &.drop-enter-to
// transform translate3d(0, 0, 0)
// .inner
// transform translate3d(0, 0, 0)
// .inner
// transform translate3d(0, 0, 0)
// .inner
// transform translate3d(300px, -400px, 0)
// transform translate3d(300px, 0, 0)
</ style >