Внести вклад
Расширенная экосистема
Устаревшие анимации

Сложные последовательности анимаций

ВАЖНО: Пакет @angular/animations теперь устарел. Команда Angular рекомендует использовать нативный CSS с animate.enter и animate.leave для анимаций во всём новом коде. Подробнее — в новом руководстве по enter- и leave-анимациям. Также см. Миграция с пакета Angular Animations, чтобы начать переход на чистые CSS-анимации в приложениях.

До сих пор мы рассматривали простые анимации отдельных HTML-элементов. Angular также позволяет анимировать согласованные последовательности — например, всю сетку или список элементов при появлении и уходе со страницы. Можно запускать несколько анимаций параллельно или выполнять дискретные анимации последовательно, одну за другой.

Функции, управляющие сложными последовательностями анимаций:

Функции Подробности
query() Находит один или несколько внутренних HTML-элементов.
stagger() Применяет каскадную задержку к анимациям нескольких элементов.
group() Запускает несколько шагов анимации параллельно.
sequence() Запускает шаги анимации один за другим.

Функция query()

Большинство сложных анимаций опираются на функцию query() для поиска дочерних элементов и применения к ним анимаций. Базовые примеры:

Примеры Подробности
query() followed by animate() Используется для запроса простых HTML-элементов и прямого применения к ним анимаций.
query() followed by animateChild() Используется для запроса дочерних элементов, у которых уже есть метаданные анимации, и запуска таких анимаций (которые иначе были бы заблокированы анимацией текущего/родительского элемента).

Первый аргумент query() — строка css selector, которая также может содержать следующие Angular-специфичные токены:

Токены Подробности
:enter
:leave
Для входящих/уходящих элементов.
:animating Для элементов, которые сейчас анимируются.
@*
@triggerName
Для элементов с любым — или конкретным — trigger.
:self Сам анимируемый элемент.

Entering and Leaving Elements

Не все дочерние элементы на самом деле считаются входящими/уходящими; иногда это может быть неочевидно и запутанно. Подробнее см. в query api docs.

Иллюстрацию этого также можно увидеть в примере анимаций (представленном в разделе введения) на вкладке Querying.

Анимация нескольких элементов с функциями query() и stagger()

После запроса дочерних элементов через query() функция stagger() позволяет задать временной промежуток между каждым элементом, анимируя элементы с задержкой между ними.

Следующий пример показывает, как использовать query() и stagger() для анимации списка (героев), добавляя каждого по очереди с небольшой задержкой сверху вниз.

  • Используйте query(), чтобы найти элемент, входящий на страницу и соответствующий определённым критериям

  • Для каждого из этих элементов используйте style(), чтобы задать одинаковый начальный стиль. Сделайте его прозрачным и используйте transform, чтобы сдвинуть его с позиции, чтобы он мог «въехать» на место.

  • Используйте stagger(), чтобы задержать каждую анимацию на 30 миллисекунд

  • Анимируйте каждый элемент на экране 0.5 секунды с кастомной easing-кривой, одновременно делая fade-in и снимая 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()

Вы видели, как добавить задержку между successive-анимациями. Но может понадобиться настроить анимации, происходящие параллельно. Например, анимировать два CSS-свойства одного элемента, но с разной функцией easing для каждого. Для этого можно использовать функцию анимации group().

ПОЛЕЗНО: Функция group() группирует шаги анимации, а не анимируемые элементы.

Следующий пример использует group() и на :enter, и на :leave для двух разных конфигураций тайминга, применяя две независимые анимации к одному элементу параллельно.

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

Последовательные vs. параллельные анимации

В сложных анимациях многое может происходить одновременно. Но что, если нужна анимация из нескольких анимаций, идущих одна за другой? Ранее вы использовали group() для запуска нескольких анимаций одновременно, параллельно.

Вторая функция, sequence(), позволяет запускать те же анимации одну за другой. Внутри sequence() шаги анимации состоят из вызовов функций style() или animate().

  • Используйте style(), чтобы сразу применить предоставленные данные стилей.
  • Используйте animate(), чтобы применить данные стилей за заданный интервал времени.

Пример анимации фильтра

Посмотрите ещё одну анимацию на странице примера. На вкладке Filter/Stagger введите текст в поле Search Heroes, например Magnet или tornado.

Фильтр работает в реальном времени по мере ввода. Элементы уходят со страницы при вводе каждой новой буквы, и фильтр становится всё строже. Список героев постепенно возвращается на страницу, когда вы удаляете каждую букву в поле фильтра.

HTML-шаблон содержит trigger с именем 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 в декораторе компонента содержит три transition.

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)
  • Фильтрует героев на основе значения search input

Для каждого изменения:

  • Скрывает элемент, уходящий из DOM, устанавливая opacity и width в 0

  • Анимирует элемент, входящий в DOM, за 300 миллисекунд. Во время анимации элемент принимает ширину и opacity по умолчанию.

  • Если несколько элементов входят в DOM или уходят из него, stagger'ит каждую анимацию, начиная сверху страницы, с задержкой 50 миллисекунд между элементами

Анимация элементов переупорядочиваемого списка

Хотя Angular корректно анимирует элементы списка *ngFor «из коробки», он не сможет сделать это, если изменится их порядок. Это потому, что он потеряет отслеживание, какой элемент какой, что приведёт к сломанным анимациям. Единственный способ помочь Angular отслеживать такие элементы — назначить TrackByFunction директиве NgForOf. Это гарантирует, что Angular всегда знает, какой элемент какой, и может применять правильные анимации к правильным элементам.

ВАЖНО: Если нужно анимировать элементы списка *ngFor и есть вероятность, что порядок таких элементов изменится во время выполнения, всегда используйте TrackByFunction.

Анимации и View Encapsulation компонента

Анимации Angular основаны на DOM-структуре компонентов и напрямую не учитывают View Encapsulation; это значит, что компоненты с ViewEncapsulation.Emulated ведут себя так же, как с ViewEncapsulation.None (ViewEncapsulation.ShadowDom и ViewEncapsulation.ExperimentalIsolatedShadowDom ведут себя иначе — обсудим ниже).

Например, если функцию query() (которую вы увидите чаще в остальной части руководства по Animations) применить наверху дерева компонентов с emulated view encapsulation, такой query сможет идентифицировать (и анимировать) DOM-элементы на любой глубине дерева.

С другой стороны, ViewEncapsulation.ShadowDom и ViewEncapsulation.ExperimentalIsolatedShadowDom меняют DOM-структуру компонента, «скрывая» DOM-элементы внутри элементов ShadowRoot. Такие манипуляции DOM мешают части реализации анимаций работать корректно, так как она опирается на простые DOM-структуры и не учитывает элементы ShadowRoot. Поэтому рекомендуется избегать применения анимаций к views, включающим компоненты с ShadowDom view encapsulation.

Краткое резюме последовательностей анимаций

Функции Angular для анимации нескольких элементов начинаются с query() для поиска внутренних элементов; например, сбора всех изображений внутри <div>. Остальные функции — stagger(), group() и sequence() — применяют каскады или позволяют контролировать, как применяются несколько шагов анимации.

Дополнительно об анимациях Angular

Вас также могут заинтересовать: