如何使用/启用动画图标?

26

2
这些动画只是示例,不是规范的一部分。它们只是展示了你自己可以做什么。 - Reactgular
6个回答

15

如其他人所述,Material Icon网站上的示例必须进行构建。

然而,我找到这个问题是为了寻求有关如何动画化角材料图标的指南,对于其他寻找相同内容的人,我有一个解决方案。默认动画可以定制为不仅仅是360度旋转。

基本上,您可以创建一个组件,在单击或父元素(如按钮)被单击时在mat-icon之间切换。

先决条件是您拥有安装了材料图标的Angular Material应用程序。我使用了Angular Material 8

这里是一个工作的Stackblitz:https://stackblitz.com/edit/angular-material-prototype-animated-icon

mat-animated-icon.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'mat-animated-icon',
  templateUrl: './mat-animated-icon.component.html',
  styleUrls: ['./mat-animated-icon.component.scss']
})
export class MatAnimatedIconComponent implements OnInit {

  @Input() start: String;
  @Input() end: String;
  @Input() colorStart: String;
  @Input() colorEnd: String;
  @Input() animate: boolean;
  @Input() animateFromParent?: boolean = false;

  constructor() { }

  ngOnInit() {
    console.log(this.colorStart);
    console.log(this.colorEnd);
  }

  toggle() {
    if(!this.animateFromParent) this.animate = !this.animate;
  }

}

mat-animated-icon.component.scss

:host {
  font-family: 'Material Icons';
  font-weight: normal;
  font-style: normal;
  font-size: 24px;  /* Preferred icon size */
  display: inline-block;
  line-height: 1;
  text-transform: none;
  letter-spacing: normal;
  word-wrap: normal;
  white-space: nowrap;
  direction: ltr;

  /* Support for all WebKit browsers. */
  -webkit-font-smoothing: antialiased;
  /* Support for Safari and Chrome. */
  text-rendering: optimizeLegibility;

  /* Support for Firefox. */
  -moz-osx-font-smoothing: grayscale;

  /* Support for IE. */
  font-feature-settings: 'liga';

  /* Rules for sizing the icon. */
  &.md-18 { font-size: 18px; }
  &.md-24 { font-size: 24px; }
  &.md-36 { font-size: 36px; }
  &.md-48 { font-size: 48px; }

  /* Rules for using icons as black on a light background. */
  &.md-dark { 
    color: rgba(0, 0, 0, 0.54);

    &.md-inactive { color: rgba(0, 0, 0, 0.26); }
  }

  /* Rules for using icons as white on a dark background. */
  &.md-light { 
    color: rgba(255, 255, 255, 1);

    &.md-inactive { color: rgba(255, 255, 255, 0.3); }
  }

  .material-icons {
    transition: transform .5s;
    &.animate {
      transform: rotate(360deg);
    }
  }
}

mat-animated-icon.component.html

<mat-icon [ngClass]="{'animate' : animate}" color="{{animate ? colorEnd : colorStart}}" (click)="toggle()">{{animate ? end : start}}</mat-icon>

var.directive.ts

一个小帮助指令

import { Directive, Input } from '@angular/core';

@Directive({
  selector: '[var]',
  exportAs: 'var'
})
export class VarDirective {

  @Input() var:any;

  constructor() { }

}

组件使用示例

<button (click)="!this.disabled && iconAnimate10.var=!iconAnimate10.var" #iconAnimate10="var" var="'false'" mat-icon-button [disabled]="false" aria-label="Example icon-button with a heart icon">
<mat-animated-icon start="menu" end="close" colorStart="none" colorEnd="none" [animate]="iconAnimate10.var" animateFromParent="true"></mat-animated-icon>

2
你能帮忙修复 Stackblitz 中的错误吗?我想从中学习,但是我无法让它正常运行!每次启动服务器时,我都会收到 Error: ENOENT: No such file or directory., '/dev/null' 的错误提示。 - Valentine Bondar

7

有一个简单的库可以帮助动画 Angular。

https://github.com/filipows/angular-animations

我刚使用它来为 Angular 8 动画喜爱的图标,非常直观易懂。

这个例子将满星变成空星,反之亦然。

组合:

import { fadeInOnEnterAnimation, fadeOutOnLeaveAnimation } from 'angular-animations';
@Component({animations: [
    fadeInOnEnterAnimation(),
    fadeOutOnLeaveAnimation()
]})

public toggleFavorite() {
    this.isFavorite = !this.isFavorite;
}

html:

 <div style="display: grid;" id="favoriteContainer" (click)=toggleFavorite() matTooltip="Favorite" >
              <mat-icon style="grid-column: 1;grid-row: 1;" *ngIf="!isFavorite" [@fadeInOnEnter] [@fadeOutOnLeave]>star_border</mat-icon>
              <mat-icon style="grid-column: 1;grid-row: 1;" *ngIf="isFavorite" [@fadeInOnEnter] [@fadeOutOnLeave]>star</mat-icon>
          </div>

1
你可以通过使用图标来实现。实现一个包含图标数组的组件,然后定期交换图标。每个图标代表一个状态/图片。
例如:在一个数组中使用以下图标,然后每隔100毫秒交换一次。

更新:

请参考在Angular中使用Animate Font Awesome图标文章。

从上述链接https://stackblitz.com/edit/animated-icons-angular-forked进行分叉。


1

在@Remy的帮助下,我完成了一个可行的示例

  • 首先安装此包npm i angular-animations --save
  • 然后在您的父级模块中导入BrowserAnimationsModule
<mat-icon matListIcon class="menu-item-icon" *ngIf="themeService.isDark();" [@fadeInOnEnter]>dark_mode</mat-icon>
<mat-icon matListIcon class="menu-item-icon" *ngIf="themeService.isLight();" [@fadeInOnEnter]>light_mode</mat-icon>
<mat-slide-toggle [checked]="themeService.isDark()" (change)="$event.checked ? setDarkTheme() : setLightTheme()"></mat-slide-toggle>

import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { fadeInOnEnterAnimation, fadeOutOnLeaveAnimation } from 'angular-animations';

@Component({
  selector: 'app-user-menu',
  templateUrl: './user-menu.component.html',
  styleUrls: ['./user-menu.component.scss'],
  animations: [
    fadeInOnEnterAnimation(),
  ]
})
export class UserMenuComponent implements OnInit, OnDestroy {

 constructor() {}

}

enter image description here


0

我知道这是一个比较老的问题,但我相信人们仍在苦苦挣扎。

让我与您分享我用了一段时间的几个文件。我很多材料都是从 StackOverflow 的另一个答案中找到的,几年前我把自己的特性添加到了这些文件中。

这是我开始使用并开始修改的 StackBlitz。我认为作者一直在添加东西,因为自从我上次看到它以来,它变得更加强大了。所以在您使下面的代码工作后,请查看他的内容!

实质上,您需要创建一个指令,然后可以在 HTML 元素上使用它。不过要注意,如果您在同一元素上使用具有 HTML 元素引用的其他指令,则需要将任何要尝试动画显示的内容包装在 span 中,或者只找出一种方式仅使用一个元素引用。需要使用包装器的一个很好的例子是触发 matMenu 的 matBadge 或 button。

我已经使用这个动画系统在页面滚动时触发动画,以及反复触发它们来显示后台任务的加载等。

您需要的 2 个文件(aos-component.ts):

import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, HostBinding, HostListener, ElementRef, NgZone } from '@angular/core';
import { Subject, Observable, of } from 'rxjs';
import { map, startWith, distinctUntilChanged, delay, scan, takeUntil, takeWhile, flatMap } from 'rxjs/operators';
import {$animations} from './aos-animations';
import {ScrollDispatcher} from '@angular/cdk/overlay';
import {coerceBooleanProperty} from '@angular/cdk/coercion';

export type wmAnimations = 'landing'|'pulse'|'beat'|'heartBeat'|'fadeIn'|'fadeInAndOut'|'fadeInRight'|'fadeInLeft'|'fadeInUp'|'fadeInDown'|'zoomIn'|'fadeOut'|'fadeOutRight'|'fadeOutLeft'|'fadeOutDown'|'fadeOutUp'|'zoomOut'|'flyingStagger';
export type wmAnimateSpeed = 'slower'|'slow'|'normal'|'fast'|'faster';

export class wmRect {
  constructor(readonly left: number, readonly top: number, readonly right: number, readonly bottom: number) {}
  get width(): number { return this.right - this.left; }
  get height(): number { return this.bottom - this.top; }
};

@Component({
  selector: '[wmAnimate]',
  template: '<ng-content></ng-content>',
  animations: $animations
})
export class AnimateComponent implements OnInit, OnDestroy {

  readonly timings = { slower: '3s', slow: '2s', normal: '1s', fast: '500ms', faster: '300ms' };
  public  replay$ = new Subject<boolean>();
  public  dispose$ = new Subject<void>();

  constructor(public elm: ElementRef, public scroll: ScrollDispatcher, public zone: NgZone) {}

  public get idle() { return { value: 'idle' }; }
  public get play() {
    return {
      value: this.animate,
      //delay: this.delay,
      params: {
        timing: this.timings[this.speed] || '1s',
        stagger: this.stagger
      }
    };
  }

  /** Selects the animation to be played */
  @Input('wmAnimate') animate: wmAnimations;

  /** Speeds up or slows down the animation */
  @Input() speed: wmAnimateSpeed = 'normal';

  /**Specifies number of elements to stagger animation */
  @Input() stagger: number = 0;

  @HostBinding('@animate')
  public trigger: string | {} = 'idle';

  /** Disables the animation */
  @Input('disabled') set disableAnimation(value: boolean) { this.disabled = coerceBooleanProperty(value); }
  @HostBinding('@.disabled')
  public disabled = false;

  /** Emits at the end of the animation */
  @Output() start = new EventEmitter<void>();
  @HostListener('@animate.start') public animationStart() { this.start.emit(); }

  /** Emits at the end of the animation */
  @Output() done = new EventEmitter<void>();
  @HostListener('@animate.done') public animationDone() { this.done.emit(); }

  /** When true, keeps the animation idle until the next replay triggers */
  @Input('paused') set pauseAnimation(value: boolean) { this.paused = coerceBooleanProperty(value); }
  public paused: boolean = false;

  /** When true, triggers the animation on element scrolling in the viewport */
  @Input('aos') set enableAOS(value: boolean) { this.aos = coerceBooleanProperty(value); }
  public aos: boolean = false;

  /** When true, triggers the animation on element scrolling in the viewport */
  @Input('once') set aosOnce(value: boolean) { this.once = coerceBooleanProperty(value); }
  public once: boolean = false;

  /** Specifies the amount of visibility triggering AOS */
  @Input() threshold: number = 0.2;

  /** If set to true, this will replay the animation indefinitely. Useful for loading/bg tasks*/
  @Input() always: boolean = false;

  /** Replays the animation */
  @Input() set replay(replay: any) {
    if(this.always){
      setInterval(() => {
        //We hardcoded 4 seconds in here, and 2 seconds in aos-animations.ts for use in only one location.
        // We should pass inputs here and make multiple animations in our animations file
        this.trigger = this.idle;
        this.replay$.next(true);
      }, 4000)
    } else {
      if(this.trigger === 'idle') { return; }

      // Re-triggers the animation again on request
      if(coerceBooleanProperty(replay)) {

        this.trigger = this.idle;
        this.replay$.next(true);
      }
    }

  }

  ngOnInit() {

    // Triggers the animation based on the input flags
    this.animateTrigger(this.elm).subscribe( trigger => {
      // Triggers the animation to play or to idle
      if (this.stagger > 0){
        for(let i = 1; i <= this.stagger; i++){
          console.log('fire staggering');
          this.trigger = trigger ? this.play : this.idle;
        }
      } else {
        this.trigger = trigger ? this.play : this.idle;
      }
    });
  }

  ngOnDestroy() { this.dispose(); }

  public dispose() {
    this.dispose$.next();
    this.dispose$.complete();
  }

  // Triggers the animation
  public animateTrigger(elm: ElementRef<HTMLElement>): Observable<boolean> {

    return this.animateReplay().pipe( flatMap( trigger => this.aos ? this.animateOnScroll(elm) : of(trigger)) );
  }

  // Triggers the animation deferred
  public animateReplay(): Observable<boolean> {

    return this.replay$.pipe( takeUntil(this.dispose$), delay(0), startWith(!this.paused) );
  }

  // Triggers the animation on scroll
  public animateOnScroll(elm: ElementRef<HTMLElement>): Observable<boolean> {

    // Returns an AOS observable
    return this.scroll.ancestorScrolled(elm, 100).pipe(
      // Makes sure to dispose on destroy
      takeUntil(this.dispose$),
      // Starts with initial element visibility
      startWith(!this.paused  && this.visibility >= this.threshold),
      // Maps the scrolling to the element visibility value
      map(() => this.visibility),
      // Applies an hysteresys, so, to trigger the animation on based on the treshold while off on full invisibility
      scan<number,boolean>((result, visiblility) => (visiblility >= this.threshold || (result ? visiblility > 0 : false))),
      // Distincts the resulting triggers
      distinctUntilChanged(),
      // Stop taking the first on trigger when aosOnce is set
      takeWhile(trigger => !trigger || !this.once, true),
      // Run NEXT within the angular zone to trigger change detection back on
      flatMap(trigger => new Observable<boolean>(observer => this.zone.run(() => observer.next(trigger))))
    );
  }

  // Computes the element visibility ratio
  public get visibility() {
    return this.intersectRatio( this.clientRect(this.elm), this.getScrollingArea(this.elm) );
  }

  public intersectRatio(rect: wmRect, cont: wmRect): number {

    // Return 1.0 when the element is fully within its scroller container
    if(rect.left > cont.left && rect.top > cont.top && rect.right < cont.right && rect.bottom < cont.bottom) {
      return 1.0;
    }

    // Computes the intersection area otherwise
    const a = Math.round(rect.width * rect.height);
    const b = Math.max(0, Math.min(rect.right, cont.right) - Math.max(rect.left, cont.left));
    const c = Math.max(0, Math.min(rect.bottom, cont.bottom) - Math.max(rect.top, cont.top));

    // Returns the amount of visible area
    return Math.round(b * c / a * 10) / 10;
  }

  // Returns the rectangular surface area of the element's scrolling container
  public getScrollingArea(elm: ElementRef<HTMLElement>): wmRect {
    // Gets the cdkScolling container, if any
    const scroller = this.scroll.getAncestorScrollContainers(elm).pop();
    // Returns the element's most likely scrolling container area
    return !!scroller ? this.clientRect( scroller.getElementRef() ) : this.windowRect();
  }

  // Element client bounding rect helper
  public clientRect(elm: ElementRef<HTMLElement>): wmRect {
    const el = !!elm && elm.nativeElement;
    return !!el && el.getBoundingClientRect();
  }

  public windowRect(): wmRect {
    return new wmRect(0,0, window.innerWidth, window.innerHeight);
  }

}

还有另一个文件 (aos-animations.ts):

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

