动画

动画是现代Web应用设计中一个很重要的方面。我们希望用户界面能在不同的状态之间更平滑的转场。如果需要,还可以用适当的动画来吸引注意力。 设计良好的动画不但会让UI更有趣,还会让它更容易使用。

Angular的动画系统赋予了制作各种动画效果的能力,以构建出与原生CSS动画性能相同的动画。 我们也获得了额外的让动画逻辑与其它应用代码紧紧集成在一起的能力,这让动画可以被更容易的触发与控制。

Angular动画是基于标准的Web动画API(Web Animations API)构建的,它们在支持此API的浏览器中会用原生方式工作。

至于其它浏览器,就需要一个填充库(polyfill)了。你可以从这里获取web-animations.min.js,并把它加入你的页面中。

目录

Contents

本章中引用的这个例子可以到在线例子 / 可下载的例子去体验。

快速起步范例:在两个状态间转场

A simple transition animation

我们来构建一个简单的动画,它会让一个元素用模型驱动的方式在两个状态之间转场。

动画会被定义在@Component元数据中。在添加动画之前,先引入一些与动画有关的函数:

app.module.ts (@NgModule imports excerpt)

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

hero-list-basic.component.ts

import {
  Component,
  Input
} from '@angular/core';
import {
  trigger,
  state,
  style,
  animate,
  transition
} from '@angular/animations';

通过这些,可以在组件元数据中定义一个名叫heroState动画触发器。它在两个状态activeinactive之间进行转场。 当英雄处于激活状态时,它会把该元素显示得稍微大一点、亮一点。

hero-list-basic.component.ts (@Component excerpt)

  animations: [
    trigger('heroState', [
      state('inactive', style({
        backgroundColor: '#eee',
        transform: 'scale(1)'
      })),
      state('active',   style({
        backgroundColor: '#cfd8dc',
        transform: 'scale(1.1)'
      })),
      transition('inactive => active', animate('100ms ease-in')),
      transition('active => inactive', animate('100ms ease-out'))
    ])
  ]

在这个例子中,我们在元数据中用内联的方式定义了动画样式(colortransform)。在即将到来的一个Angular版本中,还将支持从组件的CSS样式表中提取样式。

我们刚刚定义了一个动画,但它还没有被用到任何地方。要想使用它,可以在模板中用[@triggerName]语法来把它附加到一个或多个元素上。

hero-list-basic.component.ts (excerpt)

template: `
  <ul>
    <li *ngFor="let hero of heroes"
        [@heroState]="hero.state"
        (click)="hero.toggleState()">
      {{hero.name}}
    </li>
  </ul>
`,

这里,我们把该动画触发器添加到了由ngFor重复出来的每一个元素上。每个重复出来的元素都有独立的动画效果。 然后把@triggerName属性(Attribute)的值设置成表达式hero.state。这个值应该或者是inactive或者是active,因为我们刚刚为它们俩定义过动画状态。

通过这些设置,一旦英雄对象的状态发生了变化,就会触发一个转场动画。下面是完整的组件实现:

hero-list-basic.component.ts

  1. import {
  2. Component,
  3. Input
  4. } from '@angular/core';
  5. import {
  6. trigger,
  7. state,
  8. style,
  9. animate,
  10. transition
  11. } from '@angular/animations';
  12. import { Heroes } from './hero.service';
  13. @Component({
  14. selector: 'hero-list-basic',
  15. template: `
  16. <ul>
  17. <li *ngFor="let hero of heroes"
  18. [@heroState]="hero.state"
  19. (click)="hero.toggleState()">
  20. {{hero.name}}
  21. </li>
  22. </ul>
  23. `,
  24. styleUrls: ['./hero-list.component.css'],
  25. animations: [
  26. trigger('heroState', [
  27. state('inactive', style({
  28. backgroundColor: '#eee',
  29. transform: 'scale(1)'
  30. })),
  31. state('active', style({
  32. backgroundColor: '#cfd8dc',
  33. transform: 'scale(1.1)'
  34. })),
  35. transition('inactive => active', animate('100ms ease-in')),
  36. transition('active => inactive', animate('100ms ease-out'))
  37. ])
  38. ]
  39. })
  40. export class HeroListBasicComponent {
  41. @Input() heroes: Heroes;
  42. }

状态与转场

Angular动画是由状态状态之间的转场效果所定义的。

动画状态是一个由程序代码中定义的字符串值。在上面的例子中,基于英雄对象的逻辑状态,我们使用了'active''inactive'这两种状态。 状态的来源可以是像本例中这样简单的对象属性,也可以是由方法计算出来的值。重点是,我们得能从组件模板中读取它。

我们可以为每个动画状态定义了一组样式

state('inactive', style({
  backgroundColor: '#eee',
  transform: 'scale(1)'
})),
state('active',   style({
  backgroundColor: '#cfd8dc',
  transform: 'scale(1.1)'
})),

这些state具体定义了每个状态的最终样式。一旦元素转场到那个状态,该样式就会被应用到此元素上,当它留在此状态时,这些样式也会一直保持着。 从这个意义上讲,这里其实并不只是在定义动画,而是在定义该元素在不同状态时应该具有的样式。

定义完状态,就能定义在状态之间的各种转场了。每个转场都会控制一条在一组样式和下一组样式之间切换的时间线:

transition('inactive => active', animate('100ms ease-in')),
transition('active => inactive', animate('100ms ease-out'))
In Angular animations you define states and transitions between states

如果多个转场都有同样的时间线配置,就可以把它们合并进同一个transition定义中:

transition('inactive => active, active => inactive',
 animate('100ms ease-out'))

如果要对同一个转场的两个方向都使用相同的时间线(就像前面的例子中那样),就可以使用<=>这种简写语法:

transition('inactive <=> active', animate('100ms ease-out'))

有时希望一些样式只在动画期间生效,但在结束后并不保留它们。这时可以把这些样式内联在transition中进行定义。 在这个例子中,该元素会立刻获得一组样式,然后动态转场到下一个状态。当转场结束时,这些样式并不会被保留,因为它们并没有被定义在state中。

transition('inactive => active', [
  style({
    backgroundColor: '#cfd8dc',
    transform: 'scale(1.3)'
  }),
  animate('80ms ease-in', style({
    backgroundColor: '#eee',
    transform: 'scale(1)'
  }))
]),

*(通配符)状态

*(通配符)状态匹配任何动画状态。当定义那些不需要管当前处于什么状态的样式及转场时,这很有用。比如:

The wildcard state can be used to match many different transitions at once

void状态

有一种叫做void的特殊状态,它可以应用在任何动画中。它表示元素没有被附加到视图。这种情况可能是由于它尚未被添加进来或者已经被移除了。 void状态在定义“进场”和“离场”的动画时会非常有用。

比如当一个元素离开视图时,* => void转场就会生效,而不管它在离场以前是什么状态。

The void state can be used for enter and leave transitions

*通配符状态也能匹配void

例子:进场与离场

Enter and leave animations

使用void*状态,可以定义元素进场与离场时的转场动画:

例如,在下面的animations数组中,这两个转场语句使用void => ** => void语法来让该元素以动画形式进入和离开当前视图。

hero-list-enter-leave.component.ts (excerpt)

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%)'}))
    ])
  ])
]

注意,在这个例子中,这些样式在转场定义中被直接应用到了void状态,但并没有一个单独的state(void)定义。 这么做是因为希望在进场与离场时使用不一样的转换效果:元素从左侧进场,从右侧离开。

这两个常见的动画有自己的别名:

transition(':enter', [ ... ]); // void => *
transition(':leave', [ ... ]); // * => void

范例:从不同的状态下进场和离场

Enter and leave animations combined with state animations

通过把英雄的状态用作动画的状态,还能把该动画跟以前的转场动画组合成一个复合动画。这让我们能根据该英雄的当前状态为其配置不同的进场与离场动画:

现在就对每一种转场都有了细粒度的控制:

This example transitions between active, inactive, and void states

hero-list-enter-leave.component.ts (excerpt)

animations: [
  trigger('heroState', [
    state('inactive', style({transform: 'translateX(0) scale(1)'})),
    state('active',   style({transform: 'translateX(0) scale(1.1)'})),
    transition('inactive => active', animate('100ms ease-in')),
    transition('active => inactive', animate('100ms ease-out')),
    transition('void => inactive', [
      style({transform: 'translateX(-100%) scale(1)'}),
      animate(100)
    ]),
    transition('inactive => void', [
      animate(100, style({transform: 'translateX(100%) scale(1)'}))
    ]),
    transition('void => active', [
      style({transform: 'translateX(0) scale(0)'}),
      animate(200)
    ]),
    transition('active => void', [
      animate(200, style({transform: 'translateX(0) scale(0)'}))
    ])
  ])
]

可动的(Animatable)属性与单位

由于Angular的动画支持是基于Web Animations标准的,所以也能支持浏览器认为可以参与动画的任何属性。这些属性包括位置(position)、大小(size)、变换(transform)、颜色(color)、边框(border)等很多属性。W3C维护着 一个“可动”属性列表

尺寸类属性(如位置、大小、边框等)包括一个数字值和一个用来定义长度单位的后缀:

对大多数尺寸类属性而言,还能只定义一个数字,那就表示它使用的是像素(px)数:

自动属性值计算

Animation with automated height calculation

有时候,我们想在动画中使用的尺寸类样式,它的值在开始运行之前都是不可知的。比如,元素的宽度和高度往往依赖于它们的内容和屏幕的尺寸。处理这些属性对CSS动画而言通常是相当棘手的。

如果用Angular动画,就可以用一个特殊的*属性值来处理这种情况。该属性的值将会在运行期被计算出来,然后插入到这个动画中。

这个例子中的“离场”动画会取得该元素在离场前的高度,并且把它从这个高度用动画转场到0高度:

animations: [
  trigger('shrinkOut', [
    state('in', style({height: '*'})),
    transition('* => void', [
      style({height: '*'}),
      animate(250, style({height: 0}))
    ])
  ])
]

动画时间线

对每一个动画转场效果,有三种时间线属性可以调整:持续时间(duration)、延迟(delay)和缓动(easing)函数。它们被合并到了一个单独的转场时间线字符串

持续时间

持续时间控制动画从开始到结束要花多长时间。可以用三种方式定义持续时间:

延迟

延迟控制的是在动画已经触发但尚未真正开始转场之前要等待多久。可以把它添加到字符串中的持续时间后面,它的选项格式也跟持续时间是一样的:

缓动函数

缓动函数用于控制动画在运行期间如何加速和减速。比如:使用ease-in函数意味着动画开始时相对缓慢,然后在进行中逐步加速。可以通过在这个字符串中的持续时间和延迟后面添加第三个值来控制使用哪个缓动函数(如果没有定义延迟就作为第二个值)。

Animations with specific timings

例子

这里是两个自定义时间线的动态演示。“进场”和“离场”都持续200毫秒,也就是0.2s,但它们有不同的缓动函数。“离场”动画会在100毫秒的延迟之后开始,也就是'0.2s 10 ease-out'

hero-list-timings.component.ts (excerpt)

animations: [
  trigger('flyInOut', [
    state('in', style({opacity: 1, transform: 'translateX(0)'})),
    transition('void => *', [
      style({
        opacity: 0,
        transform: 'translateX(-100%)'
      }),
      animate('0.2s ease-in')
    ]),
    transition('* => void', [
      animate('0.2s 10 ease-out', style({
        opacity: 0,
        transform: 'translateX(100%)'
      }))
    ])
  ])
]

基于关键帧(Keyframes)的多阶段动画

Animations with some bounce implemented with keyframes

通过定义动画的关键帧,可以把两组样式之间的简单转场,升级成一种更复杂的动画,它会在转场期间经历一个或多个中间样式。

每个关键帧都可以被指定一个偏移量,用来定义该关键帧将被用在动画期间的哪个时间点。偏移量是一个介于0(表示动画起点)和1(表示动画终点)之间的数组。

在这个例子中,我们使用关键帧来为进场和离场动画添加一些“反弹效果”:

hero-list-multistep.component.ts (excerpt)

animations: [
  trigger('flyInOut', [
    state('in', style({transform: 'translateX(0)'})),
    transition('void => *', [
      animate(300, keyframes([
        style({opacity: 0, transform: 'translateX(-100%)', offset: 0}),
        style({opacity: 1, transform: 'translateX(15px)',  offset: 0.3}),
        style({opacity: 1, transform: 'translateX(0)',     offset: 1.0})
      ]))
    ]),
    transition('* => void', [
      animate(300, keyframes([
        style({opacity: 1, transform: 'translateX(0)',     offset: 0}),
        style({opacity: 1, transform: 'translateX(-15px)', offset: 0.7}),
        style({opacity: 0, transform: 'translateX(100%)',  offset: 1.0})
      ]))
    ])
  ])
]

注意,这个偏移量并不是用绝对数字定义的时间段,而是在0到1之间的相对值(百分比)。动画的最终时间线会基于关键帧的偏移量、持续时间、延迟和缓动函数计算出来。

为关键帧定义偏移量是可选的。如果省略它们,偏移量会自动根据帧数平均分布出来。例如,三个未定义过偏移量的关键帧会分别获得偏移量:00.51

并行动画组(Group)

Parallel animations with different timings, implemented with groups

我们已经知道该如何在同一时间段进行多个样式的动画了:只要把它们都放进同一个style()定义中就行了!

但我们也可能会希望为同时发生的几个动画配置不同的时间线。比如,同时对两个CSS属性做动画,但又得为它们定义不同的缓动函数。

这种情况下就可以用动画来解决了。在这个例子中,我们同时在进场和离场时使用了组,以便能让它们使用两种不同的时间线配置。 它们被同时应用到同一个元素上,但又彼此独立运行:

hero-list-groups.component.ts (excerpt)

animations: [
  trigger('flyInOut', [
    state('in', style({width: 120, transform: 'translateX(0)', opacity: 1})),
    transition('void => *', [
      style({width: 10, transform: 'translateX(50px)', opacity: 0}),
      group([
        animate('0.3s 0.1s ease', style({
          transform: 'translateX(0)',
          width: 120
        })),
        animate('0.3s ease', style({
          opacity: 1
        }))
      ])
    ]),
    transition('* => void', [
      group([
        animate('0.3s ease', style({
          transform: 'translateX(50px)',
          width: 10
        })),
        animate('0.3s 0.2s ease', style({
          opacity: 0
        }))
      ])
    ])
  ])
]

其中一个动画组对元素的transformwidth做动画,另一个组则对opacity做动画。

动画回调

当动画开始和结束时,会触发一个回调。

对于例子中的这个关键帧,我们有一个叫做@flyInOuttrigger。在那里我们可以挂钩到那些回调,比如:

hero-list-multistep.component.ts (excerpt)

template: `
  <ul>
    <li *ngFor="let hero of heroes"
        (@flyInOut.start)="animationStarted($event)"
        (@flyInOut.done)="animationDone($event)"
        [@flyInOut]="'in'">
      {{hero.name}}
    </li>
  </ul>
`,

这些回调接收一个AnimationTransitionEvent参数,它包含一些有用的属性,例如fromStatetoStatetotalTime

无论动画是否实际执行过,那些回调都会触发。