CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / ANGULAR / P-192 · Angular 高级教程

Angular 22+ 教程 26:Animation 动画

动画不是装饰,是状态迁移的视觉表达。Angular 从早期就内建了一套声明式动画体系(@angular/animations),能以数据驱动的方式描述”状态 A 到状态 B 如何过渡”。到 v22,官方的指导思路更务实:简单动画优先 CSS,状态编排复杂时用 Angular Animations,需要精细控制时用 WAAPI。本文先讲透 Angular 动画本身,再给出选型建议。

核心五要素:trigger / state / transition / style / animate

Angular 动画围绕”触发器(trigger)“组织,挂到模板上后根据绑定值在状态间过渡:

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

@Component({
  selector: 'app-panel',
  animations: [
    trigger('openClose', [
      state('open', style({ height: '*', opacity: 1 })),
      state('closed', style({ height: '0px', opacity: 0 })),
      transition('open <=> closed', [animate('250ms ease-in-out')]),
    ]),
  ],
  template: `
    <button type="button" (click)="isOpen = !isOpen">切换</button>
    <div class="panel" [@openClose]="isOpen ? 'open' : 'closed'">面板内容</div>
  `,
})
export class PanelComponent {
  isOpen = true;
}

各要素的分工:

  • trigger(name, steps):命名并注册一组动画,模板用 [@name] 绑定
  • state(name, style):命名一个终态样式;height: '*' 表示”自适应当前内容高度”
  • transition(stateExpr, steps):声明哪两个状态之间要播放动画,<=> 是双向简写
  • style(...):静态样式快照
  • animate(duration, style):在时长内过渡到目标样式

使用前记得在应用根提供动画能力(懒加载场景推荐异步版本,没有动画触发时不会加载动画包):

bootstrapApplication(AppComponent, {
  providers: [provideAnimationsAsync()],
});

enter 与 leave:元素的进出场

元素因 @if@for 等控制流插入或移除时,可以用 :enter / :leave 伪状态做进出场动画——这是日常使用频率最高的两个:

trigger('fadeSlide', [
  transition(':enter', [
    style({ opacity: 0, transform: 'translateY(-8px)' }),
    animate('250ms ease-out', style({ opacity: 1, transform: 'translateY(0)' })),
  ]),
  transition(':leave', [
    animate('200ms ease-in', style({ opacity: 0, transform: 'translateY(-8px)' })),
  ]),
]),
@if (showTip()) {
  <p class="tip" @fadeSlide>已保存</p>
}

:enter/:leave 让”渲染”和”动画”天然同步——不存在”元素已删但动画还想播”的时序问题,框架会等 leave 动画结束才真正移除 DOM 节点。

:increment 与 :decrement:数值变化动画

当绑定值增加或减少时,可以用 :increment / :decrement 匹配。经典场景是计数器高亮:

trigger('countPulse', [
  transition(':increment', [
    animate('150ms', style({ color: '#4caf50', transform: 'scale(1.15)' })),
    animate('150ms', style({ color: '*', transform: 'scale(1)' })),
  ]),
  transition(':decrement', [
    animate('150ms', style({ color: '#f44336', transform: 'scale(0.9)' })),
    animate('150ms', style({ color: '*', transform: 'scale(1)' })),
  ]),
]),
<span class="count" [@countPulse]="likes()">{{ likes() }}</span>

信号作为动画绑定值完全可用——模板绑定表达式里的 likes() 变化即触发过渡判断。

列表编排:query 与 stagger

批量进出场用 query 选中子元素、stagger 错开时间,几行代码做出瀑布式入场:

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

trigger('listStagger', [
  transition(':enter', [
    query('.item', [
      style({ opacity: 0, transform: 'translateX(-16px)' }),
      stagger(80, animate('250ms ease-out', style({ opacity: 1, transform: 'translateX(0)' }))),
    ], { optional: true }),
  ]),
]),
<ul @listStagger>
  @for (item of items(); track item.id) {
    <li class="item">{{ item.title }}</li>
  }
</ul>

{ optional: true } 让 query 在匹配不到元素时不抛错(首渲染时机问题常见的防御项)。此外 group() 让多段动画并行、sequence() 串行,配合 keyframes() 可以描述多步动画,这里不再展开。

动画与信号:驱动方式的变化

v22 的组件状态几乎都是信号,动画绑定值直接读信号即可:

export class DrawerComponent {
  readonly open = signal(false);
}
<div class="drawer" [@drawer]="open() ? 'open' : 'closed'">...</div>

与 Zoneless 也完全兼容:动画触发器本身不依赖 Zone,绑定值(信号)变化会精确触发对应组件的更新。要避免的反模式是在 effect 里手动改样式类——那是把声明式动画倒退回命令式。

与 CSS 动画、WAAPI 的取舍

现代浏览器的能力让”纯 CSS”能覆盖过去必须靠 JS 的场景(@starting-style 支持进出场首帧、transition-behavior: allow-discrete 支持 display 过渡)。三套方案的定位:

维度CSS 动画/过渡Angular AnimationsWAAPI(element.animate)
适用场景hover、简单过渡、进出场状态机驱动、多元素编排精细逐帧控制、复用动画对象
状态编排弱(靠类名切换)强(state/transition 声明)手工编排
多元素编排query/stagger 内建手写循环
动态参数CSS 变量传递有限任意绑定表达式完全自由
包体积零成本引入 @angular/animations零成本
与 Angular 控制流集成需自行处理移除时序:enter/:leave 自动处理需自行处理
学习/维护成本中高

选型建议按顺序自问:

  1. 能不能用 CSS? 简单过渡、hover 态、纯展示型进出场,CSS 是首选,零运行时成本
  2. 有没有多状态/多元素编排? 抽屉开合、手风琴、列表瀑布入场、依赖应用状态的复杂序列——Angular Animations 的声明式表达最省心
  3. 是不是游戏级/物理级效果? 拖拽跟随、惯性滚动、粒子效果,用 WAAPI 或 requestAnimationFrame 自己驱动

另外,页面级过渡可以关注 Router 的 View Transitions 集成(provideRouter(routes, withViewTransitions())),路由切换的整页动画交给浏览器原生能力,不占应用动画预算。

性能与可维护性提示

  • 只动画 transformopacity,它们走合成器线程;避免动画 height/top/left 引发重排(自适应高度展开是个合理例外,注意限制使用范围)
  • 离开视口的元素及时移除,:leave 动画时长别超过 300ms
  • 复杂 trigger 放到独立文件用 useAnimation 复用,别让组件装饰器变成动画垃圾场
  • 调试时给 trigger 临时加 animate('0ms') 可以快速确认是”状态没切”还是”动画太慢”

系列导航

← 算法 026:图:最短路径(Dijkstra & Floyd) 目录 设计模式 026:访问者(Visitor) →
← 返回文章列表