export const $animations = [

  trigger('animate', [

    state('idle', style({ opacity: 0 }) ),

    transition('* => landing', [
      style({
        transform: 'scale(1.2)',
        opacity: 0
      }),
      animate('{{timing}} ease', style('*'))
    ], { params: { timing: '2s'}}),

    transition('* => pulse', [
      style('*'),
      animate('{{timing}} ease-in-out',
        keyframes([
          style({ transform: 'scale(1)' }),
          style({ transform: 'scale(1.05)' }),
          style({ transform: 'scale(1)' })
        ])
      )], { params: { timing: '1s'}}
    ),

    transition('* => beat', [
      style('*'),
      animate('{{timing}} cubic-bezier(.8, -0.6, 0.2, 1.5)',
        keyframes([
          style({ transform: 'scale(0.8)' }),
          style({ transform: 'scale(1.5)' }),
          style({ transform: 'scale(1)' })
        ])
      )], { params: { timing: '500ms'}}
    ),

    transition('* => heartBeat', [
      style('*'),
      animate('{{timing}} ease-in-out',
        keyframes([
          style({ transform: 'scale(1)', offset: 0 }),
          style({ transform: 'scale(1.3)', offset: 0.14 }),
          style({ transform: 'scale(1)', offset: 0.28 }),
          style({ transform: 'scale(1.3)', offset: 0.42 }),
          style({ transform: 'scale(1)', offset: 0.70 })
        ])
      )], { params: { timing: '1s'}}
    ),

    transition('* => fadeIn', [
      style({ opacity: 0 }),
      animate('{{timing}} ease-in', style('*'))
    ], { params: { timing: '1s'}}),

    transition('* => fadeInAndOut', [
      style({ opacity: 0 }),
      animate('{{timing}} ease-in', style('*')),
      animate('{{timing}} ease-in', style({ opacity: 0 }))
    ], { params: { timing: '2s'}}),

    transition('* => fadeInRight', [
      style({ opacity: 0, transform: 'translateX(-20px)' }),
      animate('{{timing}} ease-in', style('*'))
    ], { params: { timing: '1s'}}),

    transition('* => fadeInLeft', [
      style({ opacity: 0, transform: 'translateX(20px)' }),
      animate('{{timing}} ease-in', style('*'))
    ], { params: { timing: '1s'}}),

    transition('* => fadeInUp', [
      style({ opacity: 0, transform: 'translateY(20px)' }),
      animate('{{timing}} ease-in', style('*'))
    ], { params: { timing: '1s'}}),

    transition('* => fadeInDown', [
      style({ opacity: 0, transform: 'translateY(-20px)' }),
      animate('{{timing}} ease-in', style('*'))
    ], { params: { timing: '1s'}}),

    transition('* => zoomIn',
      animate('{{timing}} ease-in',
        keyframes([
          style({ opacity: 0, transform: 'scale(0.3)' }),
          style({ opacity: 1, transform: 'scale(0.65)' }),
          style({ opacity: 1, transform: 'scale(1)' })
        ])
      ), { params: { timing: '1s'}}
    ),

    transition('* => bumpIn', [
      style({ transform: 'scale(0.5)', opacity: 0 }),
      animate("{{timing}} cubic-bezier(.8, -0.6, 0.2, 1.5)",
        style({ transform: 'scale(1)', opacity: 1 }))
    ], { params: { timing: '500ms'}}),

    transition('* => flyingStagger', [
      // query(':enter', style({ opacity: 0 }), { optional: true }),
      query('.logos', [
        stagger(500, [
          animate('{{timing}} ease-in', keyframes([
            style({ opacity: 0, transform: 'translateY(-50%)', offset: 0 }),
            style({ opacity: .5, transform: 'translateY(-10px) scale(1.1)', offset: 0.3 }),
            style({ opacity: 1, transform: 'translateY(0)', offset: 1 }),
          ]))
        ])
      ])
    ], { params: { timing: '1s'}}),

    transition('fadeOut => void', [
      animate('{{timing}} ease-in', style({ opacity: 0 }))
    ]),

    transition('fadeOutRight => void', [
      animate('{{timing}} ease-in', style({ opacity: 0, transform: 'translateX(20px)' }))
    ], { params: { timing: '1s'}}),

    transition('fadeOutLeft => void', [
      animate('{{timing}} ease-in', style({ opacity: 0, transform: 'translateX(-20px)' }))
    ], { params: { timing: '1s'}}),

    transition('fadeOutDown => void', [
      animate('{{timing}} ease-in', style({ opacity: 0, transform: 'translateY(20px)' }))
    ], { params: { timing: '1s'}}),

    transition('fadeOutUp => void', [
      animate('{{timing}} ease-in', style({ opacity: 0, transform: 'translateY(-20px)' }))
    ], { params: { timing: '1s'}}),

    transition('zoomOut => void',
      animate('{{timing}} ease-in',
        keyframes([
          style({ opacity: 1, transform: 'scale(1)' }),
          style({ opacity: 0, transform: 'scale(0.3)' }),
          style({ opacity: 0, transform: 'scale(0.3)' })
        ])
      ), { params: { timing: '1s'}}
    ),
  ])
];

你还需要cdk库。请查看上面组件文件中的导入。同时确保你也从@angular/platform-browser中拥有BrowserAnimationsModule。

最后,这是我在我们应用程序的某个部分中使用它的示例:

<span *ngIf="(loaderCount$ | async) > 0" class="bgLoad" wmAnimate="fadeInAndOut" speed="slow" replay="true" always="true"><mat-icon>cloud_download</mat-icon></span>

我只是记录后台API请求和下载图标,当1个或多个请求仍在等待时。

我已经使用了同样的库来在页面上首次可见时动画化元素,如下所示:

<div wmAnimate="landing" speed="normal" class="centerVertH head" aos once *ifIsBrowser>my content...</div>

如果我想让它每次可见时都触发,那么我会从该div中删除'once'。
查看动画组件文件中有关输入的文档。您可以根据需要进行更多设置。拥有良好动画的背景是存在的。尝试一下。这将帮助您了解过渡和动画的基本知识。
希望这可以帮助您。

0

material.io 是关于如何制作 Material Design 的规范和指南,Angular Material 组件是基于这种规范构建的,但它们没有显示有关动画 Google Material 图标的任何信息。


3
问题在于,没有清晰的指南或辅助工具来制作图标动画。 - Amirreza

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接