IMPORTANT: @angular/animations 套件現已棄用。Angular 團隊建議所有新程式碼都使用原生 CSS 搭配 animate.enter 與 animate.leave 來製作動畫。詳情請見新的進入與離開動畫指南。另請參閱從 Angular Animations 套件遷移,了解如何開始把應用程式遷移到純 CSS 動畫。
到目前為止,我們已學會單一 HTML 元素的簡單動畫。 Angular 也能讓你為協調好的序加入動畫,例如整個格線或元素清單在進入與離開頁面時的動畫。 你可以選擇讓多個動畫平行執行,或讓各自獨立的動畫依序一個接一個執行。
控制複雜動畫序列的函式如下:
| 函式 | 說明 |
|---|---|
query() |
尋找一個或多個內部 HTML 元素。 |
stagger() |
為多個元素的動畫套用瀑布式延遲。 |
group() |
平行執行多個動畫步驟。 |
sequence() |
一個接一個地執行動畫步驟。 |
query() 函式
大多數複雜動畫都仰賴 query() 函式來尋找子元素並為它們套用動畫。基本範例包括:
| 範例 | 說明 |
|---|---|
query() 之後接 animate() |
用來查詢簡單的 HTML 元素並直接為它們套用動畫。 |
query() 之後接 animateChild() |
用來查詢子元素,這些子元素本身帶有套用在其上的動畫中繼資料,並觸發這類動畫(否則會被目前/父元素的動畫擋住)。 |
query() 的第一個參數是一個 CSS 選擇器字串,其中也可以包含以下 Angular 專用 token:
| Token | 說明 |
|---|---|
:enter :leave |
用於正在進入/離開的元素。 |
:animating |
用於目前正在播放動畫的元素。 |
@* @triggerName |
用於帶有任何(或特定)觸發器的元素。 |
:self |
正在播放動畫的元素本身。 |
進入與離開的元素
並非所有子元素都會真的被視為正在進入/離開;這有時可能違反直覺、令人困惑。詳情請見 query API 文件。
你也可以在動畫範例(動畫簡介章節中有介紹)的「Querying」分頁中看到這一點的圖解。
使用 query() 與 stagger() 函式為多個元素加入動畫
透過 query() 查詢子元素之後,stagger() 函式能讓你定義每個項目之間的時間間隔,讓元素以彼此延遲的方式播放動畫。
以下範例示範如何使用 query() 與 stagger() 函式,為一個清單(英雄清單)加入動畫:每個英雄依序加入,彼此之間略有延遲,從上到下排列。
使用
query()尋找符合特定條件、正在進入頁面的元素對這些元素中的每一個,使用
style()設定相同的初始樣式。 讓它變透明,並使用transform把它移出原位,讓它能滑入定位。使用
stagger()讓每個動畫延遲 30 毫秒使用自訂的緩動曲線,讓每個元素在畫面上播放 0.5 秒,同時淡入並還原 transform
hero-list-page.ts
animations: [
trigger('pageAnimations', [
transition(':enter', [
query('.hero', [
style({opacity: 0, transform: 'translateY(-100px)'}),
stagger(30, [
animate('500ms cubic-bezier(0.35, 0, 0.25, 1)', style({opacity: 1, transform: 'none'})),
]),
]),
]),
]),
使用 group() 函式的平行動畫
你已看過如何在每個連續動畫之間加入延遲。
但你可能也想設定平行發生的動畫。
例如,你可能想為同一個元素的兩個 CSS 屬性加入動畫,但每個屬性使用不同的 easing 函式。
為此,你可以使用動畫的 group() 函式。
HELPFUL: group() 函式是用來把動畫步驟分組,而不是把被動畫的元素分組。
以下範例在 :enter 與 :leave 上都使用了 group(),採用兩種不同的時間設定,因此把兩個獨立的動畫平行套用到同一個元素上。
hero-list-groups.ts (excerpt)
animations: [
trigger('flyInOut', [
state(
'in',
style({
width: '*',
transform: 'translateX(0)',
opacity: 1,
}),
),
transition(':enter', [
style({width: 10, transform: 'translateX(50px)', opacity: 0}),
group([
animate(
'0.3s 0.1s ease',
style({
transform: 'translateX(0)',
width: '*',
}),
),
animate(
'0.3s ease',
style({
opacity: 1,
}),
),
]),
]),
transition(':leave', [
group([
animate(
'0.3s ease',
style({
transform: 'translateX(50px)',
width: 10,
}),
),
animate(
'0.3s 0.2s ease',
style({
opacity: 0,
}),
),
]),
]),
]),
],
依序動畫與平行動畫
複雜動畫可以同時發生很多事。
但如果你想建立一個由多個動畫一個接一個發生的動畫呢?先前你用 group() 讓多個動畫同時平行執行。
另一個叫 sequence() 的函式,能讓你把同樣的動畫一個接一個地執行。
在 sequence() 內部,動畫步驟由 style() 或 animate() 函式呼叫組成。
篩選動畫範例
再看看範例頁面上的另一個動畫。
在「Filter/Stagger」分頁下,在 Search Heroes 文字方塊中輸入一些文字,例如 Magnet 或 tornado。
當你輸入時,篩選會即時運作。 隨著你輸入每個新字母、篩選條件越來越嚴格,元素會離開頁面。 當你刪除篩選方塊中的每個字母時,英雄清單會逐漸重新進入頁面。
HTML 範本包含一個叫 filterAnimation 的觸發器。
hero-list-page.html
<label for="search">Search heroes: </label>
<input
type="text"
id="search"
#criteria
(input)="updateCriteria(criteria.value)"
placeholder="Search heroes"
/>
<ul class="heroes" [@filterAnimation]="heroesTotal">
@for (hero of heroes; track hero) {
<li class="hero">
<div class="inner">
<span class="badge">{{ hero.id }}</span>
<span class="name">{{ hero.name }}</span>
</div>
</li>
}
</ul>
元件裝飾器中的 filterAnimation 包含三個轉場。
hero-list-page.ts
@Component({
animations: [
trigger('filterAnimation', [
transition(':enter, * => 0, * => -1', []),
transition(':increment', [
query(
':enter',
[
style({opacity: 0, width: 0}),
stagger(50, [animate('300ms ease-out', style({opacity: 1, width: '*'}))]),
],
{optional: true},
),
]),
transition(':decrement', [
query(':leave', [stagger(50, [animate('300ms ease-out', style({opacity: 0, width: 0}))])]),
]),
]),
],
})
export class HeroListPage implements OnInit {
heroesTotal = -1;
get heroes() {
return this._heroes;
}
private _heroes: Hero[] = [];
ngOnInit() {
this._heroes = HEROES;
}
updateCriteria(criteria: string) {
criteria = criteria ? criteria.trim() : '';
this._heroes = HEROES.filter((hero) =>
hero.name.toLowerCase().includes(criteria.toLowerCase()),
);
const newTotal = this.heroes.length;
if (this.heroesTotal !== newTotal) {
this.heroesTotal = newTotal;
} else if (!criteria) {
this.heroesTotal = -1;
}
}
}
這個範例中的程式碼執行以下工作:
- 當使用者第一次開啟或瀏覽到此頁面時跳過動畫(篩選動畫會縮小已存在的內容範圍,所以它只對 DOM 中已存在的元素有效)
- 根據搜尋輸入的值篩選英雄
對每一次變化:
把正在離開 DOM 的元素的 opacity 與 width 設為 0,將它隱藏
在 300 毫秒內為正在進入 DOM 的元素加入動畫。 在動畫期間,元素會回復到預設的寬度與不透明度。
若有多個元素正在進入或離開 DOM,從頁面頂部開始錯開每個動畫,每個元素之間延遲 50 毫秒
為重新排序清單的項目加入動畫
雖然 Angular 開箱即可正確為 *ngFor 清單項目加入動畫,但若它們的順序改變,Angular 就做不到了。
這是因為它會無法追蹤每個元素是哪一個,導致動畫損壞。
幫助 Angular 追蹤這類元素的唯一方法,是為 NgForOf 指令指定一個 TrackByFunction。
這能確保 Angular 永遠知道每個元素是哪一個,進而永遠能把正確的動畫套用到正確的元素上。
IMPORTANT: 若你需要為 *ngFor 清單的項目加入動畫,且這些項目的順序有可能在執行期間改變,請務必使用 TrackByFunction。
動畫與元件檢視封裝
Angular 動畫是以元件的 DOM 結構為基礎,並不直接考慮檢視封裝,這表示使用 ViewEncapsulation.Emulated 的元件,其行為與使用 ViewEncapsulation.None 完全相同(ViewEncapsulation.ShadowDom 與 ViewEncapsulation.ExperimentalIsolatedShadowDom 的行為則不同,稍後會簡短討論)。
例如,若把 query() 函式(在本動畫指南後續內容中會看到更多)套用在一棵使用模擬檢視封裝的元件樹頂端,這樣的查詢就能辨識(進而為其加入動畫)樹中任意深度的 DOM 元素。
另一方面,ViewEncapsulation.ShadowDom 與 ViewEncapsulation.ExperimentalIsolatedShadowDom 會把 DOM 元素「藏」在 ShadowRoot 元素內,改變元件的 DOM 結構。這類 DOM 操作確實會妨礙部分動畫實作正常運作,因為它仰賴簡單的 DOM 結構,且不考慮 ShadowRoot 元素。因此,建議避免為包含使用 ShadowDom 檢視封裝之元件的檢視套用動畫。
動畫序列小結
為多個元素加入動畫的 Angular 函式,從 query() 開始尋找內部元素;例如,收集一個 <div> 內的所有圖片。
其餘函式 stagger()、group() 與 sequence(),會套用瀑布效果,或讓你控制多個動畫步驟如何套用。
深入了解 Angular 動畫
你可能也會對以下內容有興趣: