Angular动画与效果

Angular提供了一个强大的动画系统,允许开发者创建复杂的组件动画、路由转场和状态过渡效果。

前提条件:使用Angular动画需要在AppModule中导入BrowserAnimationsModule

1. 动画基础配置

// app.module.ts - 配置动画模块
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; // 导入动画模块

@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule  // 添加动画模块
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule { }

动画演示

点击下方动画框或按钮体验不同动画效果

点击我

2. 基本动画实现

2.1 状态和转场动画

// fade.animation.ts - 淡入淡出动画
import {
  trigger,
  state,
  style,
  animate,
  transition
} from '@angular/animations';

export const fadeAnimation = trigger('fade', [
  // 定义状态
  state('void', style({
    opacity: 0
  })),
  state('*', style({
    opacity: 1
  })),

  // 定义转场
  transition(':enter', [
    animate('300ms ease-in')
  ]),
  transition(':leave', [
    animate('300ms ease-out', style({ opacity: 0 }))
  ])
]);

// 在组件中使用
@Component({
  selector: 'app-fade-demo',
  templateUrl: './fade-demo.component.html',
  animations: [fadeAnimation]
})
export class FadeDemoComponent {
  isVisible = true;

  toggle(): void {
    this.isVisible = !this.isVisible;
  }
}
<!-- fade-demo.component.html -->
<div class="fade-demo">
  <button (click)="toggle()">{{isVisible ? '隐藏' : '显示'}}元素</button>

  <div @fade *ngIf="isVisible" class="animated-element">
    我会淡入淡出
  </div>
</div>

2.2 多状态动画

// toggle.animation.ts - 切换状态动画
import { trigger, state, style, animate, transition } from '@angular/animations';

export const toggleAnimation = trigger('toggle', [
  // 定义多个状态
  state('open', style({
    height: '200px',
    opacity: 1,
    backgroundColor: '#4caf50'
  })),
  state('closed', style({
    height: '100px',
    opacity: 0.8,
    backgroundColor: '#f44336'
  })),
  state('disabled', style({
    height: '50px',
    opacity: 0.5,
    backgroundColor: '#9e9e9e',
    cursor: 'not-allowed'
  })),

  // 定义转场
  transition('open => closed', [
    animate('300ms ease-out')
  ]),
  transition('closed => open', [
    animate('300ms ease-in')
  ]),
  transition('* => disabled', [
    animate('500ms')
  ]),
  transition('disabled => *', [
    animate('500ms')
  ])
]);

// 在组件中使用
@Component({
  selector: 'app-toggle-demo',
  template: `
    <div class="toggle-demo">
      <button (click)="state = 'open'">打开</button>
      <button (click)="state = 'closed'">关闭</button>
      <button (click)="state = 'disabled'">禁用</button>

      <div [@toggle]="state" class="toggle-box">
        当前状态: {{state}}
      </div>
    </div>
  `,
  animations: [toggleAnimation]
})
export class ToggleDemoComponent {
  state: 'open' | 'closed' | 'disabled' = 'open';
}

3. 高级动画技巧

3.1 关键帧动画

// keyframe.animation.ts - 关键帧动画
import {
  trigger,
  state,
  style,
  animate,
  transition,
  keyframes,
  animation,
  useAnimation
} from '@angular/animations';

// 定义可重用的动画
export const bounceAnimation = animation([
  animate('1000ms', keyframes([
    style({ transform: 'scale(1)', offset: 0 }),
    style({ transform: 'scale(1.2)', offset: 0.2 }),
    style({ transform: 'scale(0.8)', offset: 0.4 }),
    style({ transform: 'scale(1.1)', offset: 0.6 }),
    style({ transform: 'scale(0.9)', offset: 0.8 }),
    style({ transform: 'scale(1)', offset: 1 })
  ]))
]);

// 关键帧动画触发器
export const keyframeTrigger = trigger('keyframe', [
  transition(':enter', [
    useAnimation(bounceAnimation)
  ]),
  transition('* => bounce', [
    useAnimation(bounceAnimation)
  ])
]);

// 复杂的进度条动画
export const progressAnimation = trigger('progress', [
  state('start', style({ width: '0%' })),
  state('complete', style({ width: '100%' })),

  transition('start => complete', [
    animate('2000ms ease-in-out', keyframes([
      style({ width: '0%', backgroundColor: '#f44336', offset: 0 }),
      style({ width: '30%', backgroundColor: '#ff9800', offset: 0.3 }),
      style({ width: '60%', backgroundColor: '#ffeb3b', offset: 0.6 }),
      style({ width: '100%', backgroundColor: '#4caf50', offset: 1 })
    ]))
  ])
]);
// 在组件中使用关键帧动画
@Component({
  selector: 'app-keyframe-demo',
  template: `
    <div class="keyframe-demo">
      <button (click)="triggerBounce()">触发弹跳动画</button>
      <button (click)="startProgress()">开始进度条</button>

      <div @keyframe="animationState" class="bounce-box">
        弹跳方块
      </div>

      <div class="progress-container">
        <div @progress="progressState" class="progress-bar"></div>
      </div>
    </div>
  `,
  animations: [keyframeTrigger, progressAnimation]
})
export class KeyframeDemoComponent {
  animationState = '';
  progressState = 'start';

  triggerBounce(): void {
    this.animationState = 'bounce';

    // 重置状态以便再次触发
    setTimeout(() => {
      this.animationState = '';
    }, 1000);
  }

  startProgress(): void {
    this.progressState = 'start';

    // 触发动画
    setTimeout(() => {
      this.progressState = 'complete';
    }, 100);
  }
}

3.2 交错动画

// stagger.animation.ts - 交错动画
import {
  trigger,
  transition,
  style,
  animate,
  query,
  stagger
} from '@angular/animations';

export const listAnimation = trigger('listAnimation', [
  transition('* => *', [
    // 为进入的元素设置初始状态
    query(':enter', [
      style({ opacity: 0, transform: 'translateY(-20px)' }),
      stagger(100, [
        animate('300ms ease-out', style({
          opacity: 1,
          transform: 'translateY(0)'
        }))
      ])
    ], { optional: true }),

    // 为离开的元素设置动画
    query(':leave', [
      stagger(-50, [
        animate('200ms ease-in', style({
          opacity: 0,
          transform: 'translateX(100px)'
        }))
      ])
    ], { optional: true })
  ])
]);

// 在组件中使用
@Component({
  selector: 'app-list-demo',
  template: `
    <div class="list-demo">
      <button (click)="addItem()">添加项目</button>
      <button (click)="removeItem()">移除项目</button>

      <ul @listAnimation class="animated-list">
        <li *ngFor="let item of items">
          {{item}}
          <button (click)="removeSpecificItem(item)">移除</button>
        </li>
      </ul>
    </div>
  `,
  animations: [listAnimation]
})
export class ListDemoComponent {
  items: string[] = ['项目1', '项目2', '项目3'];
  counter = 4;

  addItem(): void {
    this.items.push(`项目${this.counter++}`);
  }

  removeItem(): void {
    if (this.items.length > 0) {
      this.items.pop();
    }
  }

  removeSpecificItem(item: string): void {
    const index = this.items.indexOf(item);
    if (index > -1) {
      this.items.splice(index, 1);
    }
  }
}

4. 路由动画

1
2
3
4
5
// route.animations.ts - 路由转场动画
import {
  trigger,
  animateChild,
  group,
  transition,
  animate,
  style,
  query
} from '@angular/animations';

// 路由转场动画
export const routeAnimation = trigger('routeAnimation', [
  // 定义不同路由之间的转场
  transition('* <=> *', [
    // 设置初始状态
    style({ position: 'relative' }),
    query(':enter, :leave', [
      style({
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%',
        height: '100%'
      })
    ]),

    // 动画顺序
    query(':enter', [
      style({ opacity: 0 })
    ]),

    // 同时执行离开和进入动画
    group([
      query(':leave', [
        animate('300ms ease-out', style({
          opacity: 0,
          transform: 'translateX(-100px)'
        }))
      ]),
      query(':enter', [
        animate('400ms ease-in', style({
          opacity: 1,
          transform: 'translateX(0)'
        }))
      ])
    ])
  ])
]);

// 淡入淡出路有动画
export const fadeRouteAnimation = trigger('fadeRoute', [
  transition('* <=> *', [
    query(':enter', [
      style({ opacity: 0 })
    ], { optional: true }),

    query(':leave', [
      animate('200ms', style({ opacity: 0 }))
    ], { optional: true }),

    query(':enter', [
      animate('300ms', style({ opacity: 1 }))
    ], { optional: true })
  ])
]);

// 滑动路由动画
export const slideRouteAnimation = trigger('slideRoute', [
  transition(':increment', [
    style({ position: 'relative' }),
    query(':enter, :leave', [
      style({
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%'
      })
    ]),

    query(':enter', [
      style({ left: '100%' })
    ]),

    query(':leave', [
      animate('300ms ease-out', style({ left: '-100%' }))
    ]),

    query(':enter', [
      animate('300ms ease-out', style({ left: '0%' }))
    ])
  ]),

  transition(':decrement', [
    style({ position: 'relative' }),
    query(':enter, :leave', [
      style({
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%'
      })
    ]),

    query(':enter', [
      style({ left: '-100%' })
    ]),

    query(':leave', [
      animate('300ms ease-out', style({ left: '100%' }))
    ]),

    query(':enter', [
      animate('300ms ease-out', style({ left: '0%' }))
    ])
  ])
]);
// 在路由配置中使用
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
import { ContactComponent } from './contact.component';

const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
    data: { animation: 'home' }
  },
  {
    path: 'about',
    component: AboutComponent,
    data: { animation: 'about' }
  },
  {
    path: 'contact',
    component: ContactComponent,
    data: { animation: 'contact' }
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

// 在AppComponent中使用路由动画
@Component({
  selector: 'app-root',
  template: `
    <div class="app-container">
      <nav>
        <a routerLink="/">首页</a>
        <a routerLink="/about">关于</a>
        <a routerLink="/contact">联系</a>
      </nav>

      <!-- 路由出口 -->
      <div [@routeAnimation]="getAnimationData(routerOutlet)">
        <router-outlet #routerOutlet="outlet"></router-outlet>
      </div>
    </div>
  `,
  animations: [routeAnimation]
})
export class AppComponent {
  getAnimationData(outlet: RouterOutlet): string {
    return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
  }
}

5. 动画参数与配置

// configurable.animation.ts - 可配置动画
import {
  trigger,
  transition,
  animate,
  style,
  AnimationMetadata,
  AnimationOptions
} from '@angular/animations';

// 可配置的动画工厂函数
export function createFadeAnimation(
  duration: string = '300ms',
  easing: string = 'ease-in-out'
): AnimationMetadata[] {
  return [
    transition(':enter', [
      style({ opacity: 0 }),
      animate(`${duration} ${easing}`, style({ opacity: 1 }))
    ]),
    transition(':leave', [
      animate(`${duration} ${easing}`, style({ opacity: 0 }))
    ])
  ];
}

export function createSlideAnimation(
  direction: 'left' | 'right' | 'top' | 'bottom' = 'left',
  distance: string = '100px'
): AnimationMetadata[] {
  const fromStyle: any = { opacity: 0 };

  switch(direction) {
    case 'left':
      fromStyle.transform = `translateX(-${distance})`;
      break;
    case 'right':
      fromStyle.transform = `translateX(${distance})`;
      break;
    case 'top':
      fromStyle.transform = `translateY(-${distance})`;
      break;
    case 'bottom':
      fromStyle.transform = `translateY(${distance})`;
      break;
  }

  return [
    transition(':enter', [
      style(fromStyle),
      animate('300ms ease-out', style({
        opacity: 1,
        transform: 'translate(0, 0)'
      }))
    ]),
    transition(':leave', [
      animate('300ms ease-in', style(fromStyle))
    ])
  ];
}

// 动画选项配置
export const animationOptions: AnimationOptions = {
  params: {
    duration: '500ms',
    easing: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
    color: '#3498db',
    scale: 1.2
  }
};

// 参数化动画
export const parametricAnimation = trigger('parametric', [
  transition(':enter', [
    style({
      opacity: 0,
      transform: 'scale({{scale}})'
    }),
    animate('{{duration}} {{easing}}', style({
      opacity: 1,
      transform: 'scale(1)',
      backgroundColor: '{{color}}'
    }))
  ])
]);
// 在组件中使用可配置动画
@Component({
  selector: 'app-configurable-demo',
  template: `
    <div class="configurable-demo">
      <div class="controls">
        <select [(ngModel)]="animationType">
          <option value="fade">淡入淡出</option>
          <option value="slideLeft">左侧滑动</option>
          <option value="slideRight">右侧滑动</option>
          <option value="slideTop">顶部滑动</option>
          <option value="slideBottom">底部滑动</option>
        </select>

        <input type="range" [(ngModel)]="duration" min="100" max="2000" step="100">
        <span>持续时间: {{duration}}ms</span>
      </div>

      <div [@animation]="getAnimationParams()" class="animated-box">
        可配置动画
      </div>
    </div>
  `,
  animations: [
    trigger('animation', [
      transition(':enter, * => fade', createFadeAnimation()),
      transition('* => slideLeft', createSlideAnimation('left')),
      transition('* => slideRight', createSlideAnimation('right')),
      transition('* => slideTop', createSlideAnimation('top')),
      transition('* => slideBottom', createSlideAnimation('bottom'))
    ])
  ]
})
export class ConfigurableDemoComponent {
  animationType = 'fade';
  duration = 300;

  getAnimationParams(): any {
    return {
      value: this.animationType,
      params: {
        duration: `${this.duration}ms`
      }
    };
  }
}

// 使用参数化动画
@Component({
  selector: 'app-parametric-demo',
  template: `
    <div class="parametric-demo">
      <div [@parametric]="{
        value: show,
        params: {
          duration: '800ms',
          easing: 'ease-out',
          color: '#4caf50',
          scale: 0.5
        }
      }" class="parametric-box" *ngIf="show">
        参数化动画
      </div>

      <button (click)="toggle()">切换</button>
    </div>
  `,
  animations: [parametricAnimation]
})
export class ParametricDemoComponent {
  show = true;

  toggle(): void {
    this.show = !this.show;
  }
}

6. 动画钩子与事件

// animation-hooks.component.ts - 动画钩子
import {
  Component,
  HostBinding
} from '@angular/core';
import {
  trigger,
  transition,
  animate,
  style,
  state,
  AnimationEvent
} from '@angular/animations';

@Component({
  selector: 'app-animation-hooks',
  template: `
    <div class="hooks-demo">
      <button (click)="toggle()">{{isVisible ? '隐藏' : '显示'}}</button>

      <div
        [@boxAnimation]="animationState"
        (@boxAnimation.start)="onAnimationStart($event)"
        (@boxAnimation.done)="onAnimationDone($event)"
        class="animated-box">

        <p>动画钩子演示</p>
        <p>状态: {{currentState}}</p>
        <p>阶段: {{currentPhase}}</p>
      </div>

      <div class="event-log">
        <h4>动画事件日志</h4>
        <div *ngFor="let log of logs">{{log}}</div>
      </div>
    </div>
  `,
  animations: [
    trigger('boxAnimation', [
      state('visible', style({
        opacity: 1,
        transform: 'translateX(0) scale(1)'
      })),
      state('hidden', style({
        opacity: 0,
        transform: 'translateX(100px) scale(0.8)'
      })),
      transition('visible <=> hidden', [
        animate('500ms ease-in-out')
      ])
    ])
  ]
})
export class AnimationHooksComponent {
  @HostBinding('@.disabled') animationsDisabled = false;

  isVisible = true;
  animationState = 'visible';
  currentState = '';
  currentPhase = '';
  logs: string[] = [];

  toggle(): void {
    this.isVisible = !this.isVisible;
    this.animationState = this.isVisible ? 'visible' : 'hidden';
  }

  onAnimationStart(event: AnimationEvent): void {
    this.currentState = event.toState;
    this.currentPhase = 'start';

    const log = `动画开始: ${event.triggerName} - ${event.fromState} → ${event.toState}`;
    this.logs.unshift(log);

    // 限制日志数量
    if (this.logs.length > 10) {
      this.logs.pop();
    }

    console.log('动画开始:', event);
  }

  onAnimationDone(event: AnimationEvent): void {
    this.currentPhase = 'done';

    const log = `动画完成: ${event.triggerName} - 总时长: ${event.totalTime}ms`;
    this.logs.unshift(log);

    console.log('动画完成:', event);
  }

  disableAnimations(): void {
    this.animationsDisabled = true;
  }

  enableAnimations(): void {
    this.animationsDisabled = false;
  }
}

7. 动画性能优化

动画性能优化建议:
  • 使用will-change属性提示浏览器优化
  • 避免使用昂贵的CSS属性(如box-shadow、filter等)
  • 使用transformopacity代替布局属性
  • 限制同时运行的动画数量
  • 使用ngZone.runOutsideAngular()处理复杂动画
  • 实现虚拟滚动处理大量动画元素
  • 使用@.disabled绑定在需要时禁用动画
// performance.animations.ts - 高性能动画
import { trigger, transition, style, animate } from '@angular/animations';

// 高性能动画 - 使用transform和opacity
export const highPerformanceAnimation = trigger('highPerf', [
  transition(':enter', [
    style({
      opacity: 0,
      transform: 'translate3d(0, 100px, 0) scale3d(0.8, 0.8, 1)'  // 使用3D变换
    }),
    animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({
      opacity: 1,
      transform: 'translate3d(0, 0, 0) scale3d(1, 1, 1)'
    }))
  ]),
  transition(':leave', [
    animate('200ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({
      opacity: 0,
      transform: 'translate3d(0, -50px, 0) scale3d(0.9, 0.9, 1)'
    }))
  ])
]);

// 批量动画优化
export const batchAnimation = trigger('batch', [
  transition(':increment', [
    // 批量更新时使用简单动画
    style({ opacity: 0.8 }),
    animate('100ms ease-out', style({ opacity: 1 }))
  ])
]);

// 条件动画禁用
@Component({
  selector: 'app-performance-demo',
  template: `
    <div class="performance-demo" [@.disabled]="disableAllAnimations">
      <div class="controls">
        <button (click)="toggleAnimations()">
          {{disableAllAnimations ? '启用' : '禁用'}}所有动画
        </button>
        <button (click)="addItems()">添加100个项目</button>
      </div>

      <div class="items-container">
        <div *ngFor="let item of items; trackBy: trackById"
             [@highPerf]
             class="item">
          项目 {{item.id}}
        </div>
      </div>
    </div>
  `,
  animations: [highPerformanceAnimation]
})
export class PerformanceDemoComponent {
  items: { id: number }[] = [];
  disableAllAnimations = false;
  private nextId = 0;

  constructor(private ngZone: NgZone) {}

  ngOnInit(): void {
    // 初始加载一些项目
    for (let i = 0; i < 20; i++) {
      this.items.push({ id: this.nextId++ });
    }
  }

  addItems(): void {
    // 在Angular区域外执行大量添加操作
    this.ngZone.runOutsideAngular(() => {
      const newItems = [];
      for (let i = 0; i < 100; i++) {
        newItems.push({ id: this.nextId++ });
      }

      // 批量更新
      this.ngZone.run(() => {
        this.items = [...this.items, ...newItems];
      });
    });
  }

