fix(animations): properly handle cancelled animation style application
This commit is contained in:
parent
858dea98e5
commit
105e920b69
|
@ -18,7 +18,7 @@ import {ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_CLASSNAME, NG_ANIMATING_S
|
||||||
import {AnimationDriver} from './animation_driver';
|
import {AnimationDriver} from './animation_driver';
|
||||||
import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
|
import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
|
||||||
|
|
||||||
const EMPTY_PLAYER_ARRAY: AnimationPlayer[] = [];
|
const EMPTY_PLAYER_ARRAY: TransitionAnimationPlayer[] = [];
|
||||||
const NULL_REMOVAL_STATE: ElementAnimationState = {
|
const NULL_REMOVAL_STATE: ElementAnimationState = {
|
||||||
namespaceId: '',
|
namespaceId: '',
|
||||||
setForRemoval: null,
|
setForRemoval: null,
|
||||||
|
@ -708,7 +708,14 @@ export class TransitionAnimationEngine {
|
||||||
|
|
||||||
if (this._namespaceList.length &&
|
if (this._namespaceList.length &&
|
||||||
(this.totalQueuedPlayers || this.collectedLeaveElements.length)) {
|
(this.totalQueuedPlayers || this.collectedLeaveElements.length)) {
|
||||||
players = this._flushAnimations(microtaskId);
|
const cleanupFns: Function[] = [];
|
||||||
|
try {
|
||||||
|
players = this._flushAnimations(cleanupFns, microtaskId);
|
||||||
|
} finally {
|
||||||
|
for (let i = 0; i < cleanupFns.length; i++) {
|
||||||
|
cleanupFns[i]();
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < this.collectedLeaveElements.length; i++) {
|
for (let i = 0; i < this.collectedLeaveElements.length; i++) {
|
||||||
const element = this.collectedLeaveElements[i];
|
const element = this.collectedLeaveElements[i];
|
||||||
|
@ -737,7 +744,8 @@ export class TransitionAnimationEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _flushAnimations(microtaskId: number): TransitionAnimationPlayer[] {
|
private _flushAnimations(cleanupFns: Function[], microtaskId: number):
|
||||||
|
TransitionAnimationPlayer[] {
|
||||||
const subTimelines = new ElementInstructionMap();
|
const subTimelines = new ElementInstructionMap();
|
||||||
const skippedPlayers: TransitionAnimationPlayer[] = [];
|
const skippedPlayers: TransitionAnimationPlayer[] = [];
|
||||||
const skippedPlayersMap = new Map<any, AnimationPlayer[]>();
|
const skippedPlayersMap = new Map<any, AnimationPlayer[]>();
|
||||||
|
@ -745,7 +753,6 @@ export class TransitionAnimationEngine {
|
||||||
const queriedElements = new Map<any, TransitionAnimationPlayer[]>();
|
const queriedElements = new Map<any, TransitionAnimationPlayer[]>();
|
||||||
const allPreStyleElements = new Map<any, Set<string>>();
|
const allPreStyleElements = new Map<any, Set<string>>();
|
||||||
const allPostStyleElements = new Map<any, Set<string>>();
|
const allPostStyleElements = new Map<any, Set<string>>();
|
||||||
const cleanupFns: Function[] = [];
|
|
||||||
|
|
||||||
const bodyNode = getBodyNode();
|
const bodyNode = getBodyNode();
|
||||||
const allEnterNodes: any[] = this.collectedEnterElements.length ?
|
const allEnterNodes: any[] = this.collectedEnterElements.length ?
|
||||||
|
@ -855,7 +862,6 @@ export class TransitionAnimationEngine {
|
||||||
instruction.errors !.forEach(error => { msg += `- ${error}\n`; });
|
instruction.errors !.forEach(error => { msg += `- ${error}\n`; });
|
||||||
});
|
});
|
||||||
|
|
||||||
cleanupFns.forEach(fn => fn());
|
|
||||||
allPlayers.forEach(player => player.destroy());
|
allPlayers.forEach(player => player.destroy());
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
|
@ -1011,7 +1017,6 @@ export class TransitionAnimationEngine {
|
||||||
player.play();
|
player.play();
|
||||||
});
|
});
|
||||||
|
|
||||||
cleanupFns.forEach(fn => fn());
|
|
||||||
return rootPlayers;
|
return rootPlayers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1107,20 +1112,16 @@ export class TransitionAnimationEngine {
|
||||||
const allSubElements = new Set<any>();
|
const allSubElements = new Set<any>();
|
||||||
const allNewPlayers = instruction.timelines.map(timelineInstruction => {
|
const allNewPlayers = instruction.timelines.map(timelineInstruction => {
|
||||||
const element = timelineInstruction.element;
|
const element = timelineInstruction.element;
|
||||||
|
allConsumedElements.add(element);
|
||||||
|
|
||||||
// FIXME (matsko): make sure to-be-removed animations are removed properly
|
// FIXME (matsko): make sure to-be-removed animations are removed properly
|
||||||
const details = element[REMOVAL_FLAG];
|
const details = element[REMOVAL_FLAG];
|
||||||
if (details && details.removedBeforeQueried) return new NoopAnimationPlayer();
|
if (details && details.removedBeforeQueried) return new NoopAnimationPlayer();
|
||||||
|
|
||||||
const isQueriedElement = element !== rootElement;
|
const isQueriedElement = element !== rootElement;
|
||||||
let previousPlayers: AnimationPlayer[] = EMPTY_PLAYER_ARRAY;
|
const previousPlayers =
|
||||||
if (!allConsumedElements.has(element)) {
|
(allPreviousPlayersMap.get(element) || EMPTY_PLAYER_ARRAY).map(p => p.getRealPlayer());
|
||||||
allConsumedElements.add(element);
|
|
||||||
const _previousPlayers = allPreviousPlayersMap.get(element);
|
|
||||||
if (_previousPlayers) {
|
|
||||||
previousPlayers = _previousPlayers.map(p => p.getRealPlayer());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const preStyles = preStylesMap.get(element);
|
const preStyles = preStylesMap.get(element);
|
||||||
const postStyles = postStylesMap.get(element);
|
const postStyles = postStylesMap.get(element);
|
||||||
const keyframes = normalizeKeyframes(
|
const keyframes = normalizeKeyframes(
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
import {AnimationPlayer} from '@angular/animations';
|
import {AnimationPlayer} from '@angular/animations';
|
||||||
|
|
||||||
import {copyStyles, eraseStyles, setStyles} from '../../util';
|
import {allowPreviousPlayerStylesMerge, copyStyles} from '../../util';
|
||||||
|
|
||||||
import {DOMAnimation} from './dom_animation';
|
import {DOMAnimation} from './dom_animation';
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||||
public time = 0;
|
public time = 0;
|
||||||
|
|
||||||
public parentPlayer: AnimationPlayer|null = null;
|
public parentPlayer: AnimationPlayer|null = null;
|
||||||
public previousStyles: {[styleName: string]: string | number};
|
public previousStyles: {[styleName: string]: string | number} = {};
|
||||||
public currentSnapshot: {[styleName: string]: string | number} = {};
|
public currentSnapshot: {[styleName: string]: string | number} = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -37,12 +37,13 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||||
this._delay = <number>options['delay'] || 0;
|
this._delay = <number>options['delay'] || 0;
|
||||||
this.time = this._duration + this._delay;
|
this.time = this._duration + this._delay;
|
||||||
|
|
||||||
this.previousStyles = {};
|
if (allowPreviousPlayerStylesMerge(this._duration, this._delay)) {
|
||||||
previousPlayers.forEach(player => {
|
previousPlayers.forEach(player => {
|
||||||
let styles = player.currentSnapshot;
|
let styles = player.currentSnapshot;
|
||||||
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
|
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _onFinish() {
|
private _onFinish() {
|
||||||
if (!this._finished) {
|
if (!this._finished) {
|
||||||
|
|
|
@ -213,3 +213,7 @@ const DASH_CASE_REGEXP = /-+([a-z0-9])/g;
|
||||||
export function dashCaseToCamelCase(input: string): string {
|
export function dashCaseToCamelCase(input: string): string {
|
||||||
return input.replace(DASH_CASE_REGEXP, (...m: any[]) => m[1].toUpperCase());
|
return input.replace(DASH_CASE_REGEXP, (...m: any[]) => m[1].toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function allowPreviousPlayerStylesMerge(duration: number, delay: number) {
|
||||||
|
return duration === 0 || delay === 0;
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {AUTO_STYLE, AnimationPlayer, NoopAnimationPlayer, ɵStyleData} from '@an
|
||||||
|
|
||||||
import {AnimationDriver} from '../../src/render/animation_driver';
|
import {AnimationDriver} from '../../src/render/animation_driver';
|
||||||
import {containsElement, invokeQuery, matchesElement} from '../../src/render/shared';
|
import {containsElement, invokeQuery, matchesElement} from '../../src/render/shared';
|
||||||
|
import {allowPreviousPlayerStylesMerge} from '../../src/util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @experimental Animation support is experimental.
|
* @experimental Animation support is experimental.
|
||||||
|
@ -56,12 +56,15 @@ export class MockAnimationPlayer extends NoopAnimationPlayer {
|
||||||
public duration: number, public delay: number, public easing: string,
|
public duration: number, public delay: number, public easing: string,
|
||||||
public previousPlayers: any[]) {
|
public previousPlayers: any[]) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
if (allowPreviousPlayerStylesMerge(duration, delay)) {
|
||||||
previousPlayers.forEach(player => {
|
previousPlayers.forEach(player => {
|
||||||
if (player instanceof MockAnimationPlayer) {
|
if (player instanceof MockAnimationPlayer) {
|
||||||
const styles = player.currentSnapshot;
|
const styles = player.currentSnapshot;
|
||||||
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
|
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.totalTime = delay + duration;
|
this.totalTime = delay + duration;
|
||||||
}
|
}
|
||||||
|
|
|
@ -782,7 +782,7 @@ export function main() {
|
||||||
expect(players.length).toEqual(3);
|
expect(players.length).toEqual(3);
|
||||||
const [p1, p2, p3] = players;
|
const [p1, p2, p3] = players;
|
||||||
expect(p1.previousStyles).toEqual({opacity: AUTO_STYLE});
|
expect(p1.previousStyles).toEqual({opacity: AUTO_STYLE});
|
||||||
expect(p2.previousStyles).toEqual({});
|
expect(p2.previousStyles).toEqual({opacity: AUTO_STYLE});
|
||||||
expect(p3.previousStyles).toEqual({});
|
expect(p3.previousStyles).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1000,7 +1000,8 @@ export function main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly cancel items that were queried into a former animation', () => {
|
it('should properly cancel items that were queried into a former animation and pass in the associated styles into the follow-up players per element',
|
||||||
|
() => {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ani-cmp',
|
selector: 'ani-cmp',
|
||||||
template: `
|
template: `
|
||||||
|
@ -1052,16 +1053,16 @@ export function main() {
|
||||||
expect(players.length).toEqual(4);
|
expect(players.length).toEqual(4);
|
||||||
|
|
||||||
const [p1, p2, p3, p4] = players;
|
const [p1, p2, p3, p4] = players;
|
||||||
const [p1p1, p1p2] = p1.previousPlayers;
|
|
||||||
const [p2p1, p2p2] = p2.previousPlayers;
|
|
||||||
|
|
||||||
expect(p1p1).toBe(previousPlayers[3]);
|
// p1 && p2 are the starting players for item3 and item4
|
||||||
expect(p1p2).toBe(previousPlayers[8]);
|
expect(p1.previousStyles)
|
||||||
expect(p2p1).toBe(previousPlayers[4]);
|
.toEqual({opacity: AUTO_STYLE, width: AUTO_STYLE, height: AUTO_STYLE});
|
||||||
expect(p2p2).toBe(previousPlayers[9]);
|
expect(p2.previousStyles)
|
||||||
|
.toEqual({opacity: AUTO_STYLE, width: AUTO_STYLE, height: AUTO_STYLE});
|
||||||
|
|
||||||
expect(p3.previousPlayers).toEqual([]);
|
// p3 && p4 are the following players for item3 and item4
|
||||||
expect(p4.previousPlayers).toEqual([]);
|
expect(p3.previousStyles).toEqual({});
|
||||||
|
expect(p4.previousStyles).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not remove a parent container if its contents are queried into by an ancestor element',
|
it('should not remove a parent container if its contents are queried into by an ancestor element',
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {animate, state, style, transition, trigger} from '@angular/animations';
|
import {animate, query, state, style, transition, trigger} from '@angular/animations';
|
||||||
import {AnimationDriver, ɵAnimationEngine} from '@angular/animations/browser';
|
import {AnimationDriver, ɵAnimationEngine, ɵWebAnimationsDriver, ɵWebAnimationsPlayer, ɵsupportsWebAnimations} from '@angular/animations/browser';
|
||||||
import {ɵWebAnimationsDriver, ɵWebAnimationsPlayer, ɵsupportsWebAnimations} from '@angular/animations/browser'
|
import {AnimationGroupPlayer} from '@angular/animations/src/players/animation_group_player';
|
||||||
import {Component, ViewChild} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
import {TestBed} from '../../testing';
|
import {TestBed} from '../../testing';
|
||||||
|
@ -172,15 +172,125 @@ export function main() {
|
||||||
{height: '100px', offset: 0}, {height: '80px', offset: 1}
|
{height: '100px', offset: 0}, {height: '80px', offset: 1}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should compute intermediate styles properly when an animation is cancelled', () => {
|
||||||
|
@Component({
|
||||||
|
selector: 'ani-cmp',
|
||||||
|
template: `
|
||||||
|
<div [@myAnimation]="exp">...</div>
|
||||||
|
`,
|
||||||
|
animations: [
|
||||||
|
trigger(
|
||||||
|
'myAnimation',
|
||||||
|
[
|
||||||
|
transition(
|
||||||
|
'* => a',
|
||||||
|
[
|
||||||
|
style({width: 0, height: 0}),
|
||||||
|
animate('1s', style({width: '300px', height: '600px'})),
|
||||||
|
]),
|
||||||
|
transition('* => b', [animate('1s', style({opacity: 0}))]),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
class Cmp {
|
||||||
|
public exp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
|
||||||
|
const engine = TestBed.get(ɵAnimationEngine);
|
||||||
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
const cmp = fixture.componentInstance;
|
||||||
|
|
||||||
|
cmp.exp = 'a';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
let player = engine.players[0] !;
|
||||||
|
let webPlayer = player.getRealPlayer() as ɵWebAnimationsPlayer;
|
||||||
|
webPlayer.setPosition(0.5);
|
||||||
|
|
||||||
|
cmp.exp = 'b';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
player = engine.players[0] !;
|
||||||
|
webPlayer = player.getRealPlayer() as ɵWebAnimationsPlayer;
|
||||||
|
expect(approximate(parseFloat(webPlayer.previousStyles['width'] as string), 150))
|
||||||
|
.toBeLessThan(0.05);
|
||||||
|
expect(approximate(parseFloat(webPlayer.previousStyles['height'] as string), 300))
|
||||||
|
.toBeLessThan(0.05);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compute intermediate styles properly for multiple queried elements when an animation is cancelled',
|
||||||
|
() => {
|
||||||
|
@Component({
|
||||||
|
selector: 'ani-cmp',
|
||||||
|
template: `
|
||||||
|
<div [@myAnimation]="exp">
|
||||||
|
<div *ngFor="let item of items" class="target"></div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
animations: [
|
||||||
|
trigger(
|
||||||
|
'myAnimation',
|
||||||
|
[
|
||||||
|
transition(
|
||||||
|
'* => full', [query(
|
||||||
|
'.target',
|
||||||
|
[
|
||||||
|
style({width: 0, height: 0}),
|
||||||
|
animate('1s', style({width: '500px', height: '1000px'})),
|
||||||
|
])]),
|
||||||
|
transition(
|
||||||
|
'* => empty', [query('.target', [animate('1s', style({opacity: 0}))])]),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
class Cmp {
|
||||||
|
public exp: string;
|
||||||
|
public items: any[] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
|
||||||
|
const engine = TestBed.get(ɵAnimationEngine);
|
||||||
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
const cmp = fixture.componentInstance;
|
||||||
|
|
||||||
|
cmp.exp = 'full';
|
||||||
|
cmp.items = [0, 1, 2, 3, 4];
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
let player = engine.players[0] !;
|
||||||
|
let groupPlayer = player.getRealPlayer() as AnimationGroupPlayer;
|
||||||
|
let players = groupPlayer.players;
|
||||||
|
expect(players.length).toEqual(5);
|
||||||
|
|
||||||
|
for (let i = 0; i < players.length; i++) {
|
||||||
|
const p = players[i] as ɵWebAnimationsPlayer;
|
||||||
|
p.setPosition(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp.exp = 'empty';
|
||||||
|
cmp.items = [];
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
player = engine.players[0];
|
||||||
|
groupPlayer = player.getRealPlayer() as AnimationGroupPlayer;
|
||||||
|
players = groupPlayer.players;
|
||||||
|
|
||||||
|
expect(players.length).toEqual(5);
|
||||||
|
for (let i = 0; i < players.length; i++) {
|
||||||
|
const p = players[i] as ɵWebAnimationsPlayer;
|
||||||
|
expect(approximate(parseFloat(p.previousStyles['width'] as string), 250))
|
||||||
|
.toBeLessThan(0.05);
|
||||||
|
expect(approximate(parseFloat(p.previousStyles['height'] as string), 500))
|
||||||
|
.toBeLessThan(0.05);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertStyleBetween(
|
function approximate(value: number, target: number) {
|
||||||
element: any, prop: string, start: string | number, end: string | number) {
|
return Math.abs(target - value) / value;
|
||||||
const style = (window.getComputedStyle(element) as any)[prop] as string;
|
|
||||||
if (typeof start == 'number' && typeof end == 'number') {
|
|
||||||
const value = parseFloat(style);
|
|
||||||
expect(value).toBeGreaterThan(start);
|
|
||||||
expect(value).toBeLessThan(end);
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue