Ecossistema Estendido
Animations Legadas

Transitions e triggers de animação

IMPORTANTE: O pacote @angular/animations agora está obsoleto. A equipe do Angular recomenda usar CSS nativo com animate.enter e animate.leave para animações em todos os códigos novos. Saiba mais no novo guia de animação de enter e leave. Veja também Migrando do pacote de Animations do Angular para aprender como você pode começar a migrar para animações CSS puras em suas aplicações.

Este guia aprofunda em estados de transição especiais como o wildcard * e void. Ele mostra como esses estados especiais são usados para elementos entrando e saindo de uma view. Esta seção também explora múltiplos triggers de animação, callbacks de animação e animação baseada em sequência usando keyframes.

Estados predefinidos e correspondência com wildcard

No Angular, estados de transição podem ser definidos explicitamente através da função state(), ou usando os estados predefinidos wildcard * e void.

Estado wildcard

Um asterisco * ou wildcard corresponde a qualquer estado de animação. Isso é útil para definir transições que se aplicam independentemente do estado inicial ou final do elemento HTML.

Por exemplo, uma transição de open => * se aplica quando o estado do elemento muda de open para qualquer outra coisa.

wildcard state expressions

O código a seguir é outro exemplo usando o estado wildcard junto com o exemplo anterior usando os estados open e closed. Em vez de definir cada par de transição estado-para-estado, qualquer transição para closed leva 1 segundo, e qualquer transição para open leva 0.5 segundos.

Isso permite a adição de novos estados sem ter que incluir transições separadas para cada um.

open-close.component.ts

import {Component, input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';@Component({  selector: 'app-open-close',  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')]),    ]),  ],  templateUrl: 'open-close.component.html',  styleUrls: ['open-close.component.css'],})export class OpenCloseComponent {  logging = input(false);  isOpen = true;  toggle() {    this.isOpen = !this.isOpen;  }  onAnimationEvent(event: AnimationEvent) {    if (!this.logging) {      return;    }    // 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}`);  }}

Use uma sintaxe de seta dupla para especificar transições estado-para-estado em ambas as direções.

open-close.component.ts

import {Component, input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';@Component({  selector: 'app-open-close',  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')]),    ]),  ],  templateUrl: 'open-close.component.html',  styleUrls: ['open-close.component.css'],})export class OpenCloseComponent {  logging = input(false);  isOpen = true;  toggle() {    this.isOpen = !this.isOpen;  }  onAnimationEvent(event: AnimationEvent) {    if (!this.logging) {      return;    }    // 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}`);  }}

Use o estado wildcard com múltiplos estados de transição

No exemplo do botão de dois estados, o wildcard não é tão útil porque há apenas dois estados possíveis, open e closed. Em geral, use estados wildcard quando um elemento tiver múltiplos estados potenciais para os quais ele pode mudar. Se o botão pode mudar de open para closed ou para algo como inProgress, usar um estado wildcard pode reduzir a quantidade de código necessária.

wildcard state with 3 states

open-close.component.ts

import {Component, input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';@Component({  selector: 'app-open-close',  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')]),    ]),  ],  templateUrl: 'open-close.component.html',  styleUrls: ['open-close.component.css'],})export class OpenCloseComponent {  logging = input(false);  isOpen = true;  toggle() {    this.isOpen = !this.isOpen;  }  onAnimationEvent(event: AnimationEvent) {    if (!this.logging) {      return;    }    // 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}`);  }}

A transição * => * se aplica quando qualquer mudança entre dois estados ocorre.

As transições são correspondidas na ordem em que são definidas. Assim, você pode aplicar outras transições sobre a transição * => *. Por exemplo, defina mudanças de estilo ou animações que se aplicariam apenas a open => closed, e então use * => * como um fallback para pares de estados que não são especificados de outra forma.

Para fazer isso, liste as transições mais específicas antes de * => *.

Use wildcards com estilos

Use o wildcard * com um estilo para dizer à animação para usar qualquer que seja o valor de estilo atual, e animar com ele. Wildcard é um valor de fallback usado se o estado sendo animado não for declarado dentro do trigger.

open-close.component.ts

import {Component, input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';@Component({  selector: 'app-open-close',  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')]),    ]),  ],  templateUrl: 'open-close.component.html',  styleUrls: ['open-close.component.css'],})export class OpenCloseComponent {  logging = input(false);  isOpen = true;  toggle() {    this.isOpen = !this.isOpen;  }  onAnimationEvent(event: AnimationEvent) {    if (!this.logging) {      return;    }    // 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}`);  }}

Estado void

Use o estado void para configurar transições para um elemento que está entrando ou saindo de uma página. Veja Animando entrada e saída de uma view.

Combine estados wildcard e void

Combine estados wildcard e void em uma transição para disparar animações que entram e saem da página:

  • Uma transição de * => void se aplica quando o elemento sai de uma view, independentemente de qual estado ele estava antes de sair
  • Uma transição de void => * se aplica quando o elemento entra em uma view, independentemente de qual estado ele assume ao entrar
  • O estado wildcard * corresponde a qualquer estado, incluindo void

Animando entrada e saída de uma view

Esta seção mostra como animar elementos entrando ou saindo de uma página.

Adicione um novo comportamento:

  • Quando você adiciona um herói à lista de heróis, ele parece voar para a página pela esquerda
  • Quando você remove um herói da lista, ele parece voar para fora pela direita

hero-list-enter-leave.component.ts

import {Component, input, output} from '@angular/core';import {trigger, state, style, animate, transition} from '@angular/animations';import {Hero} from './hero';@Component({  selector: 'app-hero-list-enter-leave',  template: `    <ul class="heroes">      @for (hero of heroes(); track hero) {        <li [@flyInOut]="'in'">          <button class="inner" type="button" (click)="removeHero(hero.id)">            <span class="badge">{{ hero.id }}</span>            <span class="name">{{ hero.name }}</span>          </button>        </li>      }    </ul>  `,  styleUrls: ['./hero-list-page.component.css'],  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%)'}))]),    ]),  ],})export class HeroListEnterLeaveComponent {  readonly heroes = input<Hero[]>([]);  readonly remove = output<number>();  removeHero(id: number) {    this.remove.emit(id);  }}

No código acima, você aplicou o estado void quando o elemento HTML não está anexado a uma view.

Aliases :enter e :leave

:enter e :leave são aliases para as transições void => * e * => void. Esses aliases são usados por várias funções de animação.

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

É mais difícil segmentar um elemento que está entrando em uma view porque ele ainda não está no DOM. Use os aliases :enter e :leave para segmentar elementos HTML que são inseridos ou removidos de uma view.

Use *ngIf e *ngFor com :enter e :leave

A transição :enter é executada quando quaisquer views *ngIf ou *ngFor são colocadas na página, e :leave é executada quando essas views são removidas da página.

IMPORTANTE: Comportamentos de entrada/saída podem às vezes ser confusos. Como regra geral, considere que qualquer elemento sendo adicionado ao DOM pelo Angular passa pela transição :enter. Apenas elementos sendo diretamente removidos do DOM pelo Angular passam pela transição :leave. Por exemplo, a view de um elemento é removida do DOM porque seu pai está sendo removido do DOM.

Este exemplo tem um trigger especial para a animação de entrada e saída chamado myInsertRemoveTrigger. O template HTML contém o seguinte código.

insert-remove.component.html

<h2>Insert/Remove</h2><nav>  <button type="button" (click)="toggle()">Toggle Insert/Remove</button></nav>@if (isShown) {  <div @myInsertRemoveTrigger class="insert-remove-container">    <p>The box is inserted</p>  </div>}

No arquivo do component, a transição :enter define uma opacidade inicial de 0. Em seguida, ela anima para mudar essa opacidade para 1 à medida que o elemento é inserido na view.

insert-remove.component.ts

import {Component} from '@angular/core';import {trigger, transition, animate, style} from '@angular/animations';@Component({  selector: 'app-insert-remove',  animations: [    trigger('myInsertRemoveTrigger', [      transition(':enter', [style({opacity: 0}), animate('100ms', style({opacity: 1}))]),      transition(':leave', [animate('100ms', style({opacity: 0}))]),    ]),  ],  templateUrl: 'insert-remove.component.html',  styleUrls: ['insert-remove.component.css'],})export class InsertRemoveComponent {  isShown = false;  toggle() {    this.isShown = !this.isShown;  }}

Note que este exemplo não precisa usar state().

Transições :increment e :decrement

A função transition() aceita outros valores de seletor, :increment e :decrement. Use-os para iniciar uma transição quando um valor numérico aumentou ou diminuiu em valor.

ÚTIL: O exemplo a seguir usa os métodos query() e stagger(). Para mais informações sobre esses métodos, veja a página de sequências complexas.

hero-list-page.component.ts

import {Component, HostBinding, OnInit} from '@angular/core';import {trigger, transition, animate, style, query, stagger} from '@angular/animations';import {HEROES} from './mock-heroes';import {Hero} from './hero';@Component({  selector: 'app-hero-list-page',  templateUrl: 'hero-list-page.component.html',  styleUrls: ['hero-list-page.component.css'],  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'})),          ]),        ]),      ]),    ]),    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 HeroListPageComponent implements OnInit {  @HostBinding('@pageAnimations')  public animatePage = true;  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;    }  }}

Valores booleanos em transições

Se um trigger contém um valor booleano como valor de binding, então este valor pode ser correspondido usando uma expressão transition() que compara true e false, ou 1 e 0.

open-close.component.html

<nav>  <button type="button" (click)="toggle()">Toggle Boolean/Close</button></nav><div [@openClose]="isOpen ? true : false" class="open-close-container">  <p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p></div>

No trecho de código acima, o template HTML vincula um elemento <div> a um trigger chamado openClose com uma expressão de status de isOpen, e com valores possíveis de true e false. Este padrão é uma alternativa à prática de criar dois estados nomeados como open e close.

Dentro dos metadados do @Component sob a propriedade animations:, quando o estado avalia para true, a altura do elemento HTML associado é um estilo wildcard ou padrão. Neste caso, a animação usa qualquer altura que o elemento já tinha antes da animação começar. Quando o elemento está closed, o elemento é animado para uma altura de 0, o que o torna invisível.

open-close.component.ts

import {Component} from '@angular/core';import {trigger, transition, state, animate, style} from '@angular/animations';@Component({  selector: 'app-open-close-boolean',  animations: [    trigger('openClose', [      state('true', style({height: '*'})),      state('false', style({height: '0px'})),      transition('false <=> true', animate(500)),    ]),  ],  templateUrl: 'open-close.component.2.html',  styleUrls: ['open-close.component.css'],})export class OpenCloseBooleanComponent {  isOpen = false;  toggle() {    this.isOpen = !this.isOpen;  }}

Múltiplos triggers de animação

Você pode definir mais de um trigger de animação para um component. Anexe triggers de animação a diferentes elementos, e as relações pai-filho entre os elementos afetam como e quando as animações são executadas.

Animações pai-filho

Cada vez que uma animação é disparada no Angular, a animação pai sempre tem prioridade e as animações filhas são bloqueadas. Para que uma animação filha seja executada, a animação pai deve consultar cada um dos elementos que contêm animações filhas. Em seguida, ela permite que as animações sejam executadas usando a função animateChild().

Desabilitar uma animação em um elemento HTML

Um binding especial de controle de animação chamado @.disabled pode ser colocado em um elemento HTML para desativar animações nesse elemento, bem como em quaisquer elementos aninhados. Quando true, o binding @.disabled impede que todas as animações sejam renderizadas.

O exemplo de código a seguir mostra como usar este recurso.

open-close.component.html

<nav>  <button type="button" (click)="toggleAnimations()">Toggle Animations</button>  <button type="button" (click)="toggle()">Toggle Open/Closed</button></nav><div [@.disabled]="isDisabled">  <div [@childAnimation]="isOpen ? 'open' : 'closed'"    class="open-close-container">    <p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>  </div></div>

open-close.component.ts

import {Component} from '@angular/core';import {trigger, transition, state, animate, style} from '@angular/animations';@Component({  selector: 'app-open-close-toggle',  templateUrl: 'open-close.component.4.html',  styleUrls: ['open-close.component.css'],  animations: [    trigger('childAnimation', [      // ...      state(        'open',        style({          width: '250px',          opacity: 1,          backgroundColor: 'yellow',        }),      ),      state(        'closed',        style({          width: '100px',          opacity: 0.8,          backgroundColor: 'blue',        }),      ),      transition('* => *', [animate('1s')]),    ]),  ],})export class OpenCloseChildComponent {  isDisabled = false;  isOpen = false;  toggleAnimations() {    this.isDisabled = !this.isDisabled;  }  toggle() {    this.isOpen = !this.isOpen;  }}

Quando o binding @.disabled é true, o trigger @childAnimation não é acionado.

Quando um elemento dentro de um template HTML tem animações desativadas usando o host binding @.disabled, as animações são desativadas em todos os elementos internos também. Você não pode desativar seletivamente múltiplas animações em um único elemento.

Animações filhas seletivas ainda podem ser executadas em um pai desabilitado de uma das seguintes maneiras:

  • Uma animação pai pode usar a função query() para coletar elementos internos localizados em áreas desabilitadas do template HTML. Esses elementos ainda podem ser animados.
  • Uma animação filha pode ser consultada por um pai e então animada posteriormente com a função animateChild()

Desabilitar todas as animações

Para desativar todas as animações de uma aplicação Angular, coloque o host binding @.disabled no component Angular de nível superior.

app.component.ts

import {Component, HostBinding, inject} from '@angular/core';import {  trigger,  state,  style,  animate,  transition,  // ...} from '@angular/animations';import {ChildrenOutletContexts, RouterLink, RouterOutlet} from '@angular/router';import {slideInAnimation} from './animations';@Component({  selector: 'app-root',  templateUrl: 'app.component.html',  styleUrls: ['app.component.css'],  imports: [RouterLink, RouterOutlet],  animations: [    slideInAnimation,    // animation triggers go here  ],})export class AppComponent {  @HostBinding('@.disabled')  public animationsDisabled = false;  private contexts = inject(ChildrenOutletContexts);  getRouteAnimationData() {    return this.contexts.getContext('primary')?.route?.snapshot?.data?.['animation'];  }  toggleAnimations() {    this.animationsDisabled = !this.animationsDisabled;  }}

ÚTIL: Desabilitar animações em toda a aplicação é útil durante testes end-to-end (E2E).

Callbacks de animação

A função trigger() de animação emite callbacks quando ela inicia e quando termina. O exemplo a seguir apresenta um component que contém um trigger openClose.

open-close.component.ts

import {Component, input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';@Component({  selector: 'app-open-close',  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')]),    ]),  ],  templateUrl: 'open-close.component.html',  styleUrls: ['open-close.component.css'],})export class OpenCloseComponent {  logging = input(false);  isOpen = true;  toggle() {    this.isOpen = !this.isOpen;  }  onAnimationEvent(event: AnimationEvent) {    if (!this.logging) {      return;    }    // 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}`);  }}

No template HTML, o evento de animação é passado de volta via $event, como @triggerName.start e @triggerName.done, onde triggerName é o nome do trigger sendo usado. Neste exemplo, o trigger openClose aparece da seguinte forma.

open-close.component.html

<nav>  <button type="button" (click)="toggle()">Toggle Open/Close</button></nav>  <div [@openClose]="isOpen ? 'open' : 'closed'"    (@openClose.start)="onAnimationEvent($event)"    (@openClose.done)="onAnimationEvent($event)"    class="open-close-container">  <p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p></div>

Um uso potencial para callbacks de animação poderia ser cobrir uma chamada de API lenta, como uma consulta a banco de dados. Por exemplo, um botão InProgress pode ser configurado para ter sua própria animação em loop enquanto a operação do sistema backend termina.

Outra animação pode ser chamada quando a animação atual termina. Por exemplo, o botão vai do estado inProgress para o estado closed quando a chamada de API é concluída.

Uma animação pode influenciar um usuário final a perceber a operação como mais rápida, mesmo quando não é.

Callbacks podem servir como uma ferramenta de depuração, por exemplo, em conjunto com console.warn() para visualizar o progresso da aplicação no Console JavaScript do navegador para desenvolvedores. O trecho de código a seguir cria uma saída de log do console para o exemplo original, um botão com os dois estados open e closed.

open-close.component.ts

import {Component, input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';@Component({  selector: 'app-open-close',  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')]),    ]),  ],  templateUrl: 'open-close.component.html',  styleUrls: ['open-close.component.css'],})export class OpenCloseComponent {  logging = input(false);  isOpen = true;  toggle() {    this.isOpen = !this.isOpen;  }  onAnimationEvent(event: AnimationEvent) {    if (!this.logging) {      return;    }    // 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}`);  }}

Keyframes

Para criar uma animação com múltiplas etapas executadas em sequência, use keyframes.

A função keyframe() do Angular permite várias mudanças de estilo dentro de um único segmento de tempo. Por exemplo, o botão, em vez de desaparecer, poderia mudar de cor várias vezes ao longo de um único período de 2 segundos.

keyframes

O código para esta mudança de cor poderia ser assim.

status-slider.component.ts

import {Component} from '@angular/core';import {trigger, transition, state, animate, style, keyframes} from '@angular/animations';@Component({  selector: 'app-status-slider',  templateUrl: 'status-slider.component.html',  styleUrls: ['status-slider.component.css'],  animations: [    trigger('slideStatus', [      state('inactive', style({backgroundColor: 'blue'})),      state('active', style({backgroundColor: '#754600'})),      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}),          ]),        ),      ]),      transition('* => active', [        animate(          '2s',          keyframes([            style({backgroundColor: 'blue'}),            style({backgroundColor: 'red'}),            style({backgroundColor: 'orange'}),          ]),        ),      ]),    ]),  ],})export class StatusSliderComponent {  status: 'active' | 'inactive' = 'inactive';  toggle() {    if (this.status === 'active') {      this.status = 'inactive';    } else {      this.status = 'active';    }  }}

