# Animating Vue ###### tags: `Vue` `animation` `transition` ## Why Animate 你可能知道動畫效果可以提升網站的UX,但網站中哪些地方該使用?又如何在Vue app裡使用? ### Directing Focus 我們每天收發eamils、用line、滑FB or IG,接收數以萬計的資訊,當使用者來到你的網站時,很可能腦容量已經接近負荷,作為一個使用者介面設計者,此時最重要的就是迅速抓住使用者的注意力,並且導引他們以最有效率的方式使用我們的產品。 ### Inspiring Action 當使用者來到你的網站時,你希望他們做的第一件事是什麼?可能是看一行slogan、或是點某個按鈕,透過有效地使用animation,可以移除容易讓使用者分心的元素,進而促使他們做出你希望的操作 如同以下的例子,藉由button的動畫效果,我們可以快速抓到使用者的注意,並且讓他們在不需動腦思考我該如何使用這個網站的情況下立刻點選button,開始進一步與我們的產品互動。 ![](https://i.imgur.com/F2LQeDb.gif) <br> 人類原始本能讓我們不得不去注意移動的物體,因此動畫效果對於吸引人類注意力才會那麼有用。在地球上數百萬年的演化過程中,視覺靈敏度一直是人類大腦的核心功能之一,不管是狩獵今天的晚餐,或是防止自己變成其他生物的晚餐,都很仰賴大腦的這項功能。 作為一名developer,我們可以利用人類的視覺靈敏度去引導使用者將注意力放在我們希望他關注的元件上,並且與之產生互動。但要注意的是必須產生自然的動畫效果。 <br> 點選menu button,menu side bar會從左邊滑出,menut button甚至會trasform為一個closing button,再一次點選closing button後會回復成menu button,side bar向左滑入收合,一切都很流暢,沒有突然出現或消失的元件。 就像太陽東升西落是循序漸進的,不會瞬間跳出又瞬間消失,這就是自然的動畫效果。 ![](https://i.imgur.com/n9vxwN6.gif) <br> ### Creating Flow 如果今天我說接下來要說使用動畫的原因,下一句接:你有聽過安麗嗎?這就是不連貫性。在一個不注重UX的網站,很常出現不連貫性的情形,比如你點選一個按鈕或連結,你剛剛正在看的網頁內容全部消失且瞬間跳出一堆新東西,這會讓使用者每次都要重新思考:剛剛發生什麼事、我接下來要幹嘛、網頁現在是有依照我的操作改變什麼了嗎...等等的問題,容易造成使用者"認知疲乏"。 ![bad-flow](https://i.imgur.com/rGhsgcD.gif) <br> 藉由動畫,我們可以讓元件變形、點選分頁產生過渡效果,創造一個具有連貫性的flow,讓使用者感受到一切是那麼的自然。 ![good-flow](https://i.imgur.com/418snB3.gif) --- ## Vue Transitions Trasition,如同字面上的意思,可以讓元件從____到____。 e.g. From off the screen to on, from here to there, from open to closed... 在Vue中,使用<transition> tag把你要呈現transition的元件包住,這個tag可以讓我們在css中設定transition的lifecycle style,也就是enter和leave時的sttyle。 每次寫trasition前,要先想好,這個元件的default style是什麼?也就是說,當這個元件沒有transitioning時的樣子。 ### Default Style 假設今天你想讓一個box有transition的效果,box 會先fade in 然後 fade out,他的default style是什麼?在這個例子,default style會是opacity: 1,也就是transition enter to & away from 的階段。 HTML的element的default style就是opacity: 1,因此,在這個例子中,我們不需要設定default style,事實上,大部份時候都不需要設定default style,畢竟default style就是element原本的樣子。 當你確認了default style是什麼以及在哪個階段,就可以來寫transition了,也就是entering and leaving。 ![](https://i.imgur.com/8hkQGGe.png) ### Entering Transition 問題:What should the starting style be? goal: ![](https://i.imgur.com/YXzGIEj.gif) 在這個例子中,starting style是opacity: 0,可以用`v-enter`來設定starting css style ```css .v-enter { /* starting style */ opacity: 0; } ``` 此時,當element進入DOM時,會是opacity: 0,然後點選open後,會瞬間變成opacity: 1,但我們不希望他瞬間opacity: 1,所以問題又來了: What should the active style be? 在這例子中,要思考的是,這個tranition要持續多久?需要在tranition開始或結束的階段改變它的速度嗎? 用`v-enter-active`可以設定當transition active時的style,更精確一點來說,就是duration & [easing function](https://www.w3schools.com/cssref/css3_pr_transition-timing-function.asp)([another ref](https://easings.net/)) ```css .v-enter { /* starting style */ opacity: 0; } .v-enter-active { /* active entering style */ transition: opacity 2s ease-in; } ``` ### Leaving Transition 問題:What should the ending style be? 現在我們的box已經在畫面上了,接著要去設定它的ending style,在這個例子中很明顯,ending style 為opacity: 0。 ```css .v-enter { /* starting style */ opacity: 0; } .v-enter-active { /* active entering style */ transition: opacity 2s ease-in; } .v-leave-to { /* ending style */ opacity: 0; } ``` 最後的問題是:What should the active leaving style be? 跟enter active很類似,可以用`.v-leave-active` ```css .v-enter { /* starting style */ opacity: 0; } .v-enter-active { /* active entering style */ transition: opacity 2s ease-in; } .v-leave-active { /* active leaving style */ transition: opacity 2s ease-out; } .v-leave-to { /* ending style */ opacity: 0; } ``` **Important: `v-enter-to` & `v-leave` 的style都是opacity: 1是瀏覽器預設的,所以不必設定,transition也能如我們想的運作。** --- ### Named Transitions 實務中,一個view可能會有多個transitions,因此使用Named Transitions去綁定不同的transition,比較易懂且容易維護。 ```htmlmixed <template> <div> <button @click="toggleModal">Open</button> <transition name="fade"> // <-- named transition <div v-if="isOpen" class="modal"> <button @click="toggleModal">Close</button> </div> </transition> </div> </template> ``` 此處name可以為任何命名,建議用transition的效果去命名,這裡我們用的是fade in and fade out,因此把這個transition命名為fade。 當我們有了named transition,在css中,就可以把v的prefix全部換成'fade'。 ```css .fade-enter, .fade-leave-to { opacity: 0; } .fade-enter-active, .fade-leave-active { transition: opacity, 0.5s ease-out; } ``` --- ## Page Transitions ### Vue Router + Transitions router切換頁面時,如何有transition? goal: ![](https://i.imgur.com/3vc5Bdo.gif) 在App.vue,使用transition tag,並命名為slide-fade: ```htmlmixed <template> <div id="app"> <div id="nav"> <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> </div> <transition name="slide-fade"> <router-view /> </transition> </div> </template> ``` 在App.vue的style中寫入slide-fade的css,這裡enter我們用transform: translateX(10px)來做到橫向移入的效果,leave to 則用-10px移出,這樣一來,slide-fade就是global css style。 ```css .slide-fade-enter { transform: translateX(10px); opacity: 0; } .slide-fade-enter-active, .slide-fade-leave-active { transition: all, 0.2s ease; } .slide-fade-leave-to { transform: translateX(-10px); opacity: 0; } ``` ### [Transition Modes](https://vuejs.org/v2/guide/transitions.html#Transition-Modes) 此時切換router的畫面應該會像這樣 ![](https://i.imgur.com/ziTYBsk.gif) 發生什麼事? open button離開的同時,about page也出現,造成短暫的瞬間畫面上有兩個頁面的content存在。 Vue透過在transition上設定mode這個attribute可以解決這個問題,有兩種mode: * in-out: 新的element先transition in,目前的element再transition out * out-in: 目前的element先transition out,新的element再transition in 設定mode="out-in" ```htmlmixed <template> <div id="app"> <div id="nav"> <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> </div> <transition name="slide-fade" mode="out-in"> <router-view /> </transition> </div> </template> ``` 到這裡便完成了page transition with router --- ## Group Transitions 使用Vue內建的`<transition-group>`component,可以將複數個list items or components同時transition。 ![](https://i.imgur.com/AUKd0st.png) ```htmlmixed <template> <div> <input v-model="newContact" placeholder="Name" type="text" /> <button @click="addContact">Add Contact</button> <ul> <li v-for="(contact, index) in contacts" :key="index"> {{ contact }} </li> <ul> </div> </template> ``` ```javascript <script> export default { data() { return { newContact: "", contacts: [ "Beau Thabeast", "Cindy Rella", "Alice Wunderlind" ] } }, methods: { addContact() { this.contacts.push(this.newContact) this.newContact = "" } } } </script> ``` 如何讓list 新增item時,產生由下往上slide in 的transition? 首先,`<transition-group>`包住`<li>`,`<transition-group>`會產生一個預設為`<span>`的tag並包住`<li>`,可以透過tag=""來改為我們想要的tag,這裡設為ul ```htmlmixed <transition-group tag="ul"> <li v-for="contact in contacts" :key="contact"> {{ contact }} </li> </transition-group> ``` 現在`<transition-group>`還不會有任何作用,如同`<transition>`,需要命名以及css: ```htmlmixed <transition-group name="slide-up" tag="ul"> <li v-for="contact in contacts" :key="contact"> {{ contact }} </li> </transition-group> ``` App.vue中 ```css .slide-up-enter { transform: translateY(10px); /* start 10px down*/ opacity: 0; } .slide-up-enter-active { transition: all 0.2s ease; } ``` result: ![](https://i.imgur.com/FP8HS9l.gif) <br> ### Triggering Transitions on Initial Render 如何在view loading進來時讓list items有transition? 只要在`<transition-group>`加上appear這個attribute,就可以讓`<transition-group>`在render list items時也使用slide-up這個trasition。 **appear亦能在`<transition>`使用** ```htmlmixed <transition-group name="slide-up" tag="ul" appear> <li v-for="contact in contacts" :key="contact"> {{ contact }} </li> </transition-group> ``` result: ![](https://i.imgur.com/VHAkjwQ.gif) <br> ### Moving Items within a Group 如何在按下sort button 讓list 依照字母順序重新排序時使用transition? ![](https://i.imgur.com/83YqouX.png) 用`v-move` 這個transition class可以設定`<transition-group>`中的items重新排序時的移動效果。 prefix v 一樣可以用transition name取代 App.vue ```css .slide-up-move { transition: transform 0.5s ease-out; } ``` result: ![](https://i.imgur.com/QfaY326.gif) --- ## JavaScript Hooks + [Velocity](http://velocityjs.org/) 當你的web application變得越來越複雜時,就可以需要更客製化或是更多樣式的animation,這時就必須用JavaScript來創造animation。 Vue內建的JavaScript Hooks和一個animation library完成這項工作。 ### JavaScript Hooks JavaScript Hooks很類似Vue’s [lifecycle hooks](https://vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks),但特別指Vue的`transition`和`transition-group` components,可以看作是Vue trasition的lifetime。 * `before-enter` * `enter` * `after-enter` * `before-leave` * `enter-cancelled` * `leave` * `after-leave` * `leave-cancelled` 可以看出他們都是在enter and leave階段被呼叫,當他們被呼叫時,可以綁定要觸發的methods,並用JavaScript寫出僅用CSS無法做到的複雜效果。 ```htmlmixed <transition @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @enter-cancelled="enterCancelled" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" @leave-cancelled="leaveCancelled" > <!-- ... --> </transition> ``` ### A Drawer Transition drawer-type component是很常見的component,按下一個鈕,從側邊滑出side bar or user profile。 starting code: Drawer.vue ```htmlmixed <template> <div> <button @click="isOpen = !isOpen"> My Profile </button> <div v-if="isOpen" class="drawer"> <img src="../assets/avatar.png" alt="avatar" /> <div></div> <div></div> <div></div> <div></div> </div> </div> </template> ``` ```javascript <script> export default { data() { return { isOpen: false } } } </script> ``` ```htmlmixed <style scoped> img { height: 2.5em; width: 2.5em; border-radius: 50%; } .drawer { display: flex; flex-direction: column; align-items: center; width: 12em; height: 20em; border-radius: 1%; background-color: #e0e0e0; box-shadow: 0.08em 0.03em 0.4em #ababab; padding-top: 0.7em; } .drawer div { height: 3.5em; width: 95%; margin-top: 0.6em; background-color: #f0f0f0; border: 0.02em solid #ababab; border-radius: 1%; } </style> ``` step 1: wrap the drawer in the transition component, and add the hooks we’ll be using ```htmlmixed <template> <div> <button @click="isOpen = !isOpen"> My Profile </button> <transition @before-enter="beforeEnter" @enter="enter" @leave="leave" :css="false" > <div v-if="isOpen" class="drawer"> <img src="../assets/avatar.png" alt="avatar" /> <div></div> <div></div> <div></div> <div></div> </div> </transition> </div> </template> ``` 建議methods的命名與lifetime的命名一致。`before-enter()`, `enter()`和`leave()`各負責的工作如下: `before-enter()`: 設定一個drawer進入畫面之前的style `enter()`: drawer進入畫面後最終完成時的style `leave()`: drawer離開畫面時的transition `:css="false"`是用來告知`transition`這裡不需要設定transition classes <br> ```javascript <script> import Velocity from 'velocity-animate' export default { data() { return { isOpen: false } }, methods: { beforeEnter(el) { // we'll place our starting style here }, enter(el, done) { // style to transition to on enter }, leave(el, done) { // style to transition away to on leave } } } </script> ``` `el`指的就是要被transitioned的element,在這個例子中是drawer的div #### Starting Style 希望drawer可以展開div以及一開始是看不見的 ```javascript beforeEnter(el) { el.style.opacity = 0 el.style.width = '0em' }, ``` #### Enter Style drawer進入畫面後展開div,並出現在畫面上, 接著定義如何產生transition。 這裡使用[Velocity](http://velocityjs.org/),先定義css style,再設定transition arguments參考[這裡](http://velocityjs.org/#arguments) easing的options可以從[這裡](https://easings.net/)找,但要注意的是,其中Back, Elastic and Bounce不適用在Velocity complete表示transition結束後的動作,這裡傳入done,告知Vue這個hook已經完成,可以進行下一個lifetime hook ```javascript enter(el, done) { Velocity( el, // element to animate { opacity: 1, width: '12em' }, // new style rules { duration: 1000, easing: 'easeOutCubic', complete: done } // define how transition happens and complete it ) } ``` #### Leave Style ```javascript= leave(el, done) { Velocity( el, { opacity: 0, width: '0em' }, { duration: 500, easing: 'easeInCubic', complete: done } ) } ``` result: ![](https://i.imgur.com/pjCWSFo.gif) <br> ### The Power of Velocity 上面的例子好像沒有難到css做不了,因為範例本身的設定就不複雜。 Velocity的spring physics可以做到更複雜的transition,[這裡](http://codepen.io/julianshapiro/pen/hyeDg)有playground。 spring physics demo: https://codepen.io/julianshapiro/full/fgjaF 將spring physics 用在剛剛的例子,只需把easing 的value換成 array ```javascript enter(el, done) { Velocity( el, { opacity: 1, width: '12em' }, { duration: 1000, easing: [60, 5], complete: done } // now with spring physics ) } ``` `easing: [60, 10]`, 第一個數字是tension,第二個數字是friction,friction越低,transition結束時的震動就越快 result: ![](https://i.imgur.com/hNJjY7y.gif) <br> ### Another Example: Cards 用`transition-group`,rotateZ可以旋轉角度,rotateX、rotateY可以壓縮X軸、Y軸。 ```htmlmixed <template> <transition-group appear @before-enter="beforeEnter" @enter="enter" :css="false" > <div class="card" v-for="card in cards" :key="card.id"> <p>{{ card.title }}</p> </div> </transition-group> </template> <script> import Velocity from 'velocity-animate' export default { data() { return { cards: [ { title: 'Could contain anything', id: 123 }, { title: 'Endless possibilities', id: 456 } ] } }, methods: { beforeEnter(el) { el.style.opacity = 0 el.style.width = '0em' }, enter(el, done) { Velocity( el, { opacity: 1, width: '12em', rotateZ: '3deg' }, { duration: 1000, easing: [70, 8], complete: done } ) } } } </script> <style scoped> .card { height: 4em; width: 12em; border-radius: 1%; background-color: #e0e0e0; box-shadow: 0.08em 0.03em 0.4em #ababab; padding-top: 1em; } </style> ``` result: ![](https://i.imgur.com/SzFkStW.gif)