-
-
Notifications
You must be signed in to change notification settings - Fork 240
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(animations): introduce NativeScriptAnimationsModule (#704)
BREAKING CHANGE: To use animations, you need to import the NativeScriptAnimationsModule from "nativescript-angular/animations" in your root NgModule.
- Loading branch information
Showing
15 changed files
with
646 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = []; | ||
} | ||
} | ||
} |
Oops, something went wrong.