# [ 想入門,我陪你 ] 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