Vue 動畫 - Transitions 及 Animation

Vue 封裝好的 transition 元件提供開發者在插入、更新、移除 DOM 時,添加動畫轉場的效果。它提供三種應用方式:

  • CSS 轉場 (CSS transition) & CSS 動畫 (CSS animation)
  • 搭配第三方動畫套件,如 Animation.css
  • 搭配專用的 JavaScript 鉤子函式

使用條件

  • 使用 v-if
  • 使用 v-show
  • 動態元件 (Dynamic Components),如使用 :is ,透過字串變數進行元件切換
  • 元件的根節點(Component Root Nodes)

方法1 - CSS 轉場 & CSS 動畫

預設的轉場類名

使用 <transition> 時,Vue 會在特定時間點將對應的 class 加到元素中,然後再移除,產生進入和離開的漸變效果。

  1. v-enter
    進入轉場開始的狀態。在元素插入前生效,在元素被插入後的下一幀移除

  2. v-enter-active
    進入轉場過程的狀態。在整個進入轉場的階段中應用,在轉場完成之後移除。在這裡可以定義轉場的過程時間,延遲。如: transition: opacity .5s

  3. v-enter-to
    進入轉場結束的狀態。在元素被插入之後下一幀生效(與此同時 v-enter 被移除),在轉場完成之後移除 (2.1.8版以上)

  4. v-leave
    離開轉場的開始狀態。在離開轉場被觸發時立即生效,下一幀被移除

  5. v-leave-active
    離開轉場生效時的狀態。在整個離開轉場的階段中應用,在轉場完成之後移除。在這裡可以定義轉場的過程時間,延遲。如: transition: opacity .5s

  6. v-leave-to
    離開轉場結束的狀態。在 v-leave 被觸發之後下一幀生效(與此同時 v-leave 被移除),在轉場完成之後移除 (2.1.8版以上)


來源: Vue 官方文件

若無自行定義類別的前綴詞,預設是 v- (如上)。若有自行定義需求,則可以在 <transition> 標籤添加 name 屬性,並且將預設類別 v- 的 v 改為自定義名稱。

CSS 轉場

1
2
3
4
5
6
7
<div id="demo">
<button @click="show = !show">Toggle</button>

<transition name="fade">
<p v-if="show">hello</p>
</transition>
</div>
1
2
3
4
5
6
new Vue({
el: '#demo',
data: {
show: true
}
})
1
2
3
4
5
6
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}

See the Pen RzeYmR by Dylan (@dylan_demo) on CodePen.

流程敘述

  • 第一次點擊按鈕,變數 show 被更改為 false,此時元素移除,同時觸發 leaveleave-activeleave-to
  • 第二次點擊按鈕,變數 show 被更改為 true,此時元素插入,同時觸發 enterenter-activeenter-to

另外 v-if 也可以改為 v-show,後者並不會移除元素,而是以 display: blockdisplay: none 做切換。

CSS 動畫

使用 CSS animation 和 CSS transition 基本上沒有太大差異,區別是在動畫中 v-enter 在節點插入 DOM 後不會立即刪除,而是在 animationend 事件觸發時刪除。

