---
# System prepended metadata

title: Angular Animation 筆記

---

# 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` 比較像是為瀏覽器爭取緩衝的時間。
:::
