Angular 轉場與觸發(fā)器

2022-07-09 18:04 更新

動畫轉場與觸發(fā)器

你已經(jīng)在簡介頁學習了 Angular 動畫的基礎知識。

本章將深入講解特殊的轉場狀態(tài),如 ?*?(通配符)和 ?void?,并說明這些特殊狀態(tài)如何作用于進入或離開視圖的元素。本章還探討了多重觸發(fā)器、動畫回調(diào),以及使用關鍵幀技術的序列動畫。

預定義狀態(tài)與通配符匹配

在 Angular 中,轉場狀態(tài)可以通過 ?state()? 函數(shù)進行顯式定義,或使用預定義的 ?*?(通配符)狀態(tài)和 ?void ?狀態(tài)。

通配符狀態(tài)

星號 ?*? 或者叫通配符可以匹配任何一個動畫狀態(tài)。它可用來定義那些不用在乎 HTML 元素的起始狀態(tài)或結束狀態(tài)的轉場動畫。

比如,一個 ?open => *? 轉場可應用在當元素的狀態(tài)從 ?open ?變成任何其它狀態(tài)時。


下面是通配符狀態(tài)的另一個代碼范例,以及我們以前使用 ?open ?和 ?closed ?狀態(tài)的實例。但這次,對于每個狀態(tài)到狀態(tài)的轉換對,我們這次規(guī)定從任何狀態(tài)轉場到 ?closed ?狀態(tài)時要花 1 秒鐘,而從任何狀態(tài)轉場到 ?open ?狀態(tài)時要花 0.5 秒。

這讓我們可以添加新狀態(tài),而不必把它手動包含到每個單獨的轉場中。

animations: [
  trigger('openClose', [
    // ...
    state('open', style({
      height: '200px',
      opacity: 1,
      backgroundColor: 'yellow'
    })),
    state('closed', style({
      height: '100px',
      opacity: 0.8,
      backgroundColor: 'blue'
    })),
    transition('* => closed', [
      animate('1s')
    ]),
    transition('* => open', [
      animate('0.5s')
    ]),
  ]),
],

使用雙向箭頭語法可以指定任意方向的狀態(tài)轉場。

transition('open <=> closed', [
  animate('0.5s')
]),

使用帶多個轉場狀態(tài)的通配符狀態(tài)

在這個雙態(tài)按鈕的例子中,通配符不是很有用,因為只有兩種可能的狀態(tài):?open ?和 ?closed?。一般而言,當一個特定狀態(tài)下的元素可能變更為多個潛在狀態(tài)時,通配符狀態(tài)會更好用。如果我們的按鈕可以從 ?open ?變成 ?closed ?或類似 ?inProgress ?的狀態(tài),則可以使用通配符狀態(tài)來減少所需的編碼量。