1
2
3
4
5
6
<div id="demo">
<button @click="show = !show">Toggle show</button>
<transition name="bounce">
<p v-if="show">Hello</p>
</transition>
</div>
1
2
3
4
5
6
new Vue({
el: '#demo',
data: {
show: true
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.bounce-enter-active {
animation: bounce-in .5s;
}
.bounce-leave-active {
animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}

See the Pen qzJMzB by Dylan (@dylan_demo) on CodePen.

方法2 - 搭配第三方動畫套件 (Animate.css)

客製化的方式除了方法一之外,Vue 也提供了另外一種方式。我們可以直接在 <transition> 標籤內插入屬性,值則為第三方庫寫好的 Class Name。以下範例使用 Animate.css 動畫庫做範例。

預設的轉場屬性

我們可以通過以下屬性來自定義轉場類名,時間點可以對應到上面提到的預設的轉場類名

  • enter-class
  • enter-active-class
  • enter-to-class (2.1.8+)
  • leave-class
  • leave-active-class
  • leave-to-class (2.1.8+)

範例

1
2
3
4
5
6
7
8
9
10
<div id="demo">
<button @click="show = !show">Toggle</button>
<transition
name="custom-classes-transition"
enter-active-class="animated tada"
leave-active-class="animated bounceOutRight"
>
<p v-if="show">hello</p>
</transition>
</div>
1
2
3
4
5
6
new Vue({
el: '#demo',
data: {
show: true
}
})

See the Pen XLxPLG by Dylan (@dylan_demo) on CodePen.

方法3 - JavaScript 鉤子函式

另一種方法則是在 <transition> 元素行內綁定鉤子,可以搭配 CSS 轉場CSS 動畫使用,也可單獨使用。

預設鉤子

  1. beforeEnter(el)
    進入轉場/動畫前啟動

  2. enter(el, callback)
    進入轉場/動畫之元素插入時啟動。done callback 則在結束時呼叫,可傳可不傳。
    方法2所對應的時間點為 enter-class

  3. afterEnter(el)
    進入轉場/動畫時啟動。
    方法2所對應的時間點為 enter-to-class

  4. enterCancelled(el)
    在未完成進入轉場/動畫時取消動作。

  5. beforeLeave(el)
    離開轉場/動畫前啟動

  6. leave(el, callback)
    離開轉場/動畫之元素插入時啟動。done callback 則在結束時呼叫,可傳可不傳。
    方法2所對應的時間點為 leave-class

  7. afterLeave(el)
    離開轉場/動畫時啟動。

  8. leaveCancelled(el)
    在未完成離開轉場/動畫時取消動作。
    方法2所對應的時間點為 leave-to-class

補充:當只用JavaScript 轉場的時候,在enter和leave中必須使用done進行callback。否則,它們將被同步呼叫,轉場會立即完成。

補充:推薦對於僅使用 JavaScript 轉場的元素添加 :css="false",Vue 會跳過 CSS 的檢測。這也可以避免過渡過程中 CSS 的影響。

範例

一個使用 Velocity.js 函式庫的簡單例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="demo">
<button @click="show = !show">
Toggle
</button>
<transition
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
:css="false"
>
<p v-if="show">
Demo
</p>
</transition>
</div>
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
new Vue({
el: '#demo',
data: {
show: false
},
methods: {
beforeEnter(el) {
el.style.opacity = 0
el.style.transformOrigin = 'left'
},
enter(el, done) {
Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 })
Velocity(el, { fontSize: '1em' }, { complete: done })
},
leave(el, done) {
Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 300 })
Velocity(el, { rotateZ: '100deg' }, { loop: 2 })
Velocity(el, {
rotateZ: '45deg',
translateY: '30px',
translateX: '30px',
opacity: 0
}, { complete: done })
}
}
})

See the Pen YoJJKq by Dylan (@dylan_demo) on CodePen.

兩個元素的過場動畫

透過 v-if 同時讓兩個元素進行過場。

1
2
3
4
5
6
7
<div id="demo">
<button @click="show = !show">toggle</button>
<transition name="fade">
<h1 v-if="show">element1</h1>
<h2 v-else>element12</h2>
</transition>
</div>

我們會發現,在動畫執行階段兩個元素會在同個時間交集,造成空間互相擠壓的問題。這並不是我們想要的結果。
而 Vue 的 transition 元件提供了一個解法,我們可以為 transition 標籤加上另一個屬性 mode

  • in-out: 新的元素先執行 enter,待完成後舊元素才執行 leave
  • out-in:舊元素先執行 leave,待完成後新元素才執行 enter (一般符合預期的狀況)

稍作修改

1
<transition name="fade" mode="out-in">

另外有一點需要特別注意,剛剛的例子使用的是兩個不同元素(h1及h2),是可以正常執行的,但是如果兩個元素是相同的就會出現一些問題。

1
2
3
4
5
6
7
<div id="demo">
<button @click="show = !show">toggle</button>
<transition name="fade">
<h1 v-if="show">element1</h1>
<h1 v-else>element12</h1>
</transition>
</div>

我們可以為 transition 標籤加上 key,來解決這個問題,key 內的值需要是唯一值,即每個元素的值不可重複,如此 Vue 才能判斷它為不同的元素,進而正常執行過場。

1
2
3
4
5
6
7
<div id="demo">
<button @click="show = !show">toggle</button>
<transition name="fade">
<h1 v-if="show" key="1">element1</h1>
<h1 v-else key="2">element12</h1>
</transition>
</div>

