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

Анимация. Часть 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())

Ссылки