animations: [
  trigger('openClose', [
    // ...
    state('open', style({
      height: '200px',
      opacity: 1,
      backgroundColor: 'yellow'
    })),
    state('closed', style({
      height: '100px',
      opacity: 0.8,
      backgroundColor: 'blue'
    })),
    transition('open => closed', [
      animate('1s')
    ]),
    transition('closed => open', [
      animate('0.5s')
    ]),
    transition('* => closed', [
      animate('1s')
    ]),
    transition('* => open', [
      animate('0.5s')
    ]),
    transition('open <=> closed', [
      animate('0.5s')
    ]),
    transition ('* => open', [
      animate ('1s',
        style ({ opacity: '*' }),
      ),
    ]),
    transition('* => *', [
      animate('1s')
    ]),

當在任意兩個狀態(tài)之間切換時,?* => *? 轉場都會生效。

轉場會按照其定義的順序進行匹配。因此,你可以在 ?* => *? 轉場的前面定義其它轉場。比如,定義只針對 ?open => closed? 的狀態(tài)變更或動畫,或 ?closed => open?,而使用 ?* => *? 作為匹配不上其它狀態(tài)對時的后備。

要這么做,只要把那些更特殊的轉場放在 ?* => *前面就行了。

使用帶樣式的通配符狀態(tài)

使用帶樣式的 ?*? 通配符來告訴動畫使用當前的狀態(tài)值,并用它進行動畫處理。通配符是一個后備值,如果未在觸發(fā)器中聲明動畫狀態(tài),就會使用這個值。

transition ('* => open', [
  animate ('1s',
    style ({ opacity: '*' }),
  ),
]),

void 狀態(tài)

可以使用 ?void ?狀態(tài)來為進入或離開頁面的元素配置轉場。

組合使用通配符和 void 狀態(tài)

可以在轉場中組合使用通配符和 ?void ?狀態(tài),以觸發(fā)那些進入和離開頁面的動畫:

  • 當元素離開視圖時,就會觸發(fā) ?* => void? 轉場,而不管它離開前處于什么狀態(tài)
  • 當元素進入視圖時,就會觸發(fā) ?void => *? 轉場,而不管它進入時處于什么狀態(tài)
  • 通配符狀態(tài) ?*? 會匹配任何狀態(tài) —— 包括 ?void?

播放進入和離開視圖時的動畫

本節(jié)介紹如何為進入和離開頁面的元素設置動畫。

添加一些新的行為:

  • 當你把一個英雄添加到英雄列表中時,它看起來是從左側飛進頁面的
  • 當你從列表中移除一個英雄時,它看起來是從右側飛出去的
animations: [
  trigger('flyInOut', [
    state('in', style({ transform: 'translateX(0)' })),
    transition('void => *', [
      style({ transform: 'translateX(-100%)' }),
      animate(100)
    ]),
    transition('* => void', [
      animate(100, style({ transform: 'translateX(100%)' }))
    ])
  ])
]

在上述代碼中,當 HTML 元素沒有附著在視圖中時,我們就會應用 ?void ?狀態(tài)。

:enter 和 :leave 別名

?:enter? 和 ?:leave? 分別是 ?void => *? 和 ?* => void? 的別名。這些別名供多個動畫函數(shù)使用。

transition ( ':enter', [ … ] );  // alias for void => *
transition ( ':leave', [ … ] );  // alias for * => void

定位進入視圖的元素更難,因為它不在 DOM 中。因此,使用別名 ?:enter? 和 ?:leave? 來定位要從視圖中插入或刪除的 HTML 元素。

和 :enter 與 :leave 一起使用 *ngIf 和 *ngFor

當任何 ?*ngIf? 或 ?*ngFor? 中的視圖放進頁面中時,會運行 ?:enter? 轉場;當移除這些視圖時,就會運行 ?:leave? 轉場。

注意:
進入/離開行為有時會令人困惑。作為經(jīng)驗法則,考慮到 Angular 添加到 DOM 的任何元素都會通過 ?:enter? 轉換傳遞,但只有通過 Angular 直接從 DOM 刪除的元素會通過 ?:leave? 轉換傳遞(例如,元素的視圖是從 DOM,因為其父級正在從 DOM 中刪除或應用程序的路由已更改,則元素將不會通過 ?:leave? 轉換)。

本例子中有一個名叫 ?myInsertRemoveTrigger ?的觸發(fā)器,來表示進入和離開動畫。其 HTML 模板包含下列代碼。

<div @myInsertRemoveTrigger *ngIf="isShown" class="insert-remove-container">
  <p>The box is inserted</p>
</div>

在組件文件中,?:enter? 轉場會將初始透明度設置為 0,然后設置動畫,當該元素已經(jīng)插入視圖中之后,把這個透明度設置為 1。

trigger('myInsertRemoveTrigger', [
  transition(':enter', [
    style({ opacity: 0 }),
    animate('100ms', style({ opacity: 1 })),
  ]),
  transition(':leave', [
    animate('100ms', style({ opacity: 0 }))
  ])
]),

請注意,此示例不需要使用?state()?。

轉場中的 :increment 和 :decrement

?transition()? 函數(shù)還能接受額外的選擇器值:?:increment? 和 ?:decrement?。當數(shù)值增加或減小時,使用這些來啟動轉場。

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 })),
      ]),
    ])
  ]),
]),

