# Angular Animation 筆記 Angular 提供的動畫功能,其實就是基於 CSS 動畫功能的拓展。之前做轉場動畫時可能需要在javascript 和 css 之間來回設定調整,而現在可以全部在 angular 元件中處理。 ## 環境設定 要使用 Angular 的動畫功能,須先在 AppModule 中導入 BrowserAnimationsModule ```javascript= import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule ], declarations: [ ], bootstrap: [ ] }) export class AppModule { } ``` 再從 angular animation library 導入動畫功能函式 ```javascript= import { trigger, state, style, animate, transition, ... } from '@angular/animations'; ``` ## 基本的動畫定義 Angular 動畫設定定義了,被觸發器(trigger)綁定的元件在指定的狀態下的靜態樣式,和當狀態異動時元件應執行的轉場動畫。 動畫設定添加在元件的 `@Component` 裝飾器中,名為 `animations:` 的 metadata 屬性內,屬性值為由 trigger 所組成的陣列。 下面以一個名稱叫 openClose 的 trigger 為例,在模板中 trigger 會以屬性型態綁定在欲作動畫的元件上,狀態值即為 'open' 或是 'close': ```htmlmixed= <div [@openClose]="isOpen ? 'open' : 'close'">...</div> ``` 在元件中的動畫設定: ```javascript= @Component({ ... animations: [ trigger('openClose', [ // 定義狀態為 'open' 時的樣式 state('open', style({ backgroundColor: 'yellow' })), // 定義狀態為 'close' 時的樣式 state('close', style({ backgroundColor: 'green' })), // 用 transition 定義狀態轉換時,元件該怎麼動作 transition('open => close', [ animate('3s', style({ opacity: 0.3 })), animate('2s') ]), transition('* => open', [ animate('2s', keyframes([ style({ backgroundColor: 'yellow' }), style({ backgroundColor: 'red' }), style({ backgroundColor: 'green' }) ])) ]), ... ]), ... ] }) ``` ![](https://media.giphy.com/media/t6lsoDbK5NL5XGMBNp/giphy.gif =250x) 在上例中,當變數 `isOpen` 由 `true` 變為 `false` 時,openClose 的狀態會由 'open' 變為 'close'。因此觸發了第一組 transition 轉場動畫,這個轉場動畫歷時 5 秒;在前 3 秒內,黃色的 div 會漸變透明度為 0.3,再以 2 秒的時間漸漸變為綠色。(transition 陣列中的 style 和 animate 預設會依序呈現) 而當 openClose 由任意值變為 'open' 時,底色會漸變為紅色,再轉為綠色,這個轉場動畫持續時間為 2 秒。 ### trigger 動畫組的觸發器。當 trigger 的狀態值發生改變時,即觸發符合條件的動畫組。trigger 的第一個參數即 trigger 名稱,亦為綁定在動畫元件上的屬性;第二個參數為 state 和 transition 組成的陣列。state 用以描述某個狀態時的靜態樣式,transition 則描述狀態轉換時的樣式轉變。 ### state 用來定義動畫在某個狀態點的靜態樣式。state 的第一個參數,為 trigger 的狀態值;第二個參數,為用來定義這個 state 樣式的 style 函數。例: :::info 當動畫被禁用時,transition 中的樣式會被忽略,但 state 中的不會。 ::: ```javascript= state('open', style({ backgroundColor: 'yellow' })) ``` 上例為,當 trigger 為 open 時,綁定的元件背景色為黃色。 ### style 用來設定樣式,參數為以 css 組成的 Style Object ([Style Object 的說明](https://www.w3schools.com/jsref/dom_obj_style.asp)) :::info style 中的屬性值可以使用萬用字元 `*`,來表示自動樣式,例: ```javascript= style({ height: 0 }), animate('1s', style({ height: '*' })) ``` 表示此動畫會將此元件由 0 撐高至父元件容許的高度。 ::: ### transition 具有觸發條件的轉場動畫組合。第一個參數為描述觸發條件的字串,代表 trigger 狀態值變動的方向;第二個參數為由 style 或 animate 組成的**陣列**,定義了這個轉場過程樣式的變化。例: ```javascript= state('open', style({ backgroundColor: 'yellow' })) transition('open => close', [ style({ opacity: 1 }), // 定義轉場一開始的樣式 animate('2s', style({ opacity: 0 })) // 定義轉場的時間,跟轉場最後的樣式 ]) ``` 代表在 trigger 狀態由 open 變為 close 時,該元件的透明度會在兩秒的時間由 1 變為 0。 :::info transition 僅能定義元件轉場的過程,離開了轉場後的樣式還是需由 state 或元件自身的樣式決定,例如: ```javascript= state('close', style({ opacity: 1 })), transition('open => close', [ animate('2s', style({ opacity: 0})) ]) ``` 當狀態由 open 變為 close 時,該元件會以 2 秒漸漸變為透明,但在轉場結束後又立即出現。 ::: transition 的第一個參數還可使用: - `'open <=> close'`:代表由 open => close 和 close => open 都會觸發 - `'* => close'`:由任意值變成 close 會觸發 - `'* => void'`:離開這個頁面時會觸發(別名 `':enter'`) - `'void => *'`:進入這個頁面時會觸發(別名 `':leave'`) - `':increment'`:當 trigger 為數字,且其值增加時觸發 - `':decrement'`:當 trigger 為數字,且其值減少時觸發 例: ```htmlmixed= <div @myTrigger *ngIf="isShown"> ... </div> ``` ```javascript= // @Component Metadata trigger('myTrigger', [ transition(':leave', [ animate('5s', style({ opacity: 0 })) ]) ]) ``` 在 `isShown` 變為 `false` 時,觸發 `myTrigger`,讓 div 以 5 秒的時間逐漸透明,然後才被移除。 ### animate 可以設定在時間內緩變的動態轉場函式。接受的參數第一個為 timing,第二個參數定義了這個時間結束時元件的樣式。 :::info 當 animate 第二個參數省略時,會以兩個 state 的靜態樣式做漸變。例: ```javascript= state('open', style({ backgroundColor: 'yellow' })), state('closed', style({ backgroundColor: 'green' })), transition('open => closed', [ animate('1s') ]) ``` 當此轉場動畫被觸發時,會讓元件底色會由黃變綠。 ::: timing 參數格式為 'duration delay easing' - duration 代表這個動畫持續的時間 - delay (非必填)動畫開始前的延遲時間 - easing (非必填) 動畫進行時的運動曲線,常用值為:ease-in、ease-out、ease-in-out 當僅有 duration 且不帶單位時,不需用引號(預設單位為 ms)。 例: ```javascript= animate(100); // 此動畫運行持續 100ms animate('100ms'); // 結果同上 animate('10s 200ms'); // 等待 200ms 後運行 10s animate('100'); // 會報錯 ``` 第二個參數可以添加 style 或 keyframes 函式來定義轉場過程中變化的樣式或關鍵影格。例: ```javascript= transition('* => void', [ animate('1s', style({ transform: 'translateX(100px)', opacity: 0 })) ]) ``` 代表該元件在要離場時,會先在 100 毫秒的時間內,向右移動 100px 並透明度變為 0,再移除這個元件。 ### keyframes 如同 css 的 @keyframes,angular animation 提供的 keyframes 函式也是以我們先定義關鍵影格,再由瀏覽器補足平滑變化的方式定義動畫。例: ```javascript= transition('* => active', [ animate('2s', keyframes([ style({ backgroundColor: 'blue', offset: 0}), style({ backgroundColor: 'red', offset: 0.8}), style({ backgroundColor: 'orange', offset: 1.0}) ])), ]), ``` 上例中,當 trigger 由任意狀態變為 'active' 時,該元件的背景將在 1.6 秒內,由藍色漸變為紅色,並在接下來的 0.4 秒漸變為橘色。 > style 的第二個參數 offset 代表該樣式的漸變,在整個轉場發生的時間點,以 0 為開始,1 為結束。若省略 offset 則預設會均分。 > ## 複雜序列的動畫函式 以上提到的都是基本的 Angular 動畫操作,我們還可以利用下面的函式組合較複雜的動畫序列: ### query 如果在元件內,還有子元件需要執行轉場動畫時,我們可以使用 query 來選取他。 query 的第一個參數使用 css selector 來選擇子元件,第二個參數則是希望子元件執行的轉場動畫定義。例如: ```javascript transition(':leave', [ group([ animate('1s', style({ backgroundColor: 'green' })), query('p', [ animate('1s', style({ fontSize: '50px' })) ]) ]) ]) ``` ![](https://media.giphy.com/media/1Bd7tch7Rlkq1cqohh/giphy.gif =250x) 如上例,在元件離場前,底色會漸變成綠色,而這個元件中的子元件 `<p>` 裡面的文字大小,也會漸漸變為 50px。 :::info 在這個例子裡,如果沒有使用 group 將兩個轉場定義包起來的話,會依序執行(先在 1 秒內將底色變為綠色,在用 1 秒將子元件 `<p>` 內文字變為 50px) ![](https://media.giphy.com/media/SJCK8JCq9jg37J958d/giphy.gif =250x) ::: ### stagger stagger 可以在每個要執行動畫的元件間插入延遲。 ```javascript= transition(':enter', [ query('li', [ style({opacity: 0}), stagger(1000, [ animate('500ms', style({ opacity: 1 })) ]) ]) ]) ``` 上例中,先使用 query 選取了元件內的子元件`<li>`,並讓 `<li>` 元件在整個元件進入這個頁面顯示時,以間格 1 秒的方式由上而下,一個接著一個地執行 animate 定義的轉場動畫。 (當 stagger 的時間參數為負時,會逆著排版順序出現。) ![](https://media.giphy.com/media/9GI7VTzU8rBQaOhyXy/giphy.gif =250x) ### group & sequence 當 transition 的第二個參數陣列中,有一個以上的style 或 animate 函式時,預設會以一個接著一個的方式執行轉場動畫。例: ```javascript= transition('open => closed', [ animate( '1s', style({ backgroundColor: 'red' }) ), style({ backgroundColor: 'blue' }), animate( '2s', style({ backgroundColor: 'black' })) ]) ``` ![](https://media.giphy.com/media/4NiFBHapIZzt5saLz3/giphy.gif =250x) 上例動畫元件底色會在 1 秒漸變為紅色,在突然變為藍色,在用 2 秒的時間由藍色漸變為黑色,最後出現綠色。(效果等同使用 sequence 函式) 如果希望在同一個元件上的多個轉場動畫**同時發生**,或是想為每個轉場指定不同的時間,則可以使用 group 函式,例: ```javascript= transition(':enter', [ style({ transform: 'translateX(100px)' }), group([ animate('1s', style({ transform: 'translateX(0)' })), animate('3s', style({ opacity: 1, backgroundColor: 'yellow' })) ]) ]) ``` ![](https://media.giphy.com/media/2aQ1d9bA6kchLjzFx5/giphy.gif =300x) 當這個元件要顯示在這個頁面上時,1 秒的由偏右 100px 位置向左移動至定位的動作,跟 3 秒的透明度漸變會同時執行。 ## 可重複使用的預定義動畫設定 Angular 提供了 animation 函式讓我們可以將動畫定義預先宣告在個別 `.ts` 檔案裡,再利用 useAnimation 函式取用已經定義好的動畫設定。 ```javascript= export const transAnimation = animation([ style({ height: '{{ height }}', opacity: '{{ opacity }}' }), animate('{{ time }}') ]); ``` > 在 `{{ }}` 內的 `height`、`opacity` 和 `time` 為在執行時替換的變數。 > 我們可以在希望取用這個動畫定義的地方,使用 useAnimation 函式,並為上面的變數賦值: ```javascript= @Component({ trigger('openClose', [ transition('open => closed', [ useAnimation(transAnimation, { params: { height: 0, opacity: 1, time: '1s' } }) ]) ]) }) ``` ## 補充)動畫效能優化 當我們使用動畫移動或改變元件樣式時,有可能會造成元件或畫面的抖動閃爍,這問題的原因可能是因為我們的操作影響了畫面原有的排版,造成畫面必須不斷地重繪,並且瀏覽器也必須不時地檢查畫面上的元件是否又被改變了。 :::info 元件要繪製並合成到螢幕上,大致可分為以下過程: ![](https://developers.google.com/web/fundamentals/performance/rendering/images/simplify-paint-complexity-and-reduce-paint-areas/frame.jpg?hl=zh-tw) ::: 如果可以將動畫的影響限制在合成層(composite layer),可以優化動畫的效能。 常見提升動畫效能的方法略舉下面幾種: ### 將操作限制在 transform / opacity 瀏覽器在進行下面幾種動畫屬性時的效能消耗較低: ![](https://developers.google.com/web/fundamentals/performance/rendering/images/stick-to-compositor-only-properties-and-manage-layer-count/safe-properties.jpg?hl=zh-tw =400x) 據 [google web fundamentals](https://developers.google.com/web/fundamentals/performance/rendering/stick-to-compositor-only-properties-and-manage-layer-count?hl=zh-tw) 指出,上述的屬性變更,只會影響到合成層。要達到這個效果,google 的網頁開發指南文中還建議需要將元件提升到獨自的圖層上。 ### 使用 `will-change` 為元件的圖層升階 如果我們希望手動為動畫圖層升階的話,可以使用 css 的 `will-change`,設定的元件會被獨立在自己的圖層。 ```css= .ur-class { will-change: < transform | opacity >; } ``` :::info google 強烈建議不要濫用 `will-change` (例如為每個動畫元件設定 `will-change` 或是讓 `will-change` 屬性常駐),因為建立圖層會耗費系統資源,除非動畫效能明顯有問題才考慮使用 `will-change`,不然還是應將圖層管理交給瀏覽器自主。 ::: `will-change` 的功能是在動畫開始之前,預先通知瀏覽器有元件要變動,讓瀏覽器可以先幫這個元件建立獨立的圖層,以防止其他元件可能會一起被重繪。因此比較好的方法是,連結動畫啟動前的事件動態為元件加上 `will-change`。 例如在父元件加上 `mouseenter` 、`mouseleave` 事件為元件新增並刪除 `will-change`: ```javascript= @HostListener('mouseover') addWillChange() { this.box.nativeElement.style.willChange = "opacity"; } @HostListener('mouseleave') removeWillChange() { this.box.nativeElement.style.willChange = "auto"; } ``` ![](https://media.giphy.com/media/fQimbKqZybCZdjIHR5/giphy.gif) 在滑鼠指標移到父元件上時,瀏覽器即會為動畫元件升階,並在滑鼠移開時降階,我們在 chrome dev tool 的 layers 頁面確認。 :::warning 實測時候發現,就算沒有手動加上 `will-change`,chrome 還是會在執行有 transform 和 opacity 的動畫時,自動為元件產生獨自的圖層(不管是使用 Angular 動畫或是一般的 css 動畫都有這個效果)。 ![](https://media.giphy.com/media/1nPadnHKrkj3XUtfNI/giphy.gif) 而其他樣式就算設定了 `will-change` 也不見得會被升階,例如 `will-change: background`,所以用上述方法提前加上 `will-change` 比較像是為瀏覽器爭取緩衝的時間。 :::