Skip to content

Commit

Permalink
fix(animations): always fire inner trigger callbacks even if blocked …
Browse files Browse the repository at this point in the history
…by parent animations (#19753)

Closes #19100

PR Close #19753
  • Loading branch information
matsko authored and mhevery committed Nov 18, 2017
1 parent b1f8eb1 commit 814f062
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -986,11 +986,15 @@ export class TransitionAnimationEngine {
}

const allPreviousPlayersMap = new Map<any, TransitionAnimationPlayer[]>();
let sortedParentElements: any[] = [];
// this map works to tell which element in the DOM tree is contained by
// which animation. Further down below this map will get populated once
// the players are built and in doing so it can efficiently figure out
// if a sub player is skipped due to a parent player having priority.
const animationElementMap = new Map<any, any>();
queuedInstructions.forEach(entry => {
const element = entry.element;
if (subTimelines.has(element)) {
sortedParentElements.unshift(element);
animationElementMap.set(element, element);
this._beforeAnimationBuild(
entry.player.namespaceId, entry.instruction, allPreviousPlayersMap);
}
Expand Down Expand Up @@ -1041,6 +1045,7 @@ export class TransitionAnimationEngine {

const rootPlayers: TransitionAnimationPlayer[] = [];
const subPlayers: TransitionAnimationPlayer[] = [];
const NO_PARENT_ANIMATION_ELEMENT_DETECTED = {};
queuedInstructions.forEach(entry => {
const {element, player, instruction} = entry;
// this means that it was never consumed by a parent animation which
Expand All @@ -1052,29 +1057,41 @@ export class TransitionAnimationEngine {
return;
}

// this will flow up the DOM and query the map to figure out
// if a parent animation has priority over it. In the situation
// that a parent is detected then it will cancel the loop. If
// nothing is detected, or it takes a few hops to find a parent,
// then it will fill in the missing nodes and signal them as having
// a detected parent (or a NO_PARENT value via a special constant).
let parentWithAnimation: any = NO_PARENT_ANIMATION_ELEMENT_DETECTED;
if (animationElementMap.size > 1) {
let elm = element;
const parentsToAdd: any[] = [];
while (elm = elm.parentNode) {
const detectedParent = animationElementMap.get(elm);
if (detectedParent) {
parentWithAnimation = detectedParent;
break;
}
parentsToAdd.push(elm);
}
parentsToAdd.forEach(parent => animationElementMap.set(parent, parentWithAnimation));
}

const innerPlayer = this._buildAnimation(
player.namespaceId, instruction, allPreviousPlayersMap, skippedPlayersMap, preStylesMap,
postStylesMap);
player.setRealPlayer(innerPlayer);

let parentHasPriority: any = null;
for (let i = 0; i < sortedParentElements.length; i++) {
const parent = sortedParentElements[i];
if (parent === element) break;
if (this.driver.containsElement(parent, element)) {
parentHasPriority = parent;
break;
}
}
player.setRealPlayer(innerPlayer);

if (parentHasPriority) {
const parentPlayers = this.playersByElement.get(parentHasPriority);
if (parentWithAnimation === NO_PARENT_ANIMATION_ELEMENT_DETECTED) {
rootPlayers.push(player);
} else {
const parentPlayers = this.playersByElement.get(parentWithAnimation);
if (parentPlayers && parentPlayers.length) {
player.parentPlayer = optimizeGroupPlayer(parentPlayers);
}
skippedPlayers.push(player);
} else {
rootPlayers.push(player);
}
} else {
eraseStyles(element, instruction.fromStyles);
Expand Down Expand Up @@ -1105,7 +1122,7 @@ export class TransitionAnimationEngine {
// fire the start/done transition callback events
skippedPlayers.forEach(player => {
if (player.parentPlayer) {
player.parentPlayer.onDestroy(() => player.destroy());
player.syncPlayerEvents(player.parentPlayer);
} else {
player.destroy();
}
Expand Down Expand Up @@ -1366,6 +1383,15 @@ export class TransitionAnimationPlayer implements AnimationPlayer {

getRealPlayer() { return this._player; }

syncPlayerEvents(player: AnimationPlayer) {
const p = this._player as any;
if (p.triggerCallback) {
player.onStart(() => p.triggerCallback('start'));
}
player.onDone(() => this.finish());
player.onDestroy(() => this.destroy());
}

private _queueEvent(name: string, callback: (event: any) => any): void {
getOrSetAsInMap(this._queuedCallbacks, name, []).push(callback);
}
Expand Down Expand Up @@ -1419,6 +1445,14 @@ export class TransitionAnimationPlayer implements AnimationPlayer {
getPosition(): number { return this.queued ? 0 : this._player.getPosition(); }

get totalTime(): number { return this._player.totalTime; }

/* @internal */
triggerCallback(phaseName: string): void {
const p = this._player as any;
if (p.triggerCallback) {
p.triggerCallback(phaseName);
}
}
}

function deleteOrUnsetInMap(map: Map<any, any[]>| {[key: string]: any}, key: any, value: any) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ export class WebAnimationsPlayer implements AnimationPlayer {
}
this.currentSnapshot = styles;
}

/* @internal */
triggerCallback(phaseName: string): void {
const methods = phaseName == 'start' ? this._onStartFns : this._onDoneFns;
methods.forEach(fn => fn());
methods.length = 0;
}
}

function _computeStyle(element: any, prop: string): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ export function main() {
const p = innerPlayer !;
expect(p.log).toEqual(['play']);
});

it('should fire start/done callbacks manually when called directly', () => {
const log: string[] = [];

const player = new WebAnimationsPlayer(element, [], {duration: 1000});
player.onStart(() => log.push('started'));
player.onDone(() => log.push('done'));

player.triggerCallback('start');
expect(log).toEqual(['started']);

player.play();
expect(log).toEqual(['started']);

player.triggerCallback('done');
expect(log).toEqual(['started', 'done']);

player.finish();
expect(log).toEqual(['started', 'done']);
});
});
}

Expand Down
7 changes: 7 additions & 0 deletions packages/animations/src/players/animation_group_player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,11 @@ export class AnimationGroupPlayer implements AnimationPlayer {
}
});
}

/* @internal */
triggerCallback(phaseName: string): void {
const methods = phaseName == 'start' ? this._onStartFns : this._onDoneFns;
methods.forEach(fn => fn());
methods.length = 0;
}
}
11 changes: 10 additions & 1 deletion packages/animations/src/players/animation_player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface AnimationPlayer {
parentPlayer: AnimationPlayer|null;
readonly totalTime: number;
beforeDestroy?: () => any;
/* @internal */
triggerCallback?: (phaseName: string) => void;
}

/**
Expand Down Expand Up @@ -91,4 +93,11 @@ export class NoopAnimationPlayer implements AnimationPlayer {
reset(): void {}
setPosition(p: number): void {}
getPosition(): number { return 0; }
}

/* @internal */
triggerCallback(phaseName: string): void {
const methods = phaseName == 'start' ? this._onStartFns : this._onDoneFns;
methods.forEach(fn => fn());
methods.length = 0;
}
}
24 changes: 24 additions & 0 deletions packages/animations/test/animation_player_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,29 @@ export function main() {
player.destroy();
expect(log).toEqual(['started', 'done', 'destroy']);
});

it('should fire start/done callbacks manually when called directly', fakeAsync(() => {
const log: string[] = [];

const player = new NoopAnimationPlayer();
player.onStart(() => log.push('started'));
player.onDone(() => log.push('done'));
flushMicrotasks();

player.triggerCallback('start');
expect(log).toEqual(['started']);

player.play();
expect(log).toEqual(['started']);

player.triggerCallback('done');
expect(log).toEqual(['started', 'done']);

player.finish();
expect(log).toEqual(['started', 'done']);

flushMicrotasks();
expect(log).toEqual(['started', 'done']);
}));
});
}
Loading

0 comments on commit 814f062

Please sign in to comment.