轉場中的邏輯值

如果某個觸發(fā)器以邏輯型的值作為綁定值,那么就可以使用能與 ?true ?和 ?false? 或 1 和 0 相比較的 ?transition()? 表達式來匹配這個值。

<div [@openClose]="isOpen ? true : false" class="open-close-container">
</div>

在上述代碼片段中,HTML 模板將 ?<div>? 元素綁定到名為 ?openClose ?的觸發(fā)器,其狀態(tài)表達式是 ?isOpen?,可能的值為 ?true ?和 ?false?。這種模式可以代替創(chuàng)建兩個命名狀態(tài) ?open ?和 ?close ?的方式。

在組件代碼中,?@Component? 元數(shù)據(jù)下的 ?animations:? 屬性中,當該狀態(tài)求值為 ?true? 時(這里表示 "open"),相關 HTML 元素的高度值為通配符樣式 ?*? 或某個默認值。在這種情況下,它會使用此元素開始動畫前的現(xiàn)有高度。當該元素是 "closed" 時,它的高度會從指定的高度運動到 0,這會讓它不可見。

animations: [
  trigger('openClose', [
    state('true', style({ height: '*' })),
    state('false', style({ height: '0px' })),
    transition('false <=> true', animate(500))
  ])
],

多重動畫觸發(fā)器

你可以為組件定義多個動畫觸發(fā)器并將這些動畫觸發(fā)器附著到不同的元素上,這些元素之間的父子關系會影響動畫的運行方式和時機。

父-子動畫

每次在 Angular 中觸發(fā)動畫時,父動畫始終會優(yōu)先,而子動畫會被阻塞。為了運行子動畫,父動畫必須查詢出包含子動畫的每個元素,然后使用 ?animateChild()? 函數(shù)來運行它們。

在某個 HTML 元素上禁用動畫

可以把一個名叫 ?@.disabled? 的動畫控制綁定放在 HTML 元素上,以禁用該元素及其子元素上的動畫。當 ?@.disabled? 綁定為 ?true ?時,就會禁止渲染所有動畫。

下面的代碼范例展示了如何使用此特性。

  • src/app/open-close.component.html
  • <div [@.disabled]="isDisabled">
      <div [@childAnimation]="isOpen ? 'open' : 'closed'"
        class="open-close-container">
        <p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
      </div>
    </div>
  • src/app/open-close.component.ts
  • @Component({
      animations: [
        trigger('childAnimation', [
          // ...
        ]),
      ],
    })
    export class OpenCloseChildComponent {
      isDisabled = false;
      isOpen = false;
    }

當 ?@.disabled? 綁定為 ?true ?時,?@childAnimation? 觸發(fā)器就不會啟動。

當 HTML 模板中的某個元素使用 ?@.disabled? 禁止了動畫時,也會同時禁止其所有內(nèi)部元素的動畫。你無法有選擇的單獨禁用單個元素上的多個動畫。

不過,選擇性的子動畫仍然可以用如下方式之一在已禁用的父元素上運行:

  • 父動畫可以使用 ?query()? 函數(shù)來收集 HTML 模板中位于禁止動畫區(qū)域內(nèi)部的元素。這些元素仍然可以播放動畫。
  • 子動畫可以被父動畫查詢,并且稍后使用 ?animateChild()? 來播放它。

禁用所有動畫

要禁用 Angular 應用中的所有動畫,只要把 ?@.disabled? 綁定放在頂層的 Angular 組件上即可。

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.css'],
  animations: [
    slideInAnimation
  ]
})
export class AppComponent {
  @HostBinding('@.disabled')
  public animationsDisabled = false;
}
注意:
禁用應用級的動畫在端到端(E2E)測試中是很有用的。

動畫回調(diào)

當動畫啟動和終止時,?trigger()? 函數(shù)會發(fā)出一些回調(diào)。在下面的例子中,我們有一個包含 ?openClose ?觸發(fā)器的組件。