  toggleAnimations(): void {
    this.disableAllAnimations = !this.disableAllAnimations;
  }

  trackById(index: number, item: any): number {
    return item.id;
  }
}

8. 动画库与第三方集成

// 与GSAP集成
import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
import gsap from 'gsap';

@Component({
  selector: 'app-gsap-demo',
  template: `
    <div #animationContainer class="gsap-demo">
      <div #animatedElement class="gsap-box">
        GSAP动画
      </div>

      <button (click)="playAnimation()">播放动画</button>
      <button (click)="reverseAnimation()">反转动画</button>
      <button (click)="restartAnimation()">重新开始</button>
    </div>
  `
})
export class GsapDemoComponent implements AfterViewInit {
  @ViewChild('animatedElement') animatedElement!: ElementRef;
  @ViewChild('animationContainer') animationContainer!: ElementRef;

  private timeline: any;

  ngAfterViewInit(): void {
    this.createTimeline();
  }

  private createTimeline(): void {
    this.timeline = gsap.timeline({
      paused: true,
      defaults: { duration: 1, ease: "power2.inOut" }
    });

    this.timeline
      .to(this.animatedElement.nativeElement, {
        x: 300,
        rotation: 360,
        scale: 1.5,
        backgroundColor: '#4caf50'
      })
      .to(this.animatedElement.nativeElement, {
        y: 100,
        rotation: -180,
        scale: 0.8,
        backgroundColor: '#2196f3'
      }, "-=0.5") // 重叠动画
      .to(this.animatedElement.nativeElement, {
        x: 0,
        y: 0,
        rotation: 0,
        scale: 1,
        backgroundColor: '#ff9800'
      });
  }

  playAnimation(): void {
    this.timeline.play();
  }

  reverseAnimation(): void {
    this.timeline.reverse();
  }

  restartAnimation(): void {
    this.timeline.restart();
  }
}

// 与Animate.css集成
@Component({
  selector: 'app-animate-css-demo',
  template: `
    <div class="animate-css-demo">
      <div class="animation-selector">
        <select [(ngModel)]="selectedAnimation">
          <option *ngFor="let anim of animations" [value]="anim">
            {{anim}}
          </option>
        </select>
        <button (click)="triggerAnimation()">播放动画</button>
      </div>

      <div class="animated-element"
           [class.animated]="isAnimating"
           [class]="selectedAnimation">
        动画元素
      </div>
    </div>
  `,
  styles: [`
    .animated-element {
      width: 200px;
      height: 200px;
      background: #3498db;
      color: white;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 24px;
      margin: 20px auto;
      border-radius: 8px;
    }
  `]
})
export class AnimateCssDemoComponent {
  animations = [
    'bounce', 'flash', 'pulse', 'rubberBand', 'shakeX', 'shakeY',
    'headShake', 'swing', 'tada', 'wobble', 'jello', 'heartBeat',
    'backInDown', 'backInLeft', 'backInRight', 'backInUp',
    'bounceIn', 'bounceInDown', 'bounceInLeft', 'bounceInRight', 'bounceInUp'
  ];

  selectedAnimation = 'bounce';
  isAnimating = false;

  triggerAnimation(): void {
    this.isAnimating = true;

    // 动画完成后重置
    setTimeout(() => {
      this.isAnimating = false;
    }, 1000);
  }
}

9. 实战案例:完整动画组件

// notification.component.ts - 带动画的通知组件
import {
  Component,
  Input,
  Output,
  EventEmitter,
  HostBinding,
  OnDestroy
} from '@angular/core';
import {
  trigger,
  state,
  style,
  animate,
  transition,
  AnimationEvent
} from '@angular/animations';

export type NotificationType = 'success' | 'error' | 'warning' | 'info';

