在学习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}"
>¥{{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
>