@Component({
  selector: 'app-open-close',
  animations: [
    trigger('openClose', [
      // ...
    ]),
  ],
  templateUrl: 'open-close.component.html',
  styleUrls: ['open-close.component.css']
})
export class OpenCloseComponent {
  onAnimationEvent(event: AnimationEvent) {
  }
}

在 HTML 模板中,動畫事件可以通過 ?$event? 傳遞回來,比如 ?@triggerName.start? 和 ?@triggerName.done?,這里的 ?triggerName ?表示所使用的觸發(fā)器名字。在我們的例子中,?openClose ?觸發(fā)器將會是這樣的。

<div [@openClose]="isOpen ? 'open' : 'closed'"
    (@openClose.start)="onAnimationEvent($event)"
    (@openClose.done)="onAnimationEvent($event)"
    class="open-close-container">
</div>

動畫回調(diào)的潛在用途之一,是用來覆蓋比較慢的 API 調(diào)用,比如查閱數(shù)據(jù)庫。比如,你可以建立一個 InProgress 按鈕,讓它擁有自己的循環(huán)動畫。當后端系統(tǒng)操作完成時,它會播放脈動效果或其它一些視覺動作。

然后,在當前動畫結束時,可以調(diào)用另一個動畫。比如,當 API 調(diào)用完成時,按鈕會從 ?inProgress ?狀態(tài)變成 ?closed ?狀態(tài)。

動畫可以影響最終用戶,讓他覺得操作更快 —— 雖然并沒有。因此,簡單的動畫是保持用戶滿意的一種經(jīng)濟有效的手段,而不必尋求提高服務器調(diào)用的速度或被迫補救那些你無法控制的情況,比如不可靠的網(wǎng)絡連接。

回調(diào)可以作為調(diào)試工具,比如與 ?console.warn()? 結合使用,以便在瀏覽器的開發(fā)者控制臺中查看應用的進度。下列代碼片段為我們原始的雙態(tài)按鈕(?open ?與 ?closed?)范例創(chuàng)建了控制臺輸出。

export class OpenCloseComponent {
  onAnimationEvent(event: AnimationEvent) {
    // openClose is trigger name in this example
    console.warn(`Animation Trigger: ${event.triggerName}`);

    // phaseName is "start" or "done"
    console.warn(`Phase: ${event.phaseName}`);

    // in our example, totalTime is 1000 (number of milliseconds in a second)
    console.warn(`Total time: ${event.totalTime}`);

    // in our example, fromState is either "open" or "closed"
    console.warn(`From: ${event.fromState}`);

    // in our example, toState either "open" or "closed"
    console.warn(`To: ${event.toState}`);

    // the HTML element itself, the button in this case
    console.warn(`Element: ${event.element}`);
  }
}

關鍵幀動畫

前一節(jié)是簡單的雙態(tài)轉場?,F(xiàn)在,我們要使用關鍵幀動畫創(chuàng)建一個具有多個順序執(zhí)行步驟的動畫。

Angular 的 ?keyframe()? 函數(shù)類似于 CSS 中的關鍵幀。關鍵幀允許在單個時間段內(nèi)進行多種樣式更改。比如,我們的按鈕可以在單個的 2 秒時間段內(nèi)多次改變顏色,而不是漸隱掉。


這些更改顏色的代碼如下所示。

