Skip to content

Commit

Permalink
feat(animations): introduce NativeScriptAnimationsModule (#704)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: To use animations, you need to import the
NativeScriptAnimationsModule from "nativescript-angular/animations" in
your root NgModule.
  • Loading branch information
sis0k0 authored and hdeshev committed Mar 28, 2017
1 parent 777046b commit f9ad6a5
Show file tree
Hide file tree
Showing 15 changed files with 646 additions and 71 deletions.
52 changes: 52 additions & 0 deletions nativescript-angular/animations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { NgModule, Injectable, NgZone, Provider, RendererFactory2 } from "@angular/core";

import {
AnimationDriver,
ɵAnimationEngine as AnimationEngine,
ɵAnimationStyleNormalizer as AnimationStyleNormalizer,
ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer
} from "@angular/animations/browser";

import { ɵAnimationRendererFactory as AnimationRendererFactory } from "@angular/platform-browser/animations";

import { NativeScriptAnimationEngine } from "./animations/animation-engine";
import { NativeScriptAnimationDriver } from "./animations/animation-driver";
import { NativeScriptModule } from "./nativescript.module";
import { NativeScriptRendererFactory } from "./renderer";

@Injectable()
export class InjectableAnimationEngine extends NativeScriptAnimationEngine {
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
super(driver, normalizer);
}
}

export function instantiateSupportedAnimationDriver() {
return new NativeScriptAnimationDriver();
}

export function instantiateRendererFactory(
renderer: NativeScriptRendererFactory, engine: AnimationEngine, zone: NgZone) {
return new AnimationRendererFactory(renderer, engine, zone);
}

export function instanciateDefaultStyleNormalizer() {
return new WebAnimationsStyleNormalizer();
}

export const NATIVESCRIPT_ANIMATIONS_PROVIDERS: Provider[] = [
{provide: AnimationDriver, useFactory: instantiateSupportedAnimationDriver},
{provide: AnimationStyleNormalizer, useFactory: instanciateDefaultStyleNormalizer},
{provide: AnimationEngine, useClass: InjectableAnimationEngine}, {
provide: RendererFactory2,
useFactory: instantiateRendererFactory,
deps: [NativeScriptRendererFactory, AnimationEngine, NgZone]
}
];

@NgModule({
imports: [NativeScriptModule],
providers: NATIVESCRIPT_ANIMATIONS_PROVIDERS,
})
export class NativeScriptAnimationsModule {
}
32 changes: 32 additions & 0 deletions nativescript-angular/animations/animation-driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AnimationPlayer } from "@angular/animations";
import { NgView } from "../element-registry";

import { NativeScriptAnimationPlayer } from "./animation-player";
import { Keyframe } from "./utils";

export abstract class AnimationDriver {
abstract animate(
element: any,
keyframes: Keyframe[],
duration: number,
delay: number,
easing: string
): AnimationPlayer;
}

export class NativeScriptAnimationDriver implements AnimationDriver {
computeStyle(element: NgView, prop: string): string {
return element.style[`css-${prop}`];
}

animate(
element: NgView,
keyframes: Keyframe[],
duration: number,
delay: number,
easing: string
): AnimationPlayer {
return new NativeScriptAnimationPlayer(
element, keyframes, duration, delay, easing);
}
}
143 changes: 143 additions & 0 deletions nativescript-angular/animations/animation-engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { ɵDomAnimationEngine as DomAnimationEngine } from "@angular/animations/browser";
import { AnimationEvent, AnimationPlayer } from "@angular/animations";

import { NgView } from "../element-registry";
import {
copyArray,
cssClasses,
deleteFromArrayMap,
eraseStylesOverride,
getOrSetAsInMap,
makeAnimationEvent,
optimizeGroupPlayer,
setStyles,
} from "./dom-utils";

const MARKED_FOR_ANIMATION = "ng-animate";

interface QueuedAnimationTransitionTuple {
element: NgView;
player: AnimationPlayer;
triggerName: string;
event: AnimationEvent;
}

// we are extending Angular's animation engine and
// overriding a few methods that work on the DOM
export class NativeScriptAnimationEngine extends DomAnimationEngine {
// this method is almost completely copied from
// the original animation engine, just replaced
// a few method invocations with overriden ones
animateTransition(element: NgView, instruction: any): AnimationPlayer {
const triggerName = instruction.triggerName;

let previousPlayers: AnimationPlayer[];
if (instruction.isRemovalTransition) {
previousPlayers = this._onRemovalTransitionOverride(element);
} else {
previousPlayers = [];
const existingTransitions = this._getTransitionAnimation(element);
const existingPlayer = existingTransitions ? existingTransitions[triggerName] : null;
if (existingPlayer) {
previousPlayers.push(existingPlayer);
}
}

// it's important to do this step before destroying the players
// so that the onDone callback below won"t fire before this
eraseStylesOverride(element, instruction.fromStyles);

// we first run this so that the previous animation player
// data can be passed into the successive animation players
let totalTime = 0;
const players = instruction.timelines.map(timelineInstruction => {
totalTime = Math.max(totalTime, timelineInstruction.totalTime);
return (<any>this)._buildPlayer(element, timelineInstruction, previousPlayers);
});

previousPlayers.forEach(previousPlayer => previousPlayer.destroy());
const player = optimizeGroupPlayer(players);
player.onDone(() => {
player.destroy();
const elmTransitionMap = this._getTransitionAnimation(element);
if (elmTransitionMap) {
delete elmTransitionMap[triggerName];
if (Object.keys(elmTransitionMap).length === 0) {
(<any>this)._activeTransitionAnimations.delete(element);
}
}
deleteFromArrayMap((<any>this)._activeElementAnimations, element, player);
setStyles(element, instruction.toStyles);
});

const elmTransitionMap = getOrSetAsInMap((<any>this)._activeTransitionAnimations, element, {});
elmTransitionMap[triggerName] = player;

this._queuePlayerOverride(
element, triggerName, player,
makeAnimationEvent(
element, triggerName, instruction.fromState, instruction.toState,
null, // this will be filled in during event creation
totalTime));

return player;
}

// overriden to use eachChild method of View
// instead of DOM querySelectorAll
private _onRemovalTransitionOverride(element: NgView): AnimationPlayer[] {
// when a parent animation is set to trigger a removal we want to
// find all of the children that are currently animating and clear
// them out by destroying each of them.
let elms = [];
element.eachChild(child => {
if (cssClasses(<NgView>child).get(MARKED_FOR_ANIMATION)) {
elms.push(child);
}

return true;
});

for (let i = 0; i < elms.length; i++) {
const elm = elms[i];
const activePlayers = this._getElementAnimation(elm);
if (activePlayers) {
activePlayers.forEach(player => player.destroy());
}

const activeTransitions = this._getTransitionAnimation(elm);
if (activeTransitions) {
Object.keys(activeTransitions).forEach(triggerName => {
const player = activeTransitions[triggerName];
if (player) {
player.destroy();
}
});
}
}

// we make a copy of the array because the actual source array is modified
// each time a player is finished/destroyed (the forEach loop would fail otherwise)
return copyArray(this._getElementAnimation(element));
}

// overriden to use cssClasses method to access native element's styles
// instead of DOM element's classList
private _queuePlayerOverride(
element: NgView, triggerName: string, player: AnimationPlayer, event: AnimationEvent) {
const tuple = <QueuedAnimationTransitionTuple>{ element, player, triggerName, event };
(<any>this)._queuedTransitionAnimations.push(tuple);
player.init();

cssClasses(element).set(MARKED_FOR_ANIMATION, true);
player.onDone(() => cssClasses(element).set(MARKED_FOR_ANIMATION, false));
}

private _getElementAnimation(element: NgView) {
return (<any>this)._activeElementAnimations.get(element);
}

private _getTransitionAnimation(element: NgView) {
return (<any>this)._activeTransitionAnimations.get(element);
}
}
108 changes: 108 additions & 0 deletions nativescript-angular/animations/animation-player.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { AnimationPlayer } from "@angular/animations";
import {
KeyframeAnimation,
KeyframeAnimationInfo,
} from "tns-core-modules/ui/animation/keyframe-animation";

import { NgView } from "../element-registry";
import { Keyframe, getAnimationCurve, parseAnimationKeyframe } from "./utils";

export class NativeScriptAnimationPlayer implements AnimationPlayer {
public parentPlayer: AnimationPlayer = null;

private _startSubscriptions: Function[] = [];
private _doneSubscriptions: Function[] = [];
private _finished = false;
private _started = false;
private animation: KeyframeAnimation;

constructor(
private target: NgView,
keyframes: Keyframe[],
duration: number,
delay: number,
easing: string
) {
this.initKeyframeAnimation(keyframes, duration, delay, easing);
}

init(): void {
}

hasStarted(): boolean {
return this._started;
}

onStart(fn: Function): void { this._startSubscriptions.push(fn); }
onDone(fn: Function): void { this._doneSubscriptions.push(fn); }
onDestroy(fn: Function): void { this._doneSubscriptions.push(fn); }

play(): void {
if (!this.animation) {
return;
}

if (!this._started) {
this._started = true;
this._startSubscriptions.forEach(fn => fn());
this._startSubscriptions = [];
}

this.animation.play(this.target)
.then(() => this.onFinish())
.catch((_e) => { });
}

pause(): void {
throw new Error("AnimationPlayer.pause method is not supported!");
}

finish(): void {
throw new Error("AnimationPlayer.finish method is not supported!");
}

reset(): void {
if (this.animation && this.animation.isPlaying) {
this.animation.cancel();
}
}

restart(): void {
this.reset();
this.play();
}

destroy(): void {
this.reset();
this.onFinish();
}

setPosition(_p: any): void {
throw new Error("AnimationPlayer.setPosition method is not supported!");
}

getPosition(): number {
return 0;
}

private initKeyframeAnimation(keyframes: Keyframe[], duration: number, delay: number, easing: string) {
let info = new KeyframeAnimationInfo();
info.isForwards = true;
info.iterations = 1;
info.duration = duration === 0 ? 0.01 : duration;
info.delay = delay;
info.curve = getAnimationCurve(easing);
info.keyframes = keyframes.map(parseAnimationKeyframe);

this.animation = KeyframeAnimation.keyframeAnimationFromInfo(info);
}

private onFinish() {
if (!this._finished) {
this._finished = true;
this._started = false;
this._doneSubscriptions.forEach(fn => fn());
this._doneSubscriptions = [];
}
}
}
Loading

0 comments on commit f9ad6a5

Please sign in to comment.