Offset

Keyframes incluem um offset que define o ponto na animação onde cada mudança de estilo ocorre. Offsets são medidas relativas de zero a um, marcando o início e o fim da animação. Eles devem ser aplicados a cada uma das etapas de keyframe se usados pelo menos uma vez.

Definir offsets para keyframes é opcional. Se você omiti-los, offsets igualmente espaçados são atribuídos automaticamente. Por exemplo, três keyframes sem offsets predefinidos recebem offsets de 0, 0.5 e 1. Especificar um offset de 0.8 para a transição do meio no exemplo anterior poderia ser assim.

keyframes with offset

O código com offsets especificados seria o seguinte.

status-slider.component.ts

import {Component} from '@angular/core';import {trigger, transition, state, animate, style, keyframes} from '@angular/animations';@Component({  selector: 'app-status-slider',  templateUrl: 'status-slider.component.html',  styleUrls: ['status-slider.component.css'],  animations: [    trigger('slideStatus', [      state('inactive', style({backgroundColor: 'blue'})),      state('active', style({backgroundColor: '#754600'})),      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}),          ]),        ),      ]),      transition('* => active', [        animate(          '2s',          keyframes([            style({backgroundColor: 'blue'}),            style({backgroundColor: 'red'}),            style({backgroundColor: 'orange'}),          ]),        ),      ]),    ]),  ],})export class StatusSliderComponent {  status: 'active' | 'inactive' = 'inactive';  toggle() {    if (this.status === 'active') {      this.status = 'inactive';    } else {      this.status = 'active';    }  }}

