Skip to content

Commit 312f3b6

Browse files
committed
feat(animations): introduce NativeScriptAnimationsModule (#704)
BREAKING CHANGE: To use animations, you need to import the NativeScriptAnimationsModule from "nativescript-angular/animations" in your root NgModule.
1 parent bb13bff commit 312f3b6

15 files changed

+646
-71
lines changed

Diff for: nativescript-angular/animations.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { NgModule, Injectable, NgZone, Provider, RendererFactory2 } from "@angular/core";
2+
3+
import {
4+
AnimationDriver,
5+
ɵAnimationEngine as AnimationEngine,
6+
ɵAnimationStyleNormalizer as AnimationStyleNormalizer,
7+
ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer
8+
} from "@angular/animations/browser";
9+
10+
import { ɵAnimationRendererFactory as AnimationRendererFactory } from "@angular/platform-browser/animations";
11+
12+
import { NativeScriptAnimationEngine } from "./animations/animation-engine";
13+
import { NativeScriptAnimationDriver } from "./animations/animation-driver";
14+
import { NativeScriptModule } from "./nativescript.module";
15+
import { NativeScriptRendererFactory } from "./renderer";
16+
17+
@Injectable()
18+
export class InjectableAnimationEngine extends NativeScriptAnimationEngine {
19+
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
20+
super(driver, normalizer);
21+
}
22+
}
23+
24+
export function instantiateSupportedAnimationDriver() {
25+
return new NativeScriptAnimationDriver();
26+
}
27+
28+
export function instantiateRendererFactory(
29+
renderer: NativeScriptRendererFactory, engine: AnimationEngine, zone: NgZone) {
30+
return new AnimationRendererFactory(renderer, engine, zone);
31+
}
32+
33+
export function instanciateDefaultStyleNormalizer() {
34+
return new WebAnimationsStyleNormalizer();
35+
}
36+
37+
export const NATIVESCRIPT_ANIMATIONS_PROVIDERS: Provider[] = [
38+
{provide: AnimationDriver, useFactory: instantiateSupportedAnimationDriver},
39+
{provide: AnimationStyleNormalizer, useFactory: instanciateDefaultStyleNormalizer},
40+
{provide: AnimationEngine, useClass: InjectableAnimationEngine}, {
41+
provide: RendererFactory2,
42+
useFactory: instantiateRendererFactory,
43+
deps: [NativeScriptRendererFactory, AnimationEngine, NgZone]
44+
}
45+
];
46+
47+
@NgModule({
48+
imports: [NativeScriptModule],
49+
providers: NATIVESCRIPT_ANIMATIONS_PROVIDERS,
50+
})
51+
export class NativeScriptAnimationsModule {
52+
}

Diff for: nativescript-angular/animations/animation-driver.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { AnimationPlayer } from "@angular/animations";
2+
import { NgView } from "../element-registry";
3+
4+
import { NativeScriptAnimationPlayer } from "./animation-player";
5+
import { Keyframe } from "./utils";
6+
7+
export abstract class AnimationDriver {
8+
abstract animate(
9+
element: any,
10+
keyframes: Keyframe[],
11+
duration: number,
12+
delay: number,
13+
easing: string
14+
): AnimationPlayer;
15+
}
16+
17+
export class NativeScriptAnimationDriver implements AnimationDriver {
18+
computeStyle(element: NgView, prop: string): string {
19+
return element.style[`css-${prop}`];
20+
}
21+
22+
animate(
23+
element: NgView,
24+
keyframes: Keyframe[],
25+
duration: number,
26+
delay: number,
27+
easing: string
28+
): AnimationPlayer {
29+
return new NativeScriptAnimationPlayer(
30+
element, keyframes, duration, delay, easing);
31+
}
32+
}

