8.动画

在前端网页开发中,动画是一种非常重要的技术。合理运用动画可以极大的提高用户的使用体验。

使用CSS3创建动画

CSS3动画的核心定义keyframestransitionkeyframes定义了动画行为,比如对于颜色渐变的动画,需要定义起始颜色和终止颜色,浏览器会自动帮助我们计算其间的所有中间态来执行动画。transition的使用更加简单,当组件的CSS属性发生变化时,使用transition来定义其过度动画的属性即可。

transition过度动画

首先新建一个名为transition.html的测试文件,在其中编写如下JavaScript、HTML和CSS代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<body>
<style>
.demo{
width: 100px;
height: 100px;
background-color: red;
}
.demo-ani{
width: 200px;
height: 200px;
background-color: blue;
transition: width 2s,height 2s, background-color 2s;
}
</style>
<div id="Application">
<div :class="cls" @click="run">

</div>
</div>
<script>
const App = Vue.createApp({
data(){
return{
cls:"demo"
}
},
methods: {
run(){
if (this.cls == "demo"){
this.cls = "demo-ani"
}else{
this.cls = "demo"
}
}
}
})
App.mount("#Application")
</script>
</body>

如以上代码所示,CSS中定义的demo-ani类中指定了transition属性,这个属性中可以设置要过度的属性以及动画时间,运行上面代码,单击页面中的色块,可以看到色块变大的过程会附带动画效果,颜色变化的过程也附带动画效果。

上面的示例代码实际上使用了简写方式,我们也可以逐条属性对动画进行设置

1
2
3
4
5
6
7
8
9
10
11
12
13
.demo {
width: 100px;
height: 100px;
background-color: red;
/* 设置动画属性 */
transition-property: width, height, background-color;
/* 设置动画执行时常 */
transition-duration: 1s;
/* 设置动画的执行方式,linear表示以线性的方式执行 */
transition-timing-function: linear;
/* 用来进行延时设置,即延时多长时间执行动画 */
transition-delay: 2s;
}

keyframes动画

transition动画适合用来创建简单的过度效果。CSS3中支持使用animation属性来设置更加复杂的动画效果。animation属性根据keyframes配置来执行基于关键帧的动画效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>

</head>
<body>
<style>
/* keyframes用来定义动画的名称和每个关键帧的状态 */
@keyframes animationl{
/* 0%表示动画起始时的状态 */
0% {
background-color: red;
width: 100px;
height: 100px;
}
/* 25%表示动画执行到4/1时的状态 */
25% {
background-color: orchid;
width: 200px;
height: 200px;
}
/* 75%表示动画执行到4/3时的状态 */
75%{
background-color: green;
width: 150px;
height: 150px;
}
/* 100%表示动画终止时的状态 */
100%{
background-color: blue;
width: 200px;
height: 200px;
}
}
.demo{
width: 100px;
height: 100px;
background-color: red;
}
.demo-ani {
animation: animationl 4s linear;
width: 200px;
height: 200px;
background-color: blue;
}
</style>
<div id="Application">
<div :class="cls" @click="run"></div>
</div>
<script>
const App = Vue.createApp({
data() {
return {
cls: "demo",
};
},
methods: {
run() {
if (this.cls == "demo") {
this.cls = "demo-ani";
} else {
this.cls = "demo";
}
},
},
});
App.mount("#Application");
</script>
</body>
</html>

对于每个状态,我们将其定义为一个关键帧,在关键帧中,可以定义元素的各种渲染属性,比如宽和高,位置,颜色等。

在定义keyframes时,如果只关心起始状态与终止状态,也可以这样定义

1
2
3
4
5
6
7
8
9
10
11
12
@keyframes animationl{
from {
background-color: red;
width: 100px;
height: 100px;
}
to {
background-color: blue;
width: 200px;
height: 200px;
}
}

定义好了keyframes关键帧,在编写CSS样式代码时可以使用animation属性为其指定动画效果,如以上代码设置要执行的动画名为animationl的关键动画帧,执行时长为4s,执行方式为线性。animation的这些配置项也可以分别进行设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.demo-ani {
/* 设置关键帧动画名称 */
animation-name: animationl;
/* 设置动画时长 */
animation-duration: 3s;
/* 设置动画播放方式: 渐入渐出 */
animation-timing-function: ease-in-out;
/* 设置动画的播放方向 */
animation-direction: alternate;
/* 设置动画的播放次数 */
animation-iteration-count: infinite;
/* 设置动画的播放状态 */
animation-play-state: running;
/* 设置播放动画的延迟时间 */
animation-delay: 1s;
/* 设置动画播放结束应用的元素样式 */
animation-fill-mode: forwards;
width: 200px;
height: 200px;
background-color: blue;
}

