# 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' })
]))
]),
...
]),
...
]
})
```

在上例中,當變數 `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' }))
])
])
])
```

如上例,在元件離場前,底色會漸變成綠色,而這個元件中的子元件 `<p>` 裡面的文字大小,也會漸漸變為 50px。
:::info
在這個例子裡,如果沒有使用 group 將兩個轉場定義包起來的話,會依序執行(先在 1 秒內將底色變為綠色,在用 1 秒將子元件 `<p>` 內文字變為 50px)

:::
### stagger
stagger 可以在每個要執行動畫的元件間插入延遲。
```javascript=
transition(':enter', [
query('li', [
style({opacity: 0}),
stagger(1000, [
animate('500ms', style({ opacity: 1 }))
])
])
])
```
上例中,先使用 query 選取了元件內的子元件`<li>`,並讓 `<li>` 元件在整個元件進入這個頁面顯示時,以間格 1 秒的方式由上而下,一個接著一個地執行 animate 定義的轉場動畫。
(當 stagger 的時間參數為負時,會逆著排版順序出現。)

### group & sequence
當 transition 的第二個參數陣列中,有一個以上的style 或 animate 函式時,預設會以一個接著一個的方式執行轉場動畫。例:
```javascript=
transition('open => closed', [
animate( '1s', style({ backgroundColor: 'red' }) ),
style({ backgroundColor: 'blue' }),
animate( '2s', style({ backgroundColor: 'black' }))
])
```

上例動畫元件底色會在 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'
}))
])
])
```

當這個元件要顯示在這個頁面上時,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
元件要繪製並合成到螢幕上,大致可分為以下過程:

:::
如果可以將動畫的影響限制在合成層(composite layer),可以優化動畫的效能。
常見提升動畫效能的方法略舉下面幾種:
### 將操作限制在 transform / opacity
瀏覽器在進行下面幾種動畫屬性時的效能消耗較低:

據 [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";
}
```

在滑鼠指標移到父元件上時,瀏覽器即會為動畫元件升階,並在滑鼠移開時降階,我們在 chrome dev tool 的 layers 頁面確認。
:::warning
實測時候發現,就算沒有手動加上 `will-change`,chrome 還是會在執行有 transform 和 opacity 的動畫時,自動為元件產生獨自的圖層(不管是使用 Angular 動畫或是一般的 css 動畫都有這個效果)。

而其他樣式就算設定了 `will-change` 也不見得會被升階,例如 `will-change: background`,所以用上述方法提前加上 `will-change` 比較像是為瀏覽器爭取緩衝的時間。
:::