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

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

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

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

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

1
2
3
4
5
6
7
8
export const reusableAnimation = animation([
    style({
        backgroundColor: '{{ backgroundColor }}',
        fontSize: '{{ fontSize }}',
        width: '{{ width }}',
    }),
    animate('{{ time }}'),
]);

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@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().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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

1
2
3
4
5
<div
    [@routeChangeAnimation]="getRouteAnimationState(outlet)"
>
    <router-outlet #outlet="outlet"></router-outlet>
</div>

app.component.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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.

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

1
2
3
4
5
6
7
8
9
style({ position: 'relative' }),
    query(':enter, :leave', [
        style({
            position: 'absolute',
            top: 0,
            left: 0,
            width: '100%',
        }),
    ]);

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

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

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
group([
    query(':leave', [
        animate('300ms ease-out', style({ left: '100%' })),
    ]),
    query(':enter', [
        animate('300ms ease-out', style({ left: '0%' })),
    ]),
]);

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

1
query(':enter', animateChild());

Ссылки

Комментарии