Diff for: nativescript-angular/animations/animation-engine.ts

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { ɵDomAnimationEngine as DomAnimationEngine } from "@angular/animations/browser";
2+
import { AnimationEvent, AnimationPlayer } from "@angular/animations";
3+
4+
import { NgView } from "../element-registry";
5+
import {
6+
copyArray,
7+
cssClasses,
8+
deleteFromArrayMap,
9+
eraseStylesOverride,
10+
getOrSetAsInMap,
11+
makeAnimationEvent,
12+
optimizeGroupPlayer,
13+
setStyles,
14+
} from "./dom-utils";
15+
16+
const MARKED_FOR_ANIMATION = "ng-animate";
17+
18+
interface QueuedAnimationTransitionTuple {
19+
element: NgView;
20+
player: AnimationPlayer;
21+
triggerName: string;
22+
event: AnimationEvent;
23+
}
24+
25+
// we are extending Angular's animation engine and
26+
// overriding a few methods that work on the DOM
27+
export class NativeScriptAnimationEngine extends DomAnimationEngine {
28+
// this method is almost completely copied from
29+
// the original animation engine, just replaced
30+
// a few method invocations with overriden ones
31+
animateTransition(element: NgView, instruction: any): AnimationPlayer {
32+
const triggerName = instruction.triggerName;
33+
34+
let previousPlayers: AnimationPlayer[];
35+
if (instruction.isRemovalTransition) {
36+
previousPlayers = this._onRemovalTransitionOverride(element);
37+
} else {
38+
previousPlayers = [];
39+
const existingTransitions = this._getTransitionAnimation(element);
40+
const existingPlayer = existingTransitions ? existingTransitions[triggerName] : null;
41+
if (existingPlayer) {
42+
previousPlayers.push(existingPlayer);
43+
}
44+
}
45+
46+
// it's important to do this step before destroying the players
47+
// so that the onDone callback below won"t fire before this
48+
eraseStylesOverride(element, instruction.fromStyles);
49+
50+
// we first run this so that the previous animation player
51+
// data can be passed into the successive animation players
52+
let totalTime = 0;
53+
const players = instruction.timelines.map(timelineInstruction => {
54+
totalTime = Math.max(totalTime, timelineInstruction.totalTime);
55+
return (<any>this)._buildPlayer(element, timelineInstruction, previousPlayers);
56+
});
57+
58+
previousPlayers.forEach(previousPlayer => previousPlayer.destroy());
59+
const player = optimizeGroupPlayer(players);
60+
player.onDone(() => {
61+
player.destroy();
62+
const elmTransitionMap = this._getTransitionAnimation(element);
63+
if (elmTransitionMap) {
64+
delete elmTransitionMap[triggerName];
65+
if (Object.keys(elmTransitionMap).length === 0) {
66+
(<any>this)._activeTransitionAnimations.delete(element);
67+
}
68+
}
69+
deleteFromArrayMap((<any>this)._activeElementAnimations, element, player);
70+
setStyles(element, instruction.toStyles);
71+
});
72+
73+
const elmTransitionMap = getOrSetAsInMap((<any>this)._activeTransitionAnimations, element, {});
74+
elmTransitionMap[triggerName] = player;
75+
76+
this._queuePlayerOverride(
77+
element, triggerName, player,
78+
makeAnimationEvent(
79+
element, triggerName, instruction.fromState, instruction.toState,
80+
null, // this will be filled in during event creation
81+
totalTime));
82+
83+
return player;
84+
}
85+
86+
// overriden to use eachChild method of View
87+
// instead of DOM querySelectorAll
88+
private _onRemovalTransitionOverride(element: NgView): AnimationPlayer[] {
89+
// when a parent animation is set to trigger a removal we want to
90+
// find all of the children that are currently animating and clear
91+
// them out by destroying each of them.
92+
let elms = [];
93+
element.eachChild(child => {
94+
if (cssClasses(<NgView>child).get(MARKED_FOR_ANIMATION)) {
95+
elms.push(child);
96+
}
97+
98+
return true;
99+
});
100+
101+
for (let i = 0; i < elms.length; i++) {
102+
const elm = elms[i];
103+
const activePlayers = this._getElementAnimation(elm);
104+
if (activePlayers) {
105+
activePlayers.forEach(player => player.destroy());
106+
}
107+
108+
const activeTransitions = this._getTransitionAnimation(elm);
109+
if (activeTransitions) {
110+
Object.keys(activeTransitions).forEach(triggerName => {
111+
const player = activeTransitions[triggerName];
112+
if (player) {
113+
player.destroy();
114+
}
115+
});
116+
}
117+
}
118+
119+
// we make a copy of the array because the actual source array is modified
120+
// each time a player is finished/destroyed (the forEach loop would fail otherwise)
121+
return copyArray(this._getElementAnimation(element));
122+
}
123+
124+
// overriden to use cssClasses method to access native element's styles
125+
// instead of DOM element's classList
126+
private _queuePlayerOverride(
127+
element: NgView, triggerName: string, player: AnimationPlayer, event: AnimationEvent) {
128+
const tuple = <QueuedAnimationTransitionTuple>{ element, player, triggerName, event };
129+
(<any>this)._queuedTransitionAnimations.push(tuple);
130+
player.init();
131+
132+
cssClasses(element).set(MARKED_FOR_ANIMATION, true);
133+
player.onDone(() => cssClasses(element).set(MARKED_FOR_ANIMATION, false));
134+
}
135+
136+
private _getElementAnimation(element: NgView) {
137+
return (<any>this)._activeElementAnimations.get(element);
138+
}
139+
140+
private _getTransitionAnimation(element: NgView) {
141+
return (<any>this)._activeTransitionAnimations.get(element);
142+
}
143+
}

