# 复杂动画序列

# 前提条件

对下列概念有基本的理解:

  • Angular 动画简介
  • 转场与触发器

到目前为止,我们已经学过了单个 HTML 元素的简单动画。Angular 还允许你在进入和离开页面时播放 "动画协调序列",比如当整个网格或元素列表进入或离开页面时,多个条目的动画之间需要彼此协调时间。你可以选择并行执行多个动画,或者按顺序逐个运行离散动画。

用来控制复杂动画序列的函数如下:

函数 Functions 详情 Details
query() 用于查找一个或多个内部 HTML 元素。Finds one or more inner HTML elements.
stagger() 用于为多元素动画应用级联延迟。Applies a cascading delay to animations for multiple elements.
group() 用于并行执行多个动画步骤。Runs multiple animation steps in parallel.
sequence() 用于逐个顺序执行多个动画步骤。Runs animation steps one after another.

# query() 函数

大多数复杂动画都依赖 query()函数来查找子元素并对其应用动画,基本的例子是:

例子 Examples 详情 Details
query() 后跟 animate()query() followed by animate() 用于查询简单的 HTML 元素并直接对它们应用动画。Used to query simple HTML elements and directly apply animations to them.
query() 后跟 animateChild()query() followed by animateChild() 用于查询子元素,这些元素本身就应用了动画元数据并触发这样的动画(否则将被当前/父元素的动画阻止)。Used to query child elements, which themselves have animations metadata applied to them and trigger such animation (which would be otherwise be blocked by the current/parent element's animation).

query()的第一个参数是一个 css 选择器 字符串,它还可以包含以下 Angular 特定的标记:

标记 Tokens 详情 Details
:enter :leave:enter :leave 用于进入/离开元素。For entering/leaving elements.
:animating 对于当前正在播放动画的元素。For elements currently animating.
@* @triggerName 对于具有任何(或特定)触发器的元素。For elements with any—or a specific—trigger.
:self 动画元素本身。The animating element itself.

进入和离开元素Entering and Leaving Elements并非所有子元素都会实际上被认为是进入/离开;有时,这可能是违反直觉和令人困惑的。有关更多信息,参阅 query api 的文档

你还可以在 Querying 选项卡下的动画实时示例(在动画介绍部分介绍)中看到这方面的插图。

# 使用 query()stagger()(交错)函数执行多元素动画

通过 query()查询子元素后,stagger()函数允许你定义每个查询的动画项之间的时间间隙,从而为元素之间延迟设置动画。

下面的例子演示了如何使用 query()stagger()函数对依次添加的英雄列表从上到下播放动画(有少许延迟)。

  • query()查阅正在进入或离开页面的任意元素。该查询会找出那些符合某种特定标准的元素

  • 对每个元素,使用 style()为其设置初始样式。使其变得透明,并使用 transform将其移出位置,以便它能滑入后就位。

  • 使用 stagger()来在每个动画之间延迟 30 毫秒

  • 对屏幕上的每个元素,根据一条自定义缓动曲线播放 0.5 秒的动画,同时将其淡入,而且逐步取消以前的位移效果

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

animations: [
  trigger('pageAnimations', [
    transition(':enter', [
      query('.hero', [
        style({opacity: 0, transform: 'translateY(-100px)'}),
        stagger(30, [
          animate('500ms cubic-bezier(0.35, 0, 0.25, 1)',
          style({ opacity: 1, transform: 'none' }))
        ])
      ])
    ])
  ]),

# 使用 group()函数播放并行动画

你已经了解了如何在两个连续的动画之间添加延迟。不过你可能还想配置一些并行的动画。比如,你可能希望为同一个元素的两个 CSS 属性设置动画,但要为每个属性使用不同的 easing函数。这时,你可以使用动画函数 group()

注意:group()函数用于对动画步骤进行分组,而不是针对动画元素。

在下面的例子中,对 :enter:leave使用分组,可以配置两种不同的时序。它们会同时作用于同一个元素,但彼此独立运行。

src/app/hero-list-groups.component.ts (excerpt)

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

# 顺序动画与平行动画

复杂动画中可以同时发生很多事情。但是当你要创建一个需要让几个子动画逐个执行的动画时,该怎么办呢?以前我们使用 group() 来同时并行运行多个动画。

第二个名叫 sequence()的函数会让你一个接一个地运行这些动画。在 sequence()中,这些动画步骤由 style()animate()的函数调用组成。

  • style()用来立即应用所指定的样式数据。

  • animate()用来在一定的时间间隔内应用样式数据。

# 过滤器动画范例

来看看范例应用中的另一个动画。在 Filter/Stagger 页,往 Search Heroes文本框中输入一些文本,比如 Magnettornado

过滤器会在你输入时实时工作。每当你键入一个新字母时,就会有一些元素离开页面,并且过滤条件也会逐渐变得更加严格。相反,当你删除过滤器中的每个字母时,英雄列表也会逐渐重新进入页面中。

HTML 模板中包含一个名叫 filterAnimation的触发器。

src/app/hero-list-page.component.html

<label for="search">Search heroes: </label>
<input type="text" id="search" #criteria
       (input)="updateCriteria(criteria.value)"
       placeholder="Search heroes">

<ul class="heroes" [@filterAnimation]="heroesTotal">
  <li *ngFor="let hero of heroes" class="hero">
    <div class="inner">
      <span class="badge">{{ hero.id }}</span>
      <span class="name">{{ hero.name }}</span>
    </div>
  </li>
</ul>

该组件装饰器中的 filterAnimation包含三个转场。

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

@Component({
  animations: [
    trigger('filterAnimation', [
      transition(':enter, * => 0, * => -1', []),
      transition(':increment', [
        query(':enter', [
          style({ opacity: 0, width: 0 }),
          stagger(50, [
            animate('300ms ease-out', style({ opacity: 1, width: '*' })),
          ]),
        ], { optional: true })
      ]),
      transition(':decrement', [
        query(':leave', [
          stagger(50, [
            animate('300ms ease-out', style({ opacity: 0, width: 0 })),
          ]),
        ])
      ]),
    ]),
  ]
})
export class HeroListPageComponent implements OnInit {
  heroesTotal = -1;

  get heroes() { return this._heroes; }
  private _heroes: Hero[] = [];

  ngOnInit() {
    this._heroes = HEROES;
  }

  updateCriteria(criteria: string) {
    criteria = criteria ? criteria.trim() : '';

    this._heroes = HEROES.filter(hero => hero.name.toLowerCase().includes(criteria.toLowerCase()));
    const newTotal = this.heroes.length;

    if (this.heroesTotal !== newTotal) {
      this.heroesTotal = newTotal;
    } else if (!criteria) {
      this.heroesTotal = -1;
    }
  }
}

这个例子中的代码包含下列任务:

  • 当用户首次打开或导航到此页面时,跳过所有动画(该动画会压扁已经存在的内容,因此它只会作用于那些已经存在于 DOM 中的元素)

  • 根据搜索框中的值过滤英雄

对于每次匹配:

  • 通过将元素的不透明度和宽度设置为 0 来隐藏正在离开 DOM 的元素

  • 对正在进入 DOM 的元素,播放一个 300 毫秒的动画。在动画期间,该元素采用其默认宽度和不透明度。

  • 如果有多个匹配的元素正在进入或离开 DOM,则从页面顶部的元素开始对每个元素进行交错(stagger),每个元素之间的延迟为 50 毫秒

# 在重新排序列表的条目时设置动画

尽管 Angular 开箱即用的支持 *ngFor列表项动画,但如果只是它们的顺序变化了,就无法支持。因为 Angular 会忘记哪个元素是哪个元素,从而导致这些动画被破坏。帮助 Angular 跟踪此类元素的唯一方法是将 TrackByFunction分配给 NgForOf指令。这可确保 Angular 始终知道哪个元素是哪个,从而允许它始终将正确的动画应用于正确的元素。

重要:如果你需要为 *ngFor列表的条目设置动画,并且此类条目的顺序有可能在运行时更改,请始终使用 TrackByFunction

# 动画和组件视图封装

Angular 动画基于组件的 DOM 结构,不会直接考虑视图封装,这意味着使用 ViewEncapsulation.Emulated的组件的行为方式与使用 ViewEncapsulation.NoneViewEncapsulation.ShadowDom行为方式不同,我们将很快讨论) .

例如,如果要在使用模拟(emulated)视图封装的组件树的顶级组件中应用 query()函数(你还会在动画指南的其余部分看到更多此类函数),则这样的查询将能够识别(并播放动画)此树的任何深度上的 DOM 元素。

另一方面, ViewEncapsulation.ShadowDom会通过在 ShadowRoot 元素中“隐藏” DOM 元素来更改组件的 DOM 结构。此类 DOM 操作就会阻碍某些动画实现的正常工作,因为它只能工作在简单的 DOM 结构上,并没有考虑 ShadowRoot元素。因此,建议避免使用 ShadowDom 视图封装将动画应用到包含组件的视图。

# 动画序列总结

Angular 中这些用于多元素动画的函数,都要从 query()开始,查找出内部元素,比如找出某个 &lt;div&gt;中的所有图片。其余函数 stagger()、group()和 sequence()会以级联方式或你的自定义逻辑来控制要如何应用多个动画步骤。

# 关于 Angular 动画的更多知识

你可能还对下列内容感兴趣:

  • Angular 动画简介

  • 转场与触发器

  • 可复用动画

  • 路由转场动画

Last Updated: 5/13/2023, 10:57:08 AM