How to use/enable animated icons?
Asked Answered
I

7

28

Does anybody know how to use/enable the animated icons in an Angular Web Application which are shown in the material design documentation: https://material.io/design/iconography/animated-icons.html#usage

Ion answered 13/7, 2018 at 12:43 Comment(1)
Those animations are examples and not part of the specification. They're just showing you what you could do yourself.Outbreak
T
15

As other have stated the examples on the Material Icon sites would have to be built.

However, I found my way to this question looking for a guide on how to animate angular material icons and for others looking for the same I have a solution. The default animation can be customized to something other than just a 360 degree rotation.

Basically you can create a component that swaps between mat-icon's when clicked or when a parent element like a button is clicked.

Prerequisites are you have a an angular material application with material icons installed. I used Angular Material 8.

Here is a working 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

a little helper directive

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

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

  @Input() var:any;

  constructor() { }

}

Example of the component in use

<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>

Tobietobin answered 11/7, 2019 at 19:28 Comment(1)
Could you help fix the error in your stackblitz, I want to learn from it, but I can't get it to go! I keep getting Error: ENOENT: No such file or directory., '/dev/null' when it starts up the serverSubmerged
H
7

There is library that easy animate angular. https://github.com/filipows/angular-animations

I just used that on angular 8 to animate favorite icons, it's very straightforward.

This example makes full star into empty star and vice versa.

Compount:

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>
Homily answered 4/11, 2019 at 12:22 Comment(0)
F
1

You could implement through a component using icons. Implement a component which contains array for icons then swap the icons regular interval. Each icon represent a state/image.

For eg : Use following icons in an array then swap it every every 100ms.

Update:

Refer to Animate Font Awesome icons in Angular article.

Forked from above https://stackblitz.com/edit/animated-icons-angular-forked

Facula answered 13/7, 2018 at 13:21 Comment(0)
B
1

With the help of @Remy, I done a working example

  • first install this package npm i angular-animations --save
  • then import the BrowserAnimationsModule inside your parent module imports
Html file
<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>
TS file
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() {}

}

Output

enter image description here

Barely answered 9/4, 2021 at 13:35 Comment(0)
B
0

material.io is specification and guid how to make material design ,angular material component is build on this kind of specification but the don't show any information about animate google material icon.

Bradytelic answered 13/7, 2018 at 13:6 Comment(1)
and that's the problem, there is no clean guide line or helping tools to animate iconsMisology
L
0

I know this is an older question, but I am sure people are still struggling with it.

Let me share with you a couple of files I have used for a while. I found a lot of this material from another answer on StackOverflow years ago, I have added my own features into these files.

Here is the StackBlitz that I started with and started to modify. I think the author has steadily been adding things, because this looks way beefed up since I have seen it last. So check out his stuff after you get the below code working!

Essentially you need to make a directive that you can use on html elements. I caution you though, if you are using an additional directive on the same element that has an HTML Element Ref, you will need to wrap whatever you are trying to animate in a span, or just figure out a way to only have one element ref. A good example of when you would need a wrapper would be a matBadge or button that triggers a matMenu.

I have used this animation system to fire animations on page scrolling, as well as firing them repeatedly to show background tasks loading/ etc.

The 2 files you'll need (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);
  }

}

And the other one (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'}}
    ),
  ])
];

You will need the cdk library as well. Look at the imports in the component file above. Make sure you have BrowserAnimationsModule as well from @angular/platform-browser

Lastly, here is an example of how I am using it in one part of our app:

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

Where I am just logging background API requests and a download icon when 1 or more requests are still pending.

I have used this same library to animate an element when it first becomes visible on the page like so:

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

If I wanted it to fire every time it became visible, then I would remove 'once' from that div.

Look at the documentation around the inputs in the animations component file. You can make some more to suit your needs. The background of having good animations is there. Mess around with it. It will help you understand transitions and animations in general.

I hope this helps you.

Larynx answered 13/7, 2022 at 21:18 Comment(0)
V
0

Ok, so I was also looking to animate icons. I came up with a simple CSS solution. First I wrap the icon in a small container:

<span class="icon-container">
    @if(action.status === "wait"){
      <mat-icon class="animate-rotate">hourglass_empty</mat-icon>
    } @else {
      <mat-icon class="animate-horizontal">fast_forward</mat-icon>
    }
      </span>

Then using the classes I start an animation in CSS:

.icon-container {
  width: 2em;
  overflow: clip;
  display: inline-block;
  border: 1px solid darkblue
}
     
    .animate-horizontal {
      animation-name: AnimateMoveOutRight;
      animation-duration: 2s;
      animation-iteration-count: infinite;
      animation-direction: normal;
    }
    
    .animate-rotate {
      animation-name: AnimateRotate;
      animation-duration: 2s;
      animation-iteration-count: infinite;
      animation-direction: normal;
    }
    
    @keyframes AnimateMoveOutRight {
      0% {
        transform: translateX(0);
      }
      100%{
        transform: translateX(2em);
      }
    }
    
    @keyframes AnimateRotate {
      0% {
        transform: rotate(0);
      }
      100%{
        transform: rotate(180deg);
      }
    }

Working example on Stackblitz

Vendetta answered 25/4 at 17:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.