See the Pen ydRRPr by Dylan (@dylan_demo) on CodePen.

多個元素的過場

前面的例子都是使用 v-if 來做過場效果,最多只有兩個元素,如果使用 v-for 產生的大量元素就必須使用 transition-group 元件。

transition-group 在編譯後預設會被轉換成 span 標籤,由這個標籤包住所有 v-for 渲染出來的元素。例如:

1
2
3
<transition-group name="fade">
<p v-for="num in 3" :key="num"> num </p>
</transition-group>

編譯結果為:

1
2
3
4
5
<span>
<p>1</p>
<p>2</p>
<p>3</p>
</span>

我們可以透過 tag 屬性將預設標籤改掉

1
2
3
<transition-group name="fade" tag="div">
<p v-for="num in 3" :key="num"> num </p>
</transition-group>

編譯結果為:

1
2
3
4
5
<div>
<p>1</p>
<p>2</p>
<p>3</p>
</div>

注意:和 transition 元件一樣,在 transition-group 的元素也必須為它加上 key

使用 bootstrap 卡片元件來製作一個簡易的新增刪除功能的表單,並且使用 <transition-group> 加上動畫效果吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="demo">
<button @click="addCard">新增卡片</button>
<transition-group name="fade" tag="div">
<div class="card mb-3 fade-item" v-for="(item, index) in data" :key="item.title" style="width: 18rem;">
<img src="" class="card-img-top">
<div class="card-body">
<h5 class="card-title"> {{item.title}} </h5>
<p class="card-text"> {{item.text}} - {{ item.timestamp }}</p>
<a href="#" class="btn btn-primary" @click="removeCard(item.timestamp)"> Delete</a>
</div>
</div>
</transition-group>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let app = new Vue({
el: '#demo',
data: {
data: []
},
methods: {
addCard() {
let timestamp = function() {
const dateTime = new Date().getTime();
return Math.floor(dateTime / 100);
}
this.data.push({
timestamp: timestamp(),
title: `標題 ${this.data.length + 1}`,
text: `我是一個內容 ${this.data.length + 1}`,
})
},
removeCard(title) {
this.data = this.data.filter((item)=> {
return item.timestamp !== title;
})
}
},
});

See the Pen Rzevvx by Dylan (@dylan_demo) on CodePen.

重複利用動畫效果

可以使用元件及插槽(Slot)封裝你精心設計的動畫效果,下個專案可以重複利用,這邊使用上面卡片的範例來做。

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="demo">
<button @click="addCard">新增卡片</button>
<list>
<div class="card mb-3 fade-item" v-for="(item, index) in data" :key="item.title" style="width: 18rem;">
<img src="" class="card-img-top">
<div class="card-body">
<h5 class="card-title"> {{item.title}} </h5>
<p class="card-text"> {{item.text}} - {{ item.timestamp }}</p>
<a href="#" class="btn btn-primary" @click="removeCard(item.timestamp)"> Delete</a>
</div>
</div>
</list>
</div>
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
// 區域元件 (二擇一)
let list = {
template: `
<transition-group name="fade" tag="div">
<slot></slot>
</transition-group>
`
}
// 全域元件 (二擇一)
Vue.component('list', {
template: `
<transition-group name="fade" tag="div">\
<slot></slot>\
</transition-group>
`
})

let app = new Vue({
el: '#demo',
components: { // 使用區域元件要加上元件物件
'list': list
},
data: {
data: []
},
methods: {
addCard() {
let timestamp = function() {
const dateTime = new Date().getTime();
return Math.floor(dateTime / 100);
}
this.data.push({
timestamp: timestamp(),
title: `標題 ${this.data.length + 1}`,
text: `我是一個內容 ${this.data.length + 1}`,
})
},
removeCard(title) {
console.log(title)
this.data = this.data.filter((item)=> {
return item.timestamp !== title;
})
}
},
})

See the Pen MMzGYp by Dylan (@dylan_demo) on CodePen.

參考資料

Vue 官方文件
Vue.js (14) - 過場效果及動畫
Vue.js: 樣式與漸變 Transitions
Vue.js: 動畫 Animations
Vue.js: 進階過渡效果