使用Javascript的方式来实现动画

动画的本质是将元素的变化以渐变的方式完成,即将大的状态变化拆分成非常多个小的状态变化,通过不断执行这些变化来达到动画地效果。使用JavaScript代码来启用定时器,按照一定频率进行组件状态地变化也可以实现动画效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<div id="Application">
<div :style="{backgroundColor: 'blue',width:width + 'px',height:height+'px'}" @click="run"></div>
</div>
<script>
const App = Vue.createApp({
data(){
return{
width:100,
height:100,
timer:null
}
},
methods: {
run(){
this.timer = setInterval(this.animation,10)
},
animation(){
if (this.width == 200){
clearInterval(this.timer)
return
}else {
this.width += 1
this.height += 1
}
}
},
})
App.mount("#Application")
</script>

setInterval方法用来开启一个定时器,上面代码中设置没10毫秒执行一次回调函数,在回调函数中,我们逐像素地将色块地尺寸放大,最终就产生了动画效果。使用JavaScript可以更加灵活地控制动画效果,在实际开发中,结合Canvas的使用,Javascript可以实现非常强大的自定义动画效果。需要注意,当动画结束,要使用clearInterval方法将对应的定时器停止。

Vue过度动画

Vue组件在页面中被插入、移出或者更新的时候都可以附带转场效果,即可以展示过度动画。例如,我们使用v-if和v-show指令控制组件的显示和隐藏时,就可以将其过程以动画的方式进行展现。

定义过度动画

Vue的过度动画的核心原理依然是采用CSS类来实现的,只是Vue帮助我们在组件的不同生命周期自动切换不同的CSS类。

Vue中默认提供了一个名为transition的内置组件,可以用其来包装要展示过度动画的组件。transition组件的name属性用来设置要执行的动画名称,Vue中约定了一系列CSS类名规则来定义各个过度过程中的组件状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<style>
.ani-enter-from {
width: 0px;
height: 0px;
background-color: red;
}
.ani-enter-active {
transition: width 2s, height 2s, background-color 2s;
}
.ani-enter-to {
width: 100px;
height: 100px;
background-color: blue;
}
.ani-leave-from {
width: 100px;
height: 100px;
background-color: blue;
}
.ani-leave-active {
transition: width 2s, height 2s, background-color 3s;
}
.ani-leave-to {
width: 0px;
height: 0px;
background-color: red;
}
</style>
<div id="Application">
<button @click="click">显示/隐藏</button>
<transition name="ani">
<div v-if="show"></div>
</transition>
</div>
<script>
const App = Vue.createApp({
data() {
return {
show: false,
};
},
methods: {
click() {
this.show = !this.show;
},
},
});
App.mount("#Application");
</script>
</body>
</html>

上面的核心代码是定义的6个特殊的CSS类,这6个CSS类没有显示地使用,但是其在组件执行动画地过程中起到了不可替代地作用。当我们为transition组件地name属性设置动画名称之后,当组件被插入页面被移除时,会自动寻找以此动画名称开头地CSS类,格式如下:

1
2
3
4
5
6
x-enter-from
x-enter-active
x-enter-to
x-leave-from
x-leave-active
x-leave-to

x 表示定义的过度动画名称。上面6种特殊的CSS类,前3种用来定义组件被插入页面的动画效果,后3种用来定义组件被移出页面的动画效果。

x-enter-from 类在组件即将被插入页面时被添加到组件上,可以理解为组件的初始状态,元素被插入页面后此类会马上被移除。

x-enter-to 类在组件被插入页面后立即被添加,此时 x-enter-from 类会被移除,可以理解为组过渡的最终状态。

x-enter-active 类在组件的整个插入过渡动画中都会被添加,直到组件的过渡动画结束后才会被移除。可以在这个类中定义组件过渡动画的时长、方式、延迟等。

x-leave-fromx-enter-from 相对应,在组件即将被移除时此类会被添加。用来定义移除组件时过渡动画的起始状态。

x-leave-to 则对应地用来设置移除组件动画的终止状态。

x-leave-active 类在组件的整个移除过渡动画中都会被添加,直到组件的过渡动画结束后才会被移除。可以在这个类中定义组件过渡动画的时长、方式、延迟等。

你可能也发现了,上面提到的6种特殊的CSS类虽然被添加的时机不同,但是最终都会被移除因此当动画执行完成后,组件的样式并不会保留,更常见的做法是在组件本身绑定一个最终状态的样式类,示例如下:

1
2
3
<transition name="ani">
<div v-if="show" class="demo"></div>
</transition>