Diff for: nativescript-angular/animations/animation-player.ts

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { AnimationPlayer } from "@angular/animations";
2+
import {
3+
KeyframeAnimation,
4+
KeyframeAnimationInfo,
5+
} from "tns-core-modules/ui/animation/keyframe-animation";
6+
7+
import { NgView } from "../element-registry";
8+
import { Keyframe, getAnimationCurve, parseAnimationKeyframe } from "./utils";
9+
10+
export class NativeScriptAnimationPlayer implements AnimationPlayer {
11+
public parentPlayer: AnimationPlayer = null;
12+
13+
private _startSubscriptions: Function[] = [];
14+
private _doneSubscriptions: Function[] = [];
15+
private _finished = false;
16+
private _started = false;
17+
private animation: KeyframeAnimation;
18+
19+
constructor(
20+
private target: NgView,
21+
keyframes: Keyframe[],
22+
duration: number,
23+
delay: number,
24+
easing: string
25+
) {
26+
this.initKeyframeAnimation(keyframes, duration, delay, easing);
27+
}
28+
29+
init(): void {
30+
}
31+
32+
hasStarted(): boolean {
33+
return this._started;
34+
}
35+
36+
onStart(fn: Function): void { this._startSubscriptions.push(fn); }
37+
onDone(fn: Function): void { this._doneSubscriptions.push(fn); }
38+
onDestroy(fn: Function): void { this._doneSubscriptions.push(fn); }
39+
40+
play(): void {
41+
if (!this.animation) {
42+
return;
43+
}
44+
45+
if (!this._started) {
46+
this._started = true;
47+
this._startSubscriptions.forEach(fn => fn());
48+
this._startSubscriptions = [];
49+
}
50+
51+
this.animation.play(this.target)
52+
.then(() => this.onFinish())
53+
.catch((_e) => { });
54+
}
55+
56+
pause(): void {
57+
throw new Error("AnimationPlayer.pause method is not supported!");
58+
}
59+
60+
finish(): void {
61+
throw new Error("AnimationPlayer.finish method is not supported!");
62+
}
63+
64+
reset(): void {
65+
if (this.animation && this.animation.isPlaying) {
66+
this.animation.cancel();
67+
}
68+
}
69+
70+
restart(): void {
71+
this.reset();
72+
this.play();
73+
}
74+
75+
destroy(): void {
76+
this.reset();
77+
this.onFinish();
78+
}
79+
80+
setPosition(_p: any): void {
81+
throw new Error("AnimationPlayer.setPosition method is not supported!");
82+
}
83+
84+
getPosition(): number {
85+
return 0;
86+
}
87+
88+
private initKeyframeAnimation(keyframes: Keyframe[], duration: number, delay: number, easing: string) {
89+
let info = new KeyframeAnimationInfo();
90+
info.isForwards = true;
91+
info.iterations = 1;
92+
info.duration = duration === 0 ? 0.01 : duration;
93+
info.delay = delay;
94+
info.curve = getAnimationCurve(easing);
95+
info.keyframes = keyframes.map(parseAnimationKeyframe);
96+
97+
this.animation = KeyframeAnimation.keyframeAnimationFromInfo(info);
98+
}
99+
100+
private onFinish() {
101+
if (!this._finished) {
102+
this._finished = true;
103+
this._started = false;
104+
this._doneSubscriptions.forEach(fn => fn());
105+
this._doneSubscriptions = [];
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)