拡張エコシステム
レガシーアニメーション

複雑なアニメーションシーケンス

IMPORTANT: @angular/animationsパッケージは現在非推奨です。Angularチームは、新しく書くコードのアニメーションにはanimate.enteranimate.leaveを使ったネイティブCSSの利用を推奨します。詳しくは、新しいenterとleaveのアニメーションガイドを参照してください。また、アプリケーションで純粋なCSSアニメーションへの移行を始める方法については、AngularのAnimationsパッケージからの移行も参照してください。

ここまで、単一のHTML要素のシンプルなアニメーションを学んできました。 Angularでは、ページに出入りする要素のグリッド全体やリスト全体など、連携したシーケンスもアニメーション化できます。 複数のアニメーションの同時実行と、個別アニメーションの順次実行の両方が可能です。

複雑なアニメーションシーケンスを制御する関数は次のとおりです:

関数 詳細
query() 内側のHTML要素を1つ以上見つけます。
stagger() 複数要素のアニメーションに段階的な遅延を適用します。
group() 複数のアニメーションステップを並行して実行します。
sequence() アニメーションステップを順番に実行します。

query() 関数

多くの複雑なアニメーションは、query() 関数で子要素を見つけ、それらにアニメーションを適用することに依存します。基本的な例は次のとおりです:

詳細
query() の後に animate() 単純なHTML要素をクエリし、直接アニメーションを適用するために使用します。
query() の後に animateChild() それ自体にアニメーションメタデータが適用された子要素をクエリし、そのアニメーションをトリガーします(そうしないと、現在の要素/親要素のアニメーションによってブロックされてしまいます)。

query() の最初の引数は CSSセレクター 文字列で、次のAngular固有のトークンも含められます:

トークン 詳細
:enter
:leave
enter/leaveする要素用。
:animating 現在アニメーション中の要素用。
@*
@triggerName
任意のトリガー、または特定のトリガーを持つ要素用。
:self アニメーション対象の要素自身。

enter と leave する要素

すべての子要素が実際にenter/leaveする要素として扱われるわけではありません。これは直感に反して混乱しやすい場合があります。詳しくは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() 関数を使った並行アニメーション

ここまで、連続する各アニメーションの間に遅延を入れる方法を見てきました。 しかし、並行して実行されるアニメーションを設定したい場合もあるでしょう。 たとえば、同じ要素の2つのCSSプロパティをアニメーションしたいが、それぞれで異なる easing 関数を使いたい場合があります。 そのような場合は、アニメーションの group() 関数を使用できます。

HELPFUL: group() 関数は、アニメーション化される要素ではなく、アニメーションの ステップ をグループ化するために使います。

次の例は、:enter:leave の両方で group() を使用し、2つの異なるタイミング設定を適用します。これにより、同じ要素に2つの独立したアニメーションを並行して適用できます。

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

順次アニメーションと並行アニメーション

複雑なアニメーションでは、同時に多くのことが起きえます。 では、複数のアニメーションが1つずつ順番に実行されるアニメーションを作りたい場合はどうでしょうか?先ほどは group() を使って、複数のアニメーションを同時に並行実行しました。

別の関数 sequence() を使うと、同じアニメーションを1つずつ順番に実行できます。 sequence() の中では、アニメーションステップは style() または animate() 関数呼び出しで構成されます。

  • style() を使って、指定したスタイルデータを即座に適用します。
  • animate() を使って、指定した時間間隔にわたってスタイルデータを適用します。

フィルターアニメーションの例

例のページにある別のアニメーションも見てみましょう。 Filter/Staggerタブで、Search Heroes テキストボックスに Magnettornado などの文字を入力します。

フィルターは入力に合わせてリアルタイムに動作します。 文字を入力するごとにフィルターが徐々に厳しくなり、要素がページから消えていきます。 フィルターボックスの文字を削除すると、ヒーローのリストが徐々にページに再表示されます。

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 には3つのトランジションが含まれます。

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から離脱する要素は、不透明度と幅を0に設定して非表示にします。

  • DOMに入ってくる要素を300ミリ秒かけてアニメーションします。 アニメーション中は、要素が既定の幅と不透明度になります。

  • DOMに入ってくる要素/離脱する要素が複数ある場合は、ページ上部から順に、各要素の間に50ミリ秒の遅延を入れてアニメーションをずらします。

並び替えられるリストの項目をアニメーション化する

Angularは既定で *ngFor のリスト項目を正しくアニメーション化しますが、並び順が変わると正しくアニメーションできなくなります。 これは、どの要素がどれなのかを追跡できなくなり、アニメーションが壊れてしまうためです。 こうした要素をAngularが追跡できるようにする唯一の方法は、NgForOf ディレクティブに TrackByFunction を割り当てることです。 これにより、Angularは常にどの要素がどれなのかを把握できるため、常に正しい要素に正しいアニメーションを適用できます。

IMPORTANT: 実行時に並び順が変わる可能性のある *ngFor リストの項目をアニメーション化する必要がある場合は、常に TrackByFunction を使用してください。

アニメーションとコンポーネントのビューカプセル化

AngularのアニメーションはコンポーネントのDOM構造に基づいており、直接 ビューカプセル化 を考慮しません。つまり、ViewEncapsulation.Emulated を使用するコンポーネントは、ViewEncapsulation.None を使用している場合とまったく同じように振る舞います(ViewEncapsulation.ShadowDomViewEncapsulation.ExperimentalIsolatedShadowDom は、後述するように異なる挙動になります)。

たとえば、(このガイドの残りでも登場する)query() 関数を、エミュレートされたビューカプセル化を使用しているコンポーネントツリーの最上部に適用すると、そのクエリはツリーのどの深さにあるDOM要素も特定でき(その結果アニメーション化でき)ます。

一方、ViewEncapsulation.ShadowDomViewEncapsulation.ExperimentalIsolatedShadowDom は、DOM要素を ShadowRoot 要素の内側に「隠す」ことでコンポーネントのDOM構造を変更します。こうしたDOM操作は、アニメーションの実装が単純なDOM構造に依存し、ShadowRoot 要素を考慮しないため、一部のアニメーションが正しく動作しない原因になります。そのため、ShadowDomのビューカプセル化を使用するコンポーネントを含むビューにアニメーションを適用することは避けることを推奨します。

アニメーションシーケンスのまとめ

複数要素をアニメーション化するAngularの関数は、まず query() で内側の要素を見つけることから始まります。たとえば、<div> 内のすべての画像を収集する場合です。 残りの関数である stagger()group()sequence() では、カスケード(段階的な遅延)を適用したり、複数のアニメーションステップをどのように適用するかを制御したりできます。

Angularアニメーションについてさらに詳しく

以下の項目にも興味があるかもしれません: