# [ 想入門,我陪你 ] Re Vue 重頭說起|Day 13:動畫與進退場
###### tags: `Vue`、`Re:Vue 重頭說起`、`Alex 宅幹嘛`
### Transitioning Single Elements/Components
#### CSS Transitions
以下四個情況會觸發動畫效果
* Conditional rendering (using **v-if**)
* Conditional display (using **v-show**)
* Dynamic components
* Component root nodes
6個狀態: 進場開始 - 正在進場 - 進場到哪 - 退場開始 - 正在退場 - 退場到哪
![](https://i.imgur.com/KKT8mac.png)
```htmlmixed=
<!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>Day 13</title>
<style>
.fade-enter {
opacity: 0
}
.fade-enter-active {
// 一個class只能跑一次 transition
// transition: all 0.5s 比較不保險
transition: opacity 0.5s
}
.fade-enter-to {
opacity: 1
}
.fade-leave {
opacity: 1
}
.fade-leave-active {
transition: opacity 0.5s
}
.fade-leave-to {
opacity: 0
}
</style>
</head>
<body>
<div id="app">
<!-- transition component 只能包一個 DOM,放兩個以上動畫會壞掉 -->
<transition name="fade">
<p v-if="show">Hello World</p>
</transition>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: '#app',
data() {
return {
show: true,
}
}
})
</script>
</body>
</html>
```
#### CSS Animation
```htmlmixed=
<!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>Day 13</title>
<style>
/* 用 animation 就不用指定開始跟結束 */
.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);
}
}
</style>
</head>
<body>
<div id="app">
<!-- transition component 只能包一個 DOM,放兩個以上動畫會壞掉 -->
<transition name="bounce">
<p v-if="show">Hello World</p>
</transition>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: '#app',
data() {
return {
show: true,
}
}
})
</script>
</body>
</html>
```
#### Custom Transition Classes
概念就是DOM自動幫你加上 Class,適合配上第三方套件的 class (animate.css)
> 不用寫 name !
```htmlmixed=
<!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>Day 13</title>
<style>
/* 用 animation 就不用指定開始跟結束 */
.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);
}
}
</style>
</head>
<body>
<div id="app">
<!-- transition component 只能包一個 DOM,放兩個以上動畫會壞掉 -->
<transition
enter-active-class="__animated__tada"
leave-active-class="__animated__bounceOutRight"
>
<p v-if="show">Hello World</p>
</transition>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: '#app',
data() {
return {
show: true,
}
}
})
</script>
</body>
</html>
```
> animate css 版本升級到 4.xx.xx版,所以跟Vue官網上的class name範例不太ㄧ樣
#### Using Transitions and Animations Together
在 Transitions 可以用 [type](https://vuejs.org/v2/api/#transition)
:::info
40:00
**type** - string, Specifies the type of transition events to wait for to determine transition end timing. Available values are "**transition**" and "**animation**". By default, it will automatically detect the type that has a longer duration.
:::
#### Explicit Transition Durations
=> 自訂動畫時間,Vue 幫你拔掉動畫的 class
> New in 2.2.0+
```htmlmixed=
<!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>Day 13</title>
<style>
/* 用 animation 就不用指定開始跟結束 */
.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);
}
}
</style>
</head>
<body>
<div id="app">
<!-- transition component 只能包一個 DOM,放兩個以上動畫會壞掉 -->
<transition name="bounce" :duration="1000">
<p v-if="show">Hello World</p>
</transition>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: '#app',
data() {
return {
show: true,
}
}
})
</script>
</body>
</html>
```
#### JavaScript Hooks
el: 得到正在處理動畫的element,至樣就不用使用 querySelector
done: 在每個階段有 done 這個callback表示完成
加入 Jquery(也可以搭配 Velocity)
![](https://i.imgur.com/zdHZrQO.jpg)
```htmlmixed=
<!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>Day 13</title>
<style>
/* 用 animation 就不用指定開始跟結束 */
.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);
}
}
</style>
</head>
<body>
<div id="app">
<!-- transition component 只能包一個 DOM,放兩個以上動畫會壞掉 -->
<transition
appear
@before-enter="beforeEnterHandler"
@after-enter="afterEnterHandler"
@enter="enterHandler"
@leave="leaveHandler" :css="false"
>
<p v-if="show">Hello World</p>
</transition>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: '#app',
data() {
return {
show: true,
}
},
methods: {
beforeEnterHandler(el, done) {
// 要先做,不但當 fadeIn 時若是 1,動畫會看起來沒出現
$(el).css({ opacity: 0 });
},
beforeEnterHandler(el, done) {
// $(el).removeAttr('style');
$(el).css({ opacity: "" });
},
enterHandler(el, done) {
//$(el).fadeIn(400, done);
$(el).fadeTo(400, 1, done);
},
leaveHandler(el, done) {
$(el).fadeOut(400, done);
},
}
})
</script>
</body>
</html>
```
:::warning
When using JavaScript-only transitions, **the done callbacks are required for the enter and leave hooks**. Otherwise, the hooks will be called synchronously and the transition will finish immediately
:::
> done 若不加上,動畫會立即結束
:::danger
It’s also a good idea to explicitly add v-bind:css="false" for **JavaScript-only** transitions so that Vue can skip the CSS detection. This also **prevents CSS rules** from accidentally interfering with the transition.
:::
因為 transition 的 css 屬性預設是 true,所以當要使用 js hook 做動畫時,要改成 false
[Doc](https://vuejs.org/v2/api/#transition)
:::warning
**css** - boolean, Whether to apply CSS transition classes. Defaults to **true**. If set to **false**, will only trigger JavaScript hooks registered via component events.
:::
> 加上 appear 可以讓網頁loading時,進入畫面也有動畫效果
#### Transitions on Initial Render
一進場,開畫面就想要進場效果(appear)
> 也可以用 v-on:appear 加上自訂的 class
#### Transitioning Between Elements
```htmlmixed=
<!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>Day 13</title>
<style>
/* 用 animation 就不用指定開始跟結束 */
.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);
}
}
</style>
</head>
<body>
<div id="app">
<!-- transition component 只能包一個 DOM,放兩個以上動畫會壞掉 -->
<transition
appear
@before-enter="beforeEnterHandler"
@after-enter="afterEnterHandler"
@enter="enterHandler"
@leave="leaveHandler" :css="false"
>
<!-- 這樣沒動畫 -->
<p v-if="show">Hello World</p>
<p v-if="!show">GoogleByb World</p>
<!-- 這樣沒動畫 -->
<p v-if="show">Hello World</p>
<p v-else>GoogleByb World</p>
<!-- 這樣有動畫 -->
<p v-if="show">Hello World</p>
<div v-else>GoogleByb World</p>
<!-- 這樣有動畫 -->
<p v-if="show" key="1">Hello World</p>
<p v-else key="2">GoogleByb World<p/>
</transition>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: '#app',
data() {
return {
show: true,
}
},
methods: {
beforeEnterHandler(el, done) {
// 要先做,不但當 fadeIn 時若是 1,動畫會看起來沒出現
$(el).css({ opacity: 0 });
},
beforeEnterHandler(el, done) {
// $(el).removeAttr('style');
$(el).css({ opacity: "" });
},
enterHandler(el, done) {
//$(el).fadeIn(400, done);
$(el).fadeTo(400, 1, done);
},
leaveHandler(el, done) {
$(el).fadeOut(400, done);
},
}
})
</script>
</body>
</html>
```
> 如果Vue是同一個 tag,他只會換掉Content,所以要額外加 key,這樣DOM才會變換觸發動畫
雖然只有一個 Element,但是 key 不一樣,所以Vue把他當作2個Elment操作,動畫照樣出現
```htmlmixed=
<!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>Day 13</title>
<style>
/* 用 animation 就不用指定開始跟結束 */
.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);
}
}
</style>
</head>
<body>
<div id="app">
<!-- transition component 只能包一個 DOM,放兩個以上動畫會壞掉 -->
<transition
appear
@before-enter="beforeEnterHandler"
@after-enter="afterEnterHandler"
@enter="enterHandler"
@leave="leaveHandler" :css="false"
>
<p :key="show">Hello World: {{ show }}</p>
</transition>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: '#app',
data() {
return {
show: true,
}
},
methods: {
beforeEnterHandler(el, done) {
// 要先做,不但當 fadeIn 時若是 1,動畫會看起來沒出現
$(el).css({ opacity: 0 });
},
beforeEnterHandler(el, done) {
// $(el).removeAttr('style');
$(el).css({ opacity: "" });
},
enterHandler(el, done) {
//$(el).fadeIn(400, done);
$(el).fadeTo(400, 1, done);
},
leaveHandler(el, done) {
$(el).fadeOut(400, done);
},
}
})
</script>
</body>
</html>
```
![](https://i.imgur.com/j79dyr3.jpg)
所以這招可以省Code如下
```htmlmixed=
<transition>
<button v-if="docState === 'saved'" key="saved">
Edit
</button>
<button v-if="docState === 'edited'" key="edited">
Save
</button>
<button v-if="docState === 'editing'" key="editing">
Cancel
</button>
</transition>
```
等同
```htmlmixed=
<transition>
<button v-bind:key="docState">
{{ buttonMessage }}
</button>
</transition>
computed: {
buttonMessage: function () {
switch (this.docState) {
case 'saved': return 'Edit'
case 'edited': return 'Save'
case 'editing': return 'Cancel'
}
}
}
```
#### Transition Modes
預設效果,兩個同時跑,一個退一進
##### out-in 先出再進 1:18:00
```htmlmixed=
<transition
appear
@before-enter="beforeEnterHandler"
@after-enter="afterEnterHandler"
@enter="enterHandler"
@leave="leaveHandler"
:css="false"
mode="out-in"
>
<p :key="show">Hello World: {{ show }}</p>
</transition>
```
##### in-out 先進再出 1:18:25
```htmlmixed=
<transition
appear
@before-enter="beforeEnterHandler"
@after-enter="afterEnterHandler"
@enter="enterHandler"
@leave="leaveHandler"
:css="false"
mode="in-out"
>
<p :key="show">Hello World: {{ show }}</p>
</transition>
```
### Transitioning Between Components
transition 內放 component,用 is 指定 dynamic component
```htmlmixed=
<!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>Day 13</title>
<style>
/* 用 animation 就不用指定開始跟結束 */
.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);
}
}
</style>
</head>
<body>
<div id="app">
<!-- transition component 只能包一個 DOM,放兩個以上動畫會壞掉 -->
<transition
appear
@before-enter="beforeEnterHandler"
@after-enter="afterEnterHandler"
@enter="enterHandler"
@leave="leaveHandler" :css="false"
>
<!-- 類似 router 的 <view> -->
<component v-bind:is="view"></component>
</transition>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: '#app',
data() {
return {
show: true,
view: 'v-a'
}
},
methods: {
beforeEnterHandler(el, done) {
// 要先做,不但當 fadeIn 時若是 1,動畫會看起來沒出現
$(el).css({ opacity: 0 });
},
beforeEnterHandler(el, done) {
// $(el).removeAttr('style');
$(el).css({ opacity: "" });
},
enterHandler(el, done) {
//$(el).fadeIn(400, done);
$(el).fadeTo(400, 1, done);
},
leaveHandler(el, done) {
$(el).fadeOut(400, done);
},
},
components: {
'v-a': {
template: '<div>Component A</div>'
},
'v-b': {
template: '<div>Component B</div>'
}
}
}
})
</script>
</body>
</html>
```
![](https://i.imgur.com/UAl0j7L.png)
![](https://i.imgur.com/dAaP5FP.png)
#### List Transitions
- List 被 for loop 出來,所以是多個DOM,無法用 **<transition>**,要用 **<transition-group>**
- <transition-group> 本身是一個 tag或會產生一個 tag **<span>**
```htmlmixed=
<transition-group name="list" tag="div">
<span v-for="item in items" v-bind:key="item" class="list-item">
{{ item }}
</span>
</transition-group>
```
#### List Move Transitions
針對 List 排序/順序做調整
```htmlmixed=
<!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>Day 13</title>
<style>
.fade-enter {
opacity: 0
}
.fade-enter-active {
transition: opacity 1s;
}
.fade-move {
transition: transform 1s;
}
.fade-enter-to {
opacity: 1
}
.fade-leave {
opacity: 1
}
.fade-leave-active {
transition: opacity 1s;
}
.fade-leave-to {
opacity: 0
}
</style>
</head>
<body>
<div id="app">
<button @click="clickHandler">Toggle</button>
<transition-group name="fade" tag="div">
<p v-for="num in items" :key="num">
{{ num }}
</p>
</transition-group>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: '#app',
data() {
return {
items: [1,2,3,4,5,6,7,8,9]
}
},
methods: {
// 打亂排序
shuffle: function () {
// _.shuffle(this.items) 這樣寫不會動到原始得資料,所以畫面不變
this.items = _.shuffle(this.items)
},
clickHandler() {
},
},
})
</script>
</body>
</html>
```
> 加上 xxxname-move 的 class 來達成 **移動的特效**
:::danger
可以把上面範例的 p tag 改成 span tag 動畫就會無效,所以要把 span 加上 css inline-block
One important note is that these FLIP transitions do not work with elements set to **display: inline**. As an alternative, you can use **display: inline-block** or place elements in a flex context.
:::
[shuffle](https://lodash.com/docs/2.4.2#shuffle)
#### Staggering List Transitions
1:42:40
#### Reusable Transitions
1:45:11
雖然透過 class 操作動畫已經達到 Reusable 的效果,但是娑要更 reusable 可以包一個客製化的component (slot)
```htmlmixed=
<!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>Day 13</title>
<style>
.fade-enter {
opacity: 0
}
.fade-enter-active {
transition: opacity 1s;
}
.fade-move {
transition: transform 1s;
}
.fade-enter-to {
opacity: 1
}
.fade-leave {
opacity: 1
}
.fade-leave-active {
transition: opacity 1s;
}
.fade-leave-to {
opacity: 0
}
</style>
</head>
<body>
<div id="app">
<my-special-transition>
<div>
我是 Child 子元件
</div>
</my-special-transition>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
// 全域載入 Component,使用時就不用每隻檔案要 import
Vue.component('my-special-transition', {
template: '\
<transition\
name="very-special-transition"\
mode="out-in"\
v-on:before-enter="beforeEnter"\
v-on:after-enter="afterEnter"\
>\
<slot></slot>\
</transition>\
',
methods: {
beforeEnter: function (el) {
// ...
},
afterEnter: function (el) {
// ...
}
}
})
new Vue({
el: '#app',
data() {
},
methods: {
},
})
</script>
</body>
</html>
```
> 好處:不用再指定 mode / name /appear給動態模組
> And functional components are especially well-suited to this task
>
#### Dynamic Transitions
1:49:40
*
* 常用在輪播範例
* name可以動態綁定
輪播會同時需要跑2種動畫
![](https://i.imgur.com/VgB6nBd.png)
```htmlmixed=
<transition v-bind:name="transitionName">
<!-- ... -->
</transition>
> 範例請看 Doc
```
### State Transitions (常用)
1:52:40
#### Animating State with
做狀態的動畫效果,可以用到此法的案例:
* numbers and calculations
* colors displayed
* the positions of SVG nodes
* the sizes and other properties of elements
核心概念,設定兩個數字,一個是原始數字,一個是跑動畫的數字,我們修改原始數字,但是顯示跟跑動是動畫數字
第一個範例用 GSAP 做 數字遞增動畫特效,But 套件很肥 (1:55:20)
第二個範例用 GSAP 做 輸入色票產生色塊,But 套件很肥 (1:57:04)
#### Dynamic State Transitions
SVG 操作 => 自己看
#### Organizing Transitions into Components
官方提供很好的模組作法 2:00:13
```htmlmixed=
<!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>Day 13</title>
<body>
<div id="app">
<animated-integer :value="value"></animated-integer>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<!-- 加入 tween.js -->
<script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script>
<script>
// 會動的數字
Vue.component('animated-integer', {
template: '<span>{{ tweeningValue }}</span>',
props: {
// 原始資料
value: {
type: Number,
required: true
}
},
data: function () {
return {
// 跑動資料
tweeningValue: 0
}
},
// 當 value 便的時候幫我跑資料
watch: {
value: function (newValue, oldValue) {
this.tween(oldValue, newValue)
}
},
mounted: function () {
this.tween(0, this.value)
},
methods: {
tween: function (startValue, endValue) {
var vm = this
function animate () {
if (TWEEN.update()) {
// requestAnimationFrame的目的是讓 new TWEEN.Tween 跑
requestAnimationFrame(animate)
}
}
new TWEEN.Tween({ tweeningValue: startValue })
.to({ tweeningValue: endValue }, 500)
.onUpdate(function () {
vm.tweeningValue = this.tweeningValue.toFixed(0)
})
.start()
animate()
}
}
})
new Vue({
el: '#app',
data() {
return {
value: 0,
}
},
methods: {
},
})
</script>
</body>
</html>
```
> 讓你不用寫計時器,動畫交給 tween js
#### Bringing Designs to Life
用 Vue 控制 svg 與 DOM 做動畫,但如果要效能好用canvas