有人知道如何在Angular Web应用程序中使用/启用动画图标吗?这些图标在材料设计文档中显示:https://material.io/design/iconography/animated-icons.html#usage
有人知道如何在Angular Web应用程序中使用/启用动画图标吗?这些图标在材料设计文档中显示:https://material.io/design/iconography/animated-icons.html#usage
如其他人所述,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>
Error: ENOENT: No such file or directory., '/dev/null'
的错误提示。 - Valentine Bondar有一个简单的库可以帮助动画 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>
更新:
请参考在Angular中使用Animate Font Awesome图标文章。
从上述链接https://stackblitz.com/edit/animated-icons-angular-forked进行分叉。
在@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() {}
}
我知道这是一个比较老的问题,但我相信人们仍在苦苦挣扎。
让我与您分享我用了一段时间的几个文件。我很多材料都是从 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>
material.io 是关于如何制作 Material Design 的规范和指南,Angular Material 组件是基于这种规范构建的,但它们没有显示有关动画 Google Material 图标的任何信息。