@Component({
  selector: 'app-notification',
  template: `
    <div class="notification" [@notification]="animationState" (@notification.done)="onAnimationDone($event)">
      <div class="notification-content" [class]="type">
        <div class="notification-icon">
          <i [class]="getIconClass()"></i>
        </div>
        <div class="notification-body">
          <h4 *ngIf="title">{{title}}</h4>
          <p>{{message}}</p>
        </div>
        <button class="notification-close" (click)="close()">
          <i class="fas fa-times"></i>
        </button>
      </div>
    </div>
  `,
  styles: [`
    .notification {
      position: fixed;
      top: 20px;
      right: 20px;
      z-index: 1000;
      max-width: 400px;
      min-width: 300px;
    }

    .notification-content {
      display: flex;
      align-items: center;
      padding: 15px;
      border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
      background: white;
      border-left: 4px solid;
    }

    .success {
      border-left-color: #4caf50;
    }

    .error {
      border-left-color: #f44336;
    }

    .warning {
      border-left-color: #ff9800;
    }

    .info {
      border-left-color: #2196f3;
    }

    .notification-icon {
      font-size: 24px;
      margin-right: 15px;
    }

    .notification-body {
      flex: 1;
    }

    .notification-body h4 {
      margin: 0 0 5px 0;
      font-size: 16px;
    }

    .notification-body p {
      margin: 0;
      color: #666;
    }

    .notification-close {
      background: none;
      border: none;
      font-size: 18px;
      color: #999;
      cursor: pointer;
      margin-left: 10px;
      padding: 5px;
    }

    .notification-close:hover {
      color: #333;
    }
  `],
  animations: [
    trigger('notification', [
      state('void', style({
        transform: 'translateX(100%)',
        opacity: 0
      })),
      state('visible', style({
        transform: 'translateX(0)',
        opacity: 1
      })),
      state('hidden', style({
        transform: 'translateX(100%)',
        opacity: 0
      })),
      transition('void => visible', [
        animate('300ms cubic-bezier(0.68, -0.55, 0.265, 1.55)')
      ]),
      transition('visible => hidden', [
        animate('200ms ease-out')
      ])
    ])
  ]
})
export class NotificationComponent implements OnDestroy {
  @Input() type: NotificationType = 'info';
  @Input() title?: string;
  @Input() message: string = '';
  @Input() duration: number = 5000; // 自动关闭时间(毫秒)

  @Output() closed = new EventEmitter();

  animationState: 'void' | 'visible' | 'hidden' = 'void';
  private timeoutId?: number;
  private autoClose = true;

  ngOnInit(): void {
    // 初始显示动画
    setTimeout(() => {
      this.animationState = 'visible';
    }, 0);

    // 设置自动关闭
    if (this.duration > 0 && this.autoClose) {
      this.timeoutId = window.setTimeout(() => {
        this.close();
      }, this.duration);
    }
  }

  close(): void {
    this.animationState = 'hidden';

    // 清除自动关闭定时器
    if (this.timeoutId) {
      clearTimeout(this.timeoutId);
    }
  }

  onAnimationDone(event: AnimationEvent): void {
    if (event.toState === 'hidden') {
      this.closed.emit();
    }
  }

  getIconClass(): string {
    switch (this.type) {
      case 'success':
        return 'fas fa-check-circle text-success';
      case 'error':
        return 'fas fa-exclamation-circle text-danger';
      case 'warning':
        return 'fas fa-exclamation-triangle text-warning';
      case 'info':
        return 'fas fa-info-circle text-info';
      default:
        return 'fas fa-info-circle';
    }
  }

  ngOnDestroy(): void {
    if (this.timeoutId) {
      clearTimeout(this.timeoutId);
    }
  }
}
动画设计原则:
  • 目的明确:动画应该有明确的目的,而不是装饰
  • 适度使用:避免过度使用动画导致用户分心
  • 一致性:保持整个应用的动画风格一致
  • 性能优先:确保动画不会影响应用性能
  • 可访问性:为偏好减少动画的用户提供选项
  • 响应式:确保动画在不同设备上都能良好工作
  • 测试:在不同浏览器和设备上测试动画效果