Você pode combinar keyframes com duration, delay e easing dentro de uma única animação.

Keyframes com pulsação

Use keyframes para criar um efeito de pulsação em suas animações definindo estilos em offsets específicos ao longo da animação.

Aqui está um exemplo de uso de keyframes para criar um efeito de pulsação:

  • Os estados originais open e closed, com as mudanças originais de altura, cor e opacidade, ocorrendo ao longo de um período de tempo de 1 segundo
  • Uma sequência de keyframes inserida no meio que faz com que o botão pareça pulsar irregularmente ao longo desse mesmo período de tempo de 1 segundo
keyframes with irregular pulsation

O trecho de código para esta animação poderia ser assim.

open-close.component.ts

import {Component, input} from '@angular/core';import {  trigger,  transition,  state,  animate,  style,  keyframes,  AnimationEvent,} from '@angular/animations';@Component({  selector: 'app-open-close',  animations: [    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}),          ]),        ),      ]),    ]),  ],  templateUrl: 'open-close.component.html',  styleUrls: ['open-close.component.css'],})export class OpenCloseKeyframeComponent {  isOpen = false;  toggle() {    this.isOpen = !this.isOpen;  }  logging = input(false);  onAnimationEvent(event: AnimationEvent) {    if (!this.logging) {      return;    }  }}