CSS代码如下:

1
2
3
4
5
.demo {
width: 100px;
height: 100px;
background-color: blue;
}

这样,组件的显示或隐藏过程就变得非常流畅。上面的示例代码使用CSS中的transition来实现动画,其实使用animation的关键帧方式定义动画效果也一样的,CSS示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<style>
@keyframes keyframe-in {
from {
width: 0px;
height: 0px;
background-color: red;
}
to {
width: 100px;
height: 100px;
background-color: blue;
}
}
@keyframes keyframe-out {
from {
width: 100px;
height: 100px;
background-color: red;
}
to {
width: 0px;
height: 0px;
background-color: blue;
}
}
.demo {
width: 100px;
height: 100px;
background-color: blue;
}
.ani-enter-from {
width: 0px;
height: 0px;
background-color: red;
}
.ani-enter-active {
transition: width 2s, height 2s, background-color 2s;
}
.ani-enter-to {
width: 100px;
height: 100px;
background-color: blue;
}
.ani-leave-from {
width: 100px;
height: 100px;
background-color: blue;
}
.ani-leave-active {
transition: width 2s, height 2s, background-color 3s;
}
.ani-leave-to {
width: 0px;
height: 0px;
background-color: red;
}
</style>

设置动画过程中的监听回调

我们知道,对于组件的加载或卸载过程,有一系列的生命周期函数会被调用。对于Vue中的专场动画来说,也可以注册一系列的函数来对其监听。示例如下:

1
2
3
4
5
6
7
8
9
10
11
<transition name="ani"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled">
<div v-if="show" class="demo"></div>
</transition>

上面注册的回调方法需要在组件的methods选项中实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
methods: {
// 组件插入过度开始前
beforeEnter(el) {
console.log("beforeEnter")
},
// 组件插入过度开始
enter(el,done){
console.log("enter")
},
// 组件插入过度后
afterEnter(el){
console.log("afterEnter")
},
// 组件插入过度取消
enterCancelled(el) {
console.log("enterCancelled")
},
// 组件移除过度开始前
beforeLeave(el){
console.log("beforeLeave")
},
// 组件移除过度开始
leave(el,done){
console.log("leave")
},
// 组件移除过度后
afterLeave(el){
consolo.log("afterLeave")
},
// 组件移除过度取消
leaveCancelled(el){
consolo.log("leaveCancelled")
}
},

有了这些函数,可以在组件过度动画过程中实现负责的业务逻辑,也可通过JavaScript来自定义过度动画,当我们需要自定义过度动画时,需要将transition组件的CSS属性关掉,代码如下:

1
2
3
4
5
6
<div id="Application">
<button @click="click">显示/隐藏</button>
<transition name="ani" :css="false">
<div v-show="show" class="demo"></div>
</transition>
</div>

有两个函数比较特殊:enter和leave。这两个函数除了会将当前元素作为参数外,还有一个函数类型done参数,如果我们将transition组件的CSS属性关闭,决定使用JavaScript来实现自定义的过度动画,这两个方法中的done函数最后必须被手动调用,否则过度动画会立即完成。

多个组件的过度动画

Vue中的transition组件支持同时包装多个互斥的子组件元素,从而实现多组件的过渡效果。

例如元素A消失的同时展示元素B

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<body>
<style>
.demo {
width: 100px;
height: 100px;
background-color: blue;
}
.demo2 {
width: 100px;
height: 100px;
background-color: blue;
}
.ani-enter-from {
width: 0px;
height: 0px;
background-color: red;
}
.ani-enter-active {
transition: width 3s, height 3s, background-color 3s;
}
.ani-enter-to {
width: 100px;
height: 100px;
background-color: blue;
}
.ani-leave-from {
width: 100px;
height: 100px;
background-color: blue;
}
.ani-leave-active {
transition: width 3s, height 3s, background-color 3s;
}
.ani-leave-to {
width: 0px;
height: 0px;
background-color: red;
}
</style>
<div id="Application">
<button @click="click">显示/隐藏</button>
<transition name="ani">
<div v-if="show" class="demo"></div>
<div v-else class="demo2"></div>
</transition>
</div>
<script>
const App = Vue.createApp({
data() {
return {
show: false,
};
},
methods: {
click(){
this.show = !this.show
},
},
});
App.mount("#Application");
</script>
</body>

运行代码,单击页面上的按钮,可以看到两个色块会以过渡动画的方式交替出现。默认情况下, 两个元素的插入和移除动画会同步进行,有些时候这并不能满足我们的需求,大多数时候需要移除动画执行完成后,再执行插入动画。要实现这一功能非常简单,只需要对transition组件的mode属性进行设置即可,当我们将其设置为out-in时, 就会先执行移除动画,再执行插入动画:将其设置为in-out时t,则会先执行插入动画,再执行移除动面,代码如下:

1
2
3
4
5
6
<transition name="ani" mode="in-out">
<div v-if="show" class="demo">
</div>
<div v-else class="demo2">
</div>
</transition>

列表过度动画

在实际开发中,列表是一种非常流行的页面设计方式。在Vue中,通常使用v-for指令来动态构建列表视图。在动态构建列表视图中,其中的元素经常会有增删、重排等操作,在Vue中使用transition-group组件可以非常方便地实现列表元素变动地动画效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<body>
<style>
.list-enter-active,
.list-leave-active {
transition: all ls ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
}
</style>
<div id="Application">
<button @click="click">添加元素</button>
<transition-group name="list">
<div v-for="item in items" :key="item">
元素:{{ item }}
</div>
</transition-group>
</div>
<script>
const App = Vue.createApp({
data(){
return {
items:[1,2,3,4,5]
}
},
methods: {
click(){
this.items.push(this.items[this.items.length-1] + 1),
console.log(this.items)
}
},
})
App.mount("#Application")
</script>

</body>

上面的代码非常简单,单击页面中的添加元素按钮后,可以看到的列表的元素在增加,并且是以渐进动画的方式插入的。

在使用transition-group组件实现列表动画时,与transition类似,首先需要定义动画所需的CSS类,上面的示例代码中,我们只定义了透明度变化的动画。有一点需要注意,如果要使用列表动画,列表中的每一个元素都需要有一个唯一的key值。如果为上面的列表在添加一个删除元素的功能,它依然会很好地展示动画效果,删除元素方法如下:

1
2
3
4
5
6
7
dele(){
if(this.items.length > 1){
this.items.pop()
}else{
alert("只有一个了,不能在删除了,否则添加元素没有基础值了")
}
}

除了对列表中元素进行插入和删除可以添加动画外,对列表元素地排序过程也可以采用动画来进行过度,只需要额外定义一个v-move类型地特殊动画类即可,例如为上面地代码增加如下CSS类:

1
2
3
.list-move {
transition: transform ls ease;
}

之后可以尝试对列表中地元素进行逆序,Vue会以动画地方式将其中地元素移动到正确地位置。

范例:优化用户列表页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户列表</title>
<script src="https://unpkg.com/vue@next"></script>
<style>
.container {
margin: 50px;
}
.content {
margin: 20px;
}
.tab {
width: 300px;
position: absolute;
}
.item {
border: gray 1px solid;
width: 148px;
text-align: center;
transition: all 0.8s ease;
display: inline-block;
}
.list-enter-active {
transition: all ls ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
}
.list-move {
transition: transform ls ease;
}
.list-leave-active {
position: absolute;
transition: all ls ease;
}
</style>
</head>
<body>
<div id="Application">
<div class="container">
<div class="content">
<input type="radio" :value="-1" v-model="sexFliter"/>全部
<input type="radio" :value="0" v-model="sexFliter"/>
<input type="radio" :value="1" v-model="sexFliter"/>
</div>
<div class="content">搜索:<input type="text" v-model="searchKey" /></div>
<div class="content">
<table border="1" width="300px">
<tr>
<th>姓名</th>
<th>性别</th>
</tr>
<tr v-for="(data, index) in showDatas">
<td>{{data.name}}</td>
<td>{{data.sex == 0 ? '男' : '女'}}</td>
</tr>
</table>
</div>
</div>
</div>
<script>
let mock = [
{
name:"小王",
sex:0
},{
name:"小红",
sex:1
},{
name:"小李",
sex:1
},{
name:"小张",
sex:0
}
]
const App = Vue.createApp({
data(){
return {
sexFliter:-1,
showDatas:[],
searchKey:""
}
},
mounted () {
// 模拟请求过程
setTimeout(this.queryAllData, 3000);
},
methods: {
queryAllData() {
this.showDatas = mock
},
fliterData() {
this.searchKey = ""
if (this.sexFliter == -1) {
this.showDatas = mock
} else {
this.showDatas = mock.filter((data)=>{
return data.sex == this.sexFliter
})
}
},
searchData() {
this.sexFliter = -1
if (this.searchKey.length == 0) {
this.showDatas = mock
} else {
this.showDatas = mock.filter((data)=>{
return data.name.search(this.searchKey) != -1
})
}
},
},
watch: {
sexFliter(oldValue, newValue) {
this.fliterData()
},
searchKey(oldValue, newValue) {
this.searchData()
}
}

})

App.mount("#Application")
</script>
</body>
</html>