Перейти к содержанию

Анимация. Часть 2

Переиспользование анимации

В Angular анимации можно определить стили один раз и использовать их в нескольких компонентах при создании для них анимированных смен состояний (подобно переиспользованию компонентов, директив и др.).

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

export const reusableAnimation = animation([
  style({
    backgroundColor: '{{ backgroundColor }}',
    fontSize: '{{ fontSize }}',
    width: '{{ width }}'
  }),
  animate('{{ time }}')
])

В двойных фигурных скобках описаны параметры, передаваемые анимации при ее вызове через useAnimation(). Также допустимо использование константных значений.

Пример такой Angular анимации.

    @Component({
       selector: 'reusable-animation',
       templateUrl: './reusable-animation.component.html',
       animations: [
           trigger('reusableAnimation', [
               transition('initial => expanded', useAnimation(reusableAnimation, {
                  params: {
                   backgroundColor: '#fff',
                   fontSize: '16px',
                   time: '0.3s',
                   width: '100%'
                }
               }))
           ])
       ]
    })
    ...

Функция useAnimation() принимает два параметра: первый - анимация, определенная для переиспользования, второй - объект, в свойстве params которого указываются значения параметров.

Сложная анимация

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

Реализуется подобное с помощью следующих функций:

  • query() - находит один и более дочерних HTML-элементов по заданному критерию в пределах элемента, к которому применяется анимация, и применяет ее к каждому из них;
  • stagger() - устанавливает задержку для найденных функцией query() элементов;
  • group() - запускает все составляющие анимации параллельно;
  • sequence() - запускает все составляющие последовательно.

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

animations: [trigger('appearingItems', [transition(':enter', [query('ul.users li', [style({ opacity: 0, transform: 'translateY(-100px)' }), stagger(-50, [animate('300ms', style({ opacity: 1, transform: 'none' }))])])])])]

Здесь Angular анимация appearingItems определяется для появляющихся элементов списка (состояние :enter) с классом стилей .users.

Первым параметром query() передается селектор (критерий поиска элементов), а вторым - массив с описанием анимации, где первое использование функции style() задает исходные стили для элементов, попадающих под критерий поиска.

Теперь перейдем к примеру с использованием group().

animations: [trigger('groupAnimation', [transition(':enter', [style({ transform: 'translateX(-100px)', opacity: 0 }), group([animate('0.3s ease', style({ transform: 'translateX(0)' })), animate('0.2s 0.15 ease', style({ opacity: 1 }))])])])]

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

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

Для выполнения этой же Angular анимации последовательно без использования задержки, используется функция sequence().

animations: [trigger('sequenceAnimation', [transition(':enter', [style({ transform: 'translateX(-100px)', opacity: 0 }), sequence([animate('0.3s ease', style({ transform: 'translateX(0)' })), animate('0.2s 0.15 ease', style({ opacity: 1 }))])])])]

Анимированная смена маршрутов

Анимация маршрутов требует понимания работы модуля маршрутизации Angular.

Фактически переход с одного URL приложения на другой - это просто смена представлений (change views). Используя анимацию Angular можно сделать смену маршрутов анимированной.

app-routing.module.ts

    const routes: Routes = [
     {
       {path: 'page1', component: Page1Component, data: {animation: 'page1'}},
       {path: 'page2', component: Page2Component, data: {animation: 'page2'}}
     }
    ];

    @NgModule({
     imports: [RouterModule.forRoot(routes)],
     exports: [RouterModule]
    })

    export class AppRoutingModule {}

app.component.html

<div [@routeChangeAnimation]="getRouteAnimationState(outlet)">
  <router-outlet #outlet="outlet"></router-outlet>
</div>

app.component.ts

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  animations: [routeChangeAnimation]
})
export class AppComponent {
  constructor() {}

  getRouteAnimationState(outlet: RouterOutlet) {
    return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation']
  }
}

change-route-animation.ts

export const routeChangeAnimation = trigger('routeChangeAnimation', [
  transition('page1 <=> page2', [
    style({ position: 'relative' }),
    query(':enter, :leave', [
      style({
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%'
      })
    ]),
    query(':enter', [style({ left: '-100%' })]),
    query(':leave', animateChild()),
    group([query(':leave', [animate('300ms ease-out', style({ left: '100%' }))]), query(':enter', [animate('300ms ease-out', style({ left: '0%' }))])]),
    query(':enter', animateChild())
  ])
])

Теперь по порядку. Для определения анимированной смены представления при смене URL используется свойство animation (название может быть другим), указанное в свойстве маршрута data. В качестве значения свойству animation задается имя состояния анимации.

В шаблоне компонента (app.component.html), в котором будет происходить загрузка представления по запрашиваемому URL, элемент <router-outlet> является дочерним по отношению к элементу <div>, для которого определяется routeChangeAnimation. Для определения имени состояния используется метод getRouteAnimationState(), извлекающий значение свойства animation, заданное для текущего маршрута.

Определение самой Angular анимации ограничивается в данном случае описанием смены между собой пары состояний page1 и page2.

Поскольку в момент смены состояний новое представление вставляется сразу же после предыдущего, элементу, для которого определен триггер, задается относительное позиционирование, а дочерним по отношению к нему элементам - абсолютное. Это нужно для избежания одновременного появления на странице двух представлений.

style({ position: 'relative' }),
  query(':enter, :leave', [
    style({
      position: 'absolute',
      top: 0,
      left: 0,
      width: '100%'
    })
  ])

Здесь query() используется для задания исходных стилей выборке элементов.

Далее представление маршрута, на который осуществляется переход, скрывается сдвигом влево.

query(':enter', [
    style({ left: '-100%'})
]),

А в представлении, с которого происходит переход, инициируется с помощью функции animateChild() вызов его дочерних анимаций.

    query(':leave', animateChild()),

Для понимания, в коде ниже анимация childAnimation является дочерней по отношению к анимации parentAnimation:

<div [@parentAnimation]="getState()">
  <div [@childAnimation]="getChildState()"></div>
</div>

Таким образом, если функция childAnimate() вызывается в parentAnimation, то будет запущена анимация childAnimation.

После функция group() запускает одновременно анимированную смену представлений. Старое представление сдвигается за пределы окна вправо, а новое, которое было изначально спрятано слева, появляется.

group([query(':leave', [animate('300ms ease-out', style({ left: '100%' }))]), query(':enter', [animate('300ms ease-out', style({ left: '0%' }))])])

И в конце инициируется запуск дочерних Angular анимаций нового представления.

query(':enter', animateChild())