Propriedades e unidades animáveis

As animações do Angular são construídas sobre web animations, então você pode animar qualquer propriedade que o navegador considere animável. Isso inclui posições, tamanhos, transformações, cores, bordas e muito mais. O W3C mantém uma lista de propriedades animáveis em sua página de CSS Transitions.

Para propriedades com um valor numérico, defina uma unidade fornecendo o valor como uma string, entre aspas, com o sufixo apropriado:

  • 50 pixels: '50px'

  • Tamanho de fonte relativo: '3em'

  • Porcentagem: '100%'

Você também pode fornecer o valor como um número. Nesses casos, o Angular assume uma unidade padrão de pixels, ou px. Expressar 50 pixels como 50 é o mesmo que dizer '50px'.

ÚTIL: A string "50" não seria considerada válida).

Cálculo automático de propriedade com wildcards

Às vezes, o valor de uma propriedade de estilo dimensional não é conhecido até o runtime. Por exemplo, elementos frequentemente têm larguras e alturas que dependem de seu conteúdo ou do tamanho da tela. Essas propriedades são frequentemente desafiadoras de animar usando CSS.

Nesses casos, você pode usar um valor de propriedade wildcard * especial sob style(). O valor dessa propriedade de estilo específica é computado em runtime e então conectado à animação.

O exemplo a seguir tem um trigger chamado shrinkOut, usado quando um elemento HTML sai da página. A animação pega qualquer altura que o elemento tenha antes de sair, e anima dessa altura para zero.

hero-list-auto.component.ts

import {Component, Output, EventEmitter, input} from '@angular/core';import {trigger, state, style, animate, transition} from '@angular/animations';import {Hero} from './hero';@Component({  selector: 'app-hero-list-auto',  templateUrl: 'hero-list-auto.component.html',  styleUrls: ['./hero-list-page.component.css'],  animations: [    trigger('shrinkOut', [      state('in', style({height: '*'})),      transition('* => void', [style({height: '*'}), animate(250, style({height: 0}))]),    ]),  ],})export class HeroListAutoComponent {  readonly heroes = input<Hero[]>([]);  @Output() remove = new EventEmitter<number>();  removeHero(id: number) {    this.remove.emit(id);  }}

Resumo de keyframes

A função keyframes() no Angular permite especificar múltiplos estilos intermediários dentro de uma única transição. Um offset opcional pode ser usado para definir o ponto na animação onde cada mudança de estilo deve ocorrer.

Mais sobre animações do Angular

Você também pode estar interessado no seguinte: