关于 Angular 动画你需要知道的一切 1. 创建主组件 📦 2. 实现列表组件(暂时没有动画) 3. 动画 🧙‍♂️ 结论

2025-06-10

你需要了解的关于 Angular 动画的一切

1. 创建主要组件

2.实现列表组件(暂时没有动画)

3. 动画

结论

动画是一种设计工具,我们可以用它来赋予抽象的数字创作以实体感和可触性,使其更加熟悉和友好。许多组件库充分利用了动画,使其组件更贴近用户。好消息是,作为 Angular 开发者,我们几乎可以开箱即用地拥有出色的组件。坏消息是,我们几乎完全依赖这些库来实现动画。以至于 Angular 动画 API 成为最被低估的 API 之一,甚至可以说是被低估的。

为了改变这种现状,我会尽力在这篇文章中向你展示所有你需要了解的 Angular 动画入门知识。为此,我们将一起构建这个很棒的组件

演示

正如他们所说,熟能生巧,所以如果您愿意,您可以通过克隆这个存储库并开始操作(这篇文章的每个部分代表存储库的一个 git 分支)。

注意 ->如果您不想继续往下看,您可以直接跳到第 3 部分,在那里您将了解 Angular 动画 API 为您提供的超能力。

1. 创建主要组件

让我们开始吧。确保你的机器上安装了NodeNPMAngular CLI,创建一个新项目,并将其命名为“awesomeContacts”或任何你想起的巧妙名称,然后创建一个“list”组件:

ng new awesomeContacts --minimal=true --style=scss --routing=false --skip-install
cd awesomeContacts && yarn # or npm i
# wait for the install to finish, then create the 'list' component
ng g c components/list --spec=false && ng serve -o
Enter fullscreen mode Exit fullscreen mode

完成后,删除 AppComponent 模板中的所有内容并用我们的 ListComponent ( <app-list></app-list>) 替换。

在我们直接进入下一部分之前,还有最后一件事要做。为了使动画正常工作,我们需要BrowserAnimationModule在 AppModule 导入中导入。所以你应该得到类似这样的内容:

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
//...

@NgModule({
  declarations: [AppComponent, ListComponent],
  imports: [BrowserModule, BrowserAnimationsModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

2.实现列表组件(暂时没有动画)

现在我们的应用和列表组件已经启动并运行,让我们快速实现一下组件。
由于这篇文章主要讲动画,我们不会过多关注这个实现。

首先,我们创建一个 Contact 对象的接口,然后创建一组要使用的联系人:

interface Contact {
  id: number;
  name: string;
  email: string;
  avatarUrl: string;
}

const CONTACTS_MOCK: Contact[] = new Array(5)
  .fill({})
  .map(
    (c: Contact, i: number) =>
      ({
        id: i,
        name: `Contact ${i}`,
        email: `email${i}@provider.com`,
        avatarUrl: `https://api.adorable.io/avatars/100/${~~(Math.random() * 100)}`,
      } as Contact),
  )
  .reverse(); // * to have them sorted in DESC order

Enter fullscreen mode Exit fullscreen mode

ℹ️ adorable io是一个很棒的公共 API,提供占位符头像。ℹ️
~~两个按位非运算符)相当于 Math.floor :)

一旦完成,实现list.component.ts应该看起来像这样:

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.scss'],
})
export class ListComponent {
  contactList: Contact[] = CONTACTS_MOCK;
  selectedContact: Contact;

  onSelectItem(contact: Contact) {
    // * selecting a contact to focus on
    this.selectedContact = contact ? { ...contact } : null;
  }

  onAddItem() {
    const rndNum = Date.now();
    const newContact: Contact = {
      id: this.contactList.length * rndNum,
      name: `Contact ${this.contactList.length}`,
      email: `email${this.contactList.length}@provider.com`,
      avatarUrl: `https://api.adorable.io/avatars/285/${rndNum}`,
    };
    // * adding a new contact to the list
    this.contactList = [newContact, ...this.contactList];
    // * selecting the newly created contact
    this.onSelectItem(newContact);
  }

  onDeleteItem(contact: Contact) {
    // * removing a contact from the list
    this.contactList = this.contactList.filter(c => c.id != contact.id);
    if (this.selectedContact && this.selectedContact.id == contact.id) {
      // * if the removed contact is beaing focused on, then we remove the focus
      this.onSelectItem(null);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

HTML 文件:

<!-- * side list -->
<aside class="side-list">
  <!-- * side list toolbar -->
  <div class="side-list-toolbar">
    <span class="title">Contacts</span>
    <button class="btn" (click)="onAddItem()">Add</button>
  </div>
  <!-- * side list items-->
  <div class="side-list-items">
    <div
      class="side-list-item"
      *ngFor="let contact of contactList; trackBy: contact?.id"
      [ngClass]="{ 'side-list-item-selected': contact.id == selectedContact?.id }"
      (click)="onSelectItem(contact)"
    >
      <div class="side-list-item-avatar">
        <img [src]="contact.avatarUrl" loading="lazy" alt="profile picture" />
      </div>
      <div class="side-list-item-info">
        <p class="side-list-item-info-name">{{ contact.name }}</p>
        <p class="side-list-item-info-email">{{ contact.email }}</p>
      </div>
    </div>
  </div>
</aside>
<!-- * content wrapper -->
<section class="side-list-content">
  <!-- * content -->
  <div class="side-list-content-data" *ngIf="!!selectedContact">
    <div class="side-list-content-data-inner">
      <div class="side-list-content-data-overview">
        <div class="side-list-content-data-overview-info">
          <h3>{{ selectedContact.name }}</h3>
          <span>{{ selectedContact.email }}</span>
        </div>
        <div class="side-list-content-data-overview-avatar">
          <img [src]="selectedContact.avatarUrl" loading="lazy" alt="profile picture" />
        </div>
      </div>
      <div class="side-list-content-data-separator">
        <h4>Overview</h4>
        <div>
          <button class="btn danger outline" (click)="onDeleteItem(selectedContact)">
            Delete
          </button>
        </div>
      </div>
      <p>Lorem</p>
    </div>
  </div>
  <!-- * empty selection -->
  <div class="side-list-content-empty" *ngIf="!selectedContact">
    <div>
      <img alt="empty-selection" loading="lazy" src="https://img.icons8.com/ios/100/000000/nui2.png" />
      <p>Select an item from the list.</p>
    </div>
  </div>
</section>
Enter fullscreen mode Exit fullscreen mode

现在,让我们快速复制一些 CSS,使我们的组件看起来漂亮:

  • list.component.scss副本
  • styles.scss副本

太好了,我们已经准备好处理动画了!

3. 动画

列表元素的动画

首先,我们先来看一下列表元素的添加和隐藏动画。目前我们有这样的动画:

项目列表

我们想要的是这个(关注左侧的列表):

动画项目列表

在组件文件中,我们需要导入动画函数,并在@Component()装饰器中添加一个名为 animations: 的元数据属性:

import { animate, style, group, query, transition, trigger } from '@angular/animations';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.css'],
  animations: [
    // we will implement the animations here
  ]
})
Enter fullscreen mode Exit fullscreen mode

让我们看看这里将使用的动画 API:

功能 它起什么作用?
扳机() 启动动画并作为所有其他动画函数调用的容器。HTML 模板绑定到触发器名称 (triggerName)。使用第一个参数声明唯一的触发器名称。
过渡() 定义两个命名状态之间的动画序列。特殊值:enter,并:leave在进入和退出状态时启动过渡
风格() 定义一个或多个用于动画的 CSS 样式。控制动画期间 HTML 元素的视觉外观。
动画() 指定过渡的时序信息。delay 和 easing 的值为可选值。其中可以包含 style() 调用。
团体() 指定一组要并行运行的动画步骤(内部动画)。动画仅在所有内部动画步骤完成后才会继续。

我们将使用trigger()将动画绑定到一个动画名称,我们将其命名为“listItemAnimation”。然后,我们将使用 和 定义添加和抑制的动画序列transition(':enter')transition(':leave')之后,我们将使用 定义起始样式style(),最后我们将使用 来设置高度动画animate()

它看起来应该是这样的:

animations: [
   trigger('listItemAnimation', [
      transition(':enter', [
        style({ height: '0px', overflow: 'hidden' }),
        group([animate('250ms ease-out', style({ height: '!' }))]),
// although group is useless here (since we have only one animation)
// i like to leave it anyway just in case i want to add another 
// parallel animation in the future
      ]),
      transition(':leave', [
        style({ height: '!', overflow: 'hidden' }),
        group([animate('250ms ease-out', style({ height: '0px' }))]),
      ]),
    ]),
]
Enter fullscreen mode Exit fullscreen mode

ℹ️'!'是一个用于自动样式的特殊标记,其中样式源自正在动画的元素,并在动画启动时应用于动画。简而言之,它是动画应用之前的状态。

现在我们已经实现了动画,让我们将它绑定到 HTML 元素。添加@listItemAnimation到“side-list-item”元素:

    <div
      class="side-list-item"
      @listItemAnimation
      ...
    >
...
</div>
Enter fullscreen mode Exit fullscreen mode

太棒了,我们快完成了,我们的联系人列表已经动画化了💪!

附言:如果您想在第一次打印时禁用此动画,则需要在父元素(列表元素)中添加一个:enter类似的空动画。当您将动画放置在组件上时,它会在触发时禁用其所有子动画。trigger('noEnterAnimation', [transition(':enter', [])]):enter:enter

动画内容

内容动画稍微有点棘手。如果我们从左到右对内容容器进行动画处理,我们有两个选择:要么调整宽度,要么平移元素。可惜的是,这两种方案都行不通。如果我们调整宽度,就会得到这样的效果:

宽度动画

随着宽度增加,内容也出现了,但感觉不对,因为内容没有从左向右滑动。为了解决这个问题,我们可以尝试将内容从左向右平移,但请看会发生什么:

翻译动画

它会产生一个闪烁效果(空的选择组件),因为当我们平移内容元素时,我们没有为其宽度设置动画,所以它会在眨眼间从当前值变为 0。因此,解决方案是,我们必须在为宽度设置动画的同时平移内容。

在下图中,我们有一个黑色的外部容器和蓝色的内部容器:

内部和外部元素

因此,我们将在平移蓝色容器的同时,对黑色容器的宽度进行动画处理。看起来应该像这样:

好的动画

为此,我们需要为内容元素(黑色容器:“side-list-content-data”)及其子元素(蓝色容器:“side-list-content-data-inner”)添加动画。为此,我们需要使用另一个 Angular 动画 API:

功能 它起什么作用?
询问() 用于查找当前元素内的一个或多个内部 HTML 元素。

在该 API 的帮助下,实现应该如下所示:

   trigger('sideContentAnimation', [
      transition(':enter', [
        // we set the width of the outer container to 0, and hide the
        // overflow (so the inner container won't be visible)
        style({ width: '0px', overflow: 'hidden' }),
        group([
          // we animate the outer container width to it's original value
          animate('250ms ease-out', style({ width: '!' })),
          // in the same time we translate the inner element all the
          // way from left to right
          query('.side-list-content-data-inner', [
            style({ transform: 'translateX(-110%)' }),
            group([animate('250ms ease-out', style({ transform: 'translateX(-0%)' }))]),
          ]),
        ]),
      ]),
      transition(':leave', [
        style({ overflow: 'hidden' }),
        group([
          animate('250ms ease-out', style({ width: '0' })),
          query('.side-list-content-data-inner', [
            style({ transform: 'translateX(-0%)' }),
            group([animate('250ms ease-out', style({ transform: 'translateX(-110%)' }))]),
          ]),
        ]),
      ]),
    ])
Enter fullscreen mode Exit fullscreen mode

您可以在此处找到完整的源文件

使用动画更新的 HTML 文件sideContentAnimation

  <!-- * content -->
  <div 
      class="side-list-content-data" 
      *ngIf="!!selectedContact" 
      @sideContentAnimation
  >
  ...
  </div>
Enter fullscreen mode Exit fullscreen mode

就是这样,我们已经完成了所需的行为,并且我们学习了如何使用 Angular 动画 API 来制作这些相当复杂的动画。

结论

总而言之,我们学习了如何使用trigger()来定义动画名称,然后我们用它transition()来定义不同状态之间的动画(例如:enter:leave)。之后,我们用它来style()定义动画开始时的样式,我们用它来animate()更改样式,我们还了解了如何使用 来同时执行多个动画group()。最后,我们用它来query()将动画附加到我们正在动画的子元素上。

希望你喜欢这篇文章,我们下期再见。祝你
编程愉快!


这篇文章就到这里。希望你喜欢。如果喜欢,请分享给你的朋友和同事。你也可以在 Twitter 上关注我@theAngularGuy,这会对我有很大帮助。

祝你有美好的一天 !


接下来读什么?

鏂囩珷鏉ユ簮锛�https://dev.to/mustapha/all-you-need-to-know-about-angular-animations-1c09
PREV
Angular:使用 ngTemplateOutlet 构建更多动态组件🎭简介定义用例 #1:上下文感知模板用例 #2:模板重载用例 #3:树总结
NEXT
教你的孩子用 Python 构建自己的游戏 - 1