在线文档教程

动画

动画

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

概览

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

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

对于 Angular 6,如果浏览器没有提供对 Web 动画 API 的原生支持,Angular 就会自动改用 CSS 的关键帧动画作为后备实现。这意味,除非要在代码中使用 AnimationBuilder ,否则不必使用相关的腻子脚本。 如果你要在代码中使用 AnimationBuilder ,就要从 Angular CLI 自动生成的 polyfills.ts 文件中反注释掉 web-animations-js 腻子脚本。

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

准备工作

在往应用中添加动画之前,你要首先在应用的根模块中引入一些与动画有关的模块和函数。

app.module.ts (animation module import excerpt)

content_copyimport { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule{ imports: [ BrowserModule, BrowserAnimationsModule ], // ... more stuff ... }) export class AppModule { }

基本例子

这里的动画例子用来给英雄列表添加动画。

Hero 类有一个 name 属性、一个 state 属性(用于表明该英雄是否为激活状态)和一个 toggleState() 函数,用来在这两种状态之间切换。

hero.service.ts (Hero class)

content_copyexport class Hero { constructor(public name: string, public state = 'inactive') { } toggleState() { this.state = this.state === 'active' ? 'inactive' : 'active'; } }

在屏幕的顶部(app.hero-team-builder.component.ts)是一系列按钮,用于从列表中添加和删除英雄(通过 HeroService)。 这些按钮会引起列表的变化,同时可以看到列表中的所有范例组件。

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

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

动画会被定义在 @Component 元数据中。

hero-list-basic.component.ts

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

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

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

content_copyanimations: [ 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)

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

这里,动画触发器被添加到了由 ngFor 重复出来的每一个元素上。每个重复出来的元素都有独立的动画效果。 然后把 @triggerName 属性(Attribute)的值设置成表达式 hero.state。这个值应该是 inactiveactive 之一。

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

hero-list-basic.component.ts

content_copyimport { Component, Input} from '@angular/core';import { trigger, state, style, animate, transition} from '@angular/animations'; import { Hero } from './hero.service'; @Component{ selector: 'app-hero-list-basic', template: ` <ul> <li *ngFor="let hero of heroes" [@heroState]="hero.state" (click)="hero.toggleState()"> {{hero.name}} </li> </ul> `, styleUrls: ['./hero-list.component.css'], 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')) ]) ]})export class HeroListBasicComponent { @Input() heroes: Hero[];}

状态与转场

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

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

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

src/app/hero-list-basic.component.ts

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

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

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

src/app/hero-list-basic.component.ts

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

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

src/app/hero-list-combined-transitions.component.ts

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

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

src/app/hero-list-twoway.component.ts

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

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

src/app/hero-list-inline-styles.component.ts

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

*(通配符)状态

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

  • 当该元素的状态从 active 变成任何其它状态时,active => * 转场都会生效。

void 状态

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

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

* 通配符状态也能匹配 void

例子:进场与离场

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

  • 进场:void => *

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

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

content_copyanimations: [ 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) 定义。 这么做是因为希望在进场与离场时使用不一样的转换效果:元素从左侧进场,从右侧离开。

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

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

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

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

  • 非激活英雄进场:void => inactive

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

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

content_copyanimations: [ 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 维护着 一个“可动”属性列表。

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

  • '50px'

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

  • 50 相当于 '50px'

自动属性值计算

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

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

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

src/app/hero-list-auto.component.ts

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

动画时间线

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

持续时间

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

  • 作为一个普通数字,以毫秒为单位,如:100

延迟

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

  • 等待 100 毫秒,然后运行 200 毫秒:'0.2s 100ms'

缓动函数

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

  • 等待 100 毫秒,然后运行 200 毫秒,并且带缓动:'0.2s 100ms ease-out'

例子

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

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

content_copyanimations: [ 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 0.1s ease-out', style{ opacity: 0, transform: 'translateX(100%)' })) ]) ]) ]

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

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

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

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

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

content_copyanimations: [ 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)

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

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

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

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

content_copyanimations: [ 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)

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

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

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