transition('* => active', [
  animate('2s', keyframes([
    style({ backgroundColor: 'blue' }),
    style({ backgroundColor: 'red' }),
    style({ backgroundColor: 'orange' })
  ]))

偏移

關鍵幀包括一個用來定義動畫中每個樣式何時開始更改的偏移(offset)屬性。偏移是個 0 到 1 之間的相對值,分別標記動畫的開始和結束時間,并且只要使用了它,就要同樣應用于這個關鍵幀的每個步驟。

定義關鍵幀的偏移量是可選的。如果省略它們,就會自動分配均勻間隔的偏移。比如,三個沒有預定義偏移的關鍵幀會分別使用 0、0.5、1 作為偏移。在上面的例子中,還可以為中間的轉場指定偏移量 0.8。代碼如下。


帶有指定偏移量的代碼如下。

transition('* => active', [
  animate('2s', keyframes([
    style({ backgroundColor: 'blue', offset: 0}),
    style({ backgroundColor: 'red', offset: 0.8}),
    style({ backgroundColor: '#754600', offset: 1.0})
  ])),
]),
transition('* => inactive', [
  animate('2s', keyframes([
    style({ backgroundColor: '#754600', offset: 0}),
    style({ backgroundColor: 'red', offset: 0.2}),
    style({ backgroundColor: 'blue', offset: 1.0})
  ]))
]),

你可以在單個動畫中組合使用 ?duration?、?delay ?和 ?easing ?來定義關鍵幀。

帶脈動效果的關鍵幀

通過在整個動畫中定義特定偏移處的樣式,可以使用關鍵幀在動畫中創(chuàng)建脈動效果。

下面是使用關鍵幀創(chuàng)建脈動效果的例子:

  • 原始的 ?open ?和 ?closed ?狀態(tài)(包括其原始的高度、顏色和透明度)會在一秒鐘內(nèi)逐漸發(fā)生變化。
  • 插在中間的關鍵幀序列會導致該按鈕在一秒鐘內(nèi)出現(xiàn)不規(guī)則的脈動。


此動畫的代碼片段是這樣的。

trigger('openClose', [
  state('open', style({
    height: '200px',
    opacity: 1,
    backgroundColor: 'yellow'
  })),
  state('close', style({
    height: '100px',
    opacity: 0.5,
    backgroundColor: 'green'
  })),
  // ...
  transition('* => *', [
    animate('1s', keyframes ( [
      style({ opacity: 0.1, offset: 0.1 }),
      style({ opacity: 0.6, offset: 0.2 }),
      style({ opacity: 1,   offset: 0.5 }),
      style({ opacity: 0.2, offset: 0.7 })
    ]))
  ])
])

可動的屬性與單位

Angular 的動畫支持是基于 Web 動畫的,所以你可以動瀏覽器認為可動(animatable)的任意屬性。包括位置、大小、變形、顏色、邊框等。W3C 在 CSS 轉場頁也維護了一個可動屬性的列表。

對于帶有數(shù)值的位置屬性,可以把值作為字符串(別忘了帶引號)并使用適當?shù)暮缶Y來定義其單位:

  • 50 像素:?'50px' ?
  • 相對字體大?。?'3em' ?
  • 百分比:?'100%' ?

你還可以用數(shù)字形式提供這個值(不帶單位),這種情況下,Angular 假設默認的單位是像素(?px?)。把 50 像素表示為 ?50 ?和 ?'50px'? 是一樣的。

注意:
字符串形式的 ?"50"? 是無效的。

使用通配符自動計算屬性

有時你在運行之前并不知道某個樣式的屬性值。比如,元素的寬度和高度通常取決于其內(nèi)容和屏幕大小。在使用 CSS 動畫時,這些屬性通常會具有挑戰(zhàn)性(譯注:因為 CSS 動畫不支持自動確定寬高)。

這些情況下,你可以在 ?style()? 中指定通配符 ?*? 屬性,以便在運行期間計算該屬性的值,然后把它插入到動畫中。

下面的例子中有一個名叫 ?shrinkOut ?的觸發(fā)器,它會在 HTML 元素離開頁面時使用。該動畫會使用它離開之前的任意高度,并從該高度動畫到 0。

animations: [
  trigger('shrinkOut', [
    state('in', style({ height: '*' })),
    transition('* => void', [
      style({ height: '*' }),
      animate(250, style({ height: 0 }))
    ])
  ])
]

關鍵幀動畫總結

Angular 中的 ?keyframes()? 函數(shù)允許你在單個轉場中指定多個臨時樣式,并使用可選的 ?offset ?來定義動畫中每次樣式變化的發(fā)生時機。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號