该篇为翻译文章:原文为:Intro to Vue.js: Animations
该系列文章目录:
Vue.js #1-渲染、指令、事件(原文)Vue.js #3-Vue-cli及生命周期(原文)
Vue.js #4-Vuex (原文)Vue.js #5-动画 (当前)
一些背景
在Vue当中,有像<transition>
和</transition><transition -group>
这样的内置组件供我们使用。如果你以前用过React的话,可能用过它里面的ReactCSSTransitionGroup
组件,其实它们是很相似的,但仍有一些让人眼前一亮的不同点。
我们先从CSS的Transitions动画开始说起,然后就是CSS的Animations动画,再然后就是JS控制动画及通过生命周期的勾子函数还控制动画。这篇文章中将不会社稷到「过渡动画」,但这也比较容易。这里有我写的一个关于「过渡动画」的例子,我可能将会针对它单独写一篇文章。
译者:原文这中间介绍了CSS3的Transition和Animation的区别,这里我没有翻译,如果有读者对此不是很了解,可以去查阅相关资料。
CSS Transitions
假如现在有一个非常简单的弹窗。我们通过点击一个按钮让它显示或隐藏。通过之前的文章我们可以写出大概的结构:新建一个Vue实例,里面有一个按钮,然后在里面新建一个组件,通过设置数据状态来控制组件的显示与隐藏,当然,需要使用到v-if
或v-show
来根据数据状态控制组件是否可见。最后也可以通过<slot>
传一个关闭按钮到弹窗中。
<div id="app">
<h3>Let's trigger this here modal!</h3>
<button @click="toggleShow">
<span v-if="isShowing">Hide child</span>
<span v-else>Show child</span>
</button>
<app-child v-if="isShowing" class="modal">
<button @click="toggleShow">
Close
</button>
</app-child>
</div>
<script type="text/x-template" id="childarea">
<div>
<h2>Here I am!</h2>
<slot></slot>
</div>
</script>
const Child = {
template: '#childarea'
};
new Vue({
el: '#app',
data() {
return {
isShowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
},
components: {
appChild: Child
}
});
结果:http://codepen.io/sdras/pen/df955fc6a1f482dcf104f2648746f8b3
这样,弹窗是可以工作的,但是没动画,太呆板了~
我们已经通过使用v-if
来安装和卸载组件了,现在,我们只需要添加transition
组件就可以实现动画:
<transition name="fade">
<app-child v-if="isShowing" class="modal">
<button @click="toggleShow">
Close
</button>
</app-child>
</transition>
我们只是在组件外面加了一个<transition>
。然后,我们可以通过我们在CSS当中写的一些以v-
开头的动画样式进行动画。enter/leave
用来设置动画的起点状态,enter-active/leave-active
里面用来设置一些动画属性(例如动画时间,延时,属性等等),enter-to/leave-to
则是设置动画的终点状态。
通过一张图来理解比较好:
就我个人而言,我不喜欢使用默认的以v-
来写样式。我会给transition组件取个动画名字:name="fade"
。
那么样式可以这样来写:
.fade-enter-active, .fade-leave-active {
transition: opacity 0.25s ease-out;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
注意,我给动画的两个状态都加了ease-out
的动画方式。这种动画方式针对于我当前的这个opacity进行动画是效果比较好的。但是当你对一些transform属性进行动画的时候,可能就需要对enter-active和enter-leave进行单独的设置,例如给enter-active添加ease-out
,给enter-leave添加ease-in
。总之效果需根据实际情况来进行调整。
我们把.fade-enter
和.fade-to
都设置了opacity:0
,这个是动画的起点状态和终点状态。你可能觉得需要加一个opacity:1
给.fade-enter-to
和.fade-leave
,但这是没有必要的,因为组件的默认状态就是这样的。
结果:http://codepen.io/sdras/pen/6ef951b970faf929d8580199fe8ea6ba
动画可以正常工作了!但是,如果我们现在想在弹窗显示的时候把背景进行淡化该怎么办呢?我们不能针对背景元素进行使用</transition><transition>
组件,因为它只会在组件被安装和卸载的时候才会起作用,而我们的背景是一直待在那里的。我们可以来改变背景元素的class来应用CSS动画:
<div v-bind:class="[isShowing ? blurClass : '', bkClass]">
<h3>Let's trigger this here modal!</h3>
<button @click="toggleShow">
<span v-if="isShowing">Hide child</span>
<span v-else>Show child</span>
</button>
</div>
.bk {
transition: all 0.1s ease-out;
}
.blur {
filter: blur(2px);
opacity: 0.4;
}
new Vue({
el: '#app',
data() {
return {
isShowing: false,
bkClass: 'bk',
blurClass: 'blur'
}
},
...
});
结果:http://codepen.io/sdras/pen/4daa105fc18da0e223b6be9a200b349d
CSS Animation
现在,我们了解了transition是怎么工作的了,现在我们使用animation动画的话,还是使用</transition><transition>
组件,仍然可以给它取一个名字用来和CSS进行关联。唯一不同的是,我们只需要设置结束的状态并且它中间的动画方式即可,那么中间的动画形式则使用@keyframes
来设置。
之前使用transition动画时,我们知道了通过给</transition><transition>
设置一个名字来应用对应的CSS。而现在,我们直接指定不同的class来应用不同的动画,比如:
enter-active-class="toasty"
leave-active-class="bounceOut"
这就意味着,我们可以使用一些现成的CSS动画库(因为可以直接指定class名)。
现在,我们让一个球「跳」进来,并且「滚」出去:
<div id="app">
<h3>Bounce the Ball!</h3>
<button @click="toggleShow">
<span v-if="isShowing">Get it gone!</span>
<span v-else>Here we go!</span>
</button>
<transition
name="ballmove"
enter-active-class="bouncein"
leave-active-class="rollout">
<div v-if="isShowing">
<app-child class="child"></app-child>
</div>
</transition>
</div>
「跳」进来CSS:
@mixin ballb($yaxis: 0) {
transform: translate3d(0, $yaxis, 0);
}
@keyframes bouncein {
1% { @include ballb(-400px); }
20%, 40%, 60%, 80%, 95%, 99%, 100% { @include ballb() }
30% { @include ballb(-80px); }
50% { @include ballb(-40px); }
70% { @include ballb(-30px); }
90% { @include ballb(-15px); }
97% { @include ballb(-10px); }
}
.bouncein {
animation: bouncein 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
.ballmove-enter {
@include ballb(-400px);
}
「滚」出去CSS:
@keyframes rollout {
0% { transform: translate3d(0, 300px, 0); }
100% { transform: translate3d(1000px, 300px, 0); }
}
@keyframes ballroll {
0% { transform: rotate(0); }
100% { transform: rotate(1000deg); }
}
.rollout {
width: 60px;
height: 60px;
animation: rollout 2s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
div {
animation: ballroll 2s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
}
}
结果:http://codepen.io/sdras/pen/pRWxGg
Transition Modes-动画模式
如果你在一个组件进行消失的时候,让另一个组件进行动画载入,你可能会发现这样一个奇怪的现象(这个例子来自官方文档):
对此,Vue提供了两种「动画模式」供我们使用:
- In-out:当前元素必须等新元素的进入动画完成后才触发动画。
- Out-in:当前元素先进行消失动画,结束之后,新元素再进行进入动画。
看下面这个例子,是我们加了out-in
动画模式之后的效果:
<transition name="flip" mode="out-in">
<slot v-if="!isShowing"></slot>
<img v-else src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/cartoonvideo14.jpeg" />
</transition>
结果:http://codepen.io/sdras/pen/mRpoOG
如果我们把out-in
模式去掉,结果则不是我们想要的。
JS动画
</transition><transition>
为我们提供了一些很好用的JS钩子函数。所有的钩子函数都接收一个el
参数(即element-元素的简称),enter和leave还会传入另外一个done
参数,看名称就应该能猜出来,它是动画结束时的回调。注意,我们需要把css绑定一个false
来关闭CSS动画,从而使用JS动画来取而代之:
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-Leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
:css="false">
</transition>
当然,一个最基本的动画需要包含「进入」和「退出」:
<transition
@enter="enterEl"
@leave="leaveEl"
:css="false">
<!-- put element here-->
</transition>
methods: {
enterEl(el, done) {
//entrance animation
done();
},
leaveEl(el, done) {
//exit animation
done();
},
}
现在,让我们引入GreenSock动画来完成JS动画:
new Vue({
el: '#app',
data() {
return {
message: 'This is a good place to type things.',
load: false
}
},
methods: {
beforeEnter(el) {
TweenMax.set(el, {
transformPerspective: 600,
perspective: 300,
transformStyle: "preserve-3d",
autoAlpha: 1
});
},
enter(el, done) {
...
tl.add("drop");
for (var i = 0; i < wordCount; i++) {
tl.from(split.words[i], 1.5, {
z: Math.floor(Math.random() * (1 + 150 - -150) + -150),
ease: Bounce.easeOut
}, "drop+=0." + (i/ 0.5));
...
}
}
});
结果:http://codepen.io/sdras/pen/MJedjd
上面的动画中有两个比较有趣的点是需要注意的。第一,我在GreenSock的Timeline实例中设置了{onComplete:done}
,即在动画结束时执行回调函数。第二,在beforeEnter
中加入了TweenMax.set
的代码来在动画开始之前设置文字的一些初始属性,这里设置的是transform-style: preserve-3d
。
注意,我们当然也可以直接用CSS来设置初始化属性状态。一个经常被问到的问题就是,我们到底应该是把初始化属性写在CSS里还是写在TweenMax.set
(JS)里呢?一般说来,我会把它都写在TweenMax.set
当中,因为这样只需要改一个地方,比较容易维护。
生命周期里的动画
之前说的一些动画应该比较好懂。可是,如果你需要一些比较复杂的动画,比如需要操作大量的DOM元素,该怎么办呢?这个时候就需要一些生命周期钩子函数介入了。下面的例子当中,我们同时使用了</transition><transition>
组件和mounted()
方法来创建动画。
结果:http://codepen.io/sdras/pen/YNpaoJ
当我们在进行单个元素的动画的时候,我们会用到</transition><transition>
组件,比如,手机的「主页按钮」显示动画:
<transition
@before-enter="beforeEnterStroke"
@enter="enterStroke"
:css="false"
appear>
<path class="main-button" d="M413,272.2c5.1,1.4,7.2,4.7,4.7,7.4s-8.7,3.8-13.8,2.5-7.2-4.7-4.7-7.4S407.9,270.9,413,272.2Z" transform="translate(0 58)" fill="none"/>
</transition>
beforeEnterStroke(el) {
el.style.strokeWidth = 0;
el.style.stroke = 'orange';
},
enterStroke(el, done) {
const tl = new TimelineMax({
onComplete: done
});
tl.to(el, 0.75, {
strokeWidth: 1,
ease: Circ.easeOut
}, 1);
tl.to(el, 4, {
strokeWidth: 0,
opacity: 0,
ease: Sine.easeOut
});
},
但是,当我们的第一个天气组件显示的时候,会有30个甚至更多的元素进行动画,这个时候如果每个元素都加上</transition><transition>
的话就没那么灵活和高效率了。所以,我们可以把动画都加到mounted()
当中,也就是当组件被载入的时候加载动画:
const Tornadoarea = {
template: '#tornadoarea',
mounted () {
let audio = new Audio('https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/tornado.mp3'),
tl = new TimelineMax();
audio.play();
tl.add("tornado");
//tornado timeline begins
tl.staggerFromTo(".tornado-group ellipse", 1, {
opacity: 0
}, {
opacity: 1,
ease: Sine.easeOut
}, 0.15, "tornado");
...
}
};
结语
虽然这个系列文章已经社稷了很多东西,但它终归不会像官方文档那样很详细,仍然有很多东西是我没的提及到的,例如:routing,mixins,服务端渲染等等。下面,列出一些学习资源给大家: