fix(animations): retain styling when transition destinations are changed (#12208)

Closes #9661
Closes #12208
This commit is contained in:
Matias Niemelä 2016-11-14 16:59:06 -08:00 committed by Victor Berchet
parent 46023e4792
commit 9de76ebfa5
24 changed files with 403 additions and 79 deletions

View File

@ -41,7 +41,9 @@ const _ANIMATION_TIME_VAR = o.variable('totalTime');
const _ANIMATION_START_STATE_STYLES_VAR = o.variable('startStateStyles'); const _ANIMATION_START_STATE_STYLES_VAR = o.variable('startStateStyles');
const _ANIMATION_END_STATE_STYLES_VAR = o.variable('endStateStyles'); const _ANIMATION_END_STATE_STYLES_VAR = o.variable('endStateStyles');
const _ANIMATION_COLLECTED_STYLES = o.variable('collectedStyles'); const _ANIMATION_COLLECTED_STYLES = o.variable('collectedStyles');
const EMPTY_MAP = o.literalMap([]); const _PREVIOUS_ANIMATION_PLAYERS = o.variable('previousPlayers');
const _EMPTY_MAP = o.literalMap([]);
const _EMPTY_ARRAY = o.literalArr([]);
class _AnimationBuilder implements AnimationAstVisitor { class _AnimationBuilder implements AnimationAstVisitor {
private _fnVarName: string; private _fnVarName: string;
@ -110,10 +112,15 @@ class _AnimationBuilder implements AnimationAstVisitor {
_callAnimateMethod( _callAnimateMethod(
ast: AnimationStepAst, startingStylesExpr: any, keyframesExpr: any, ast: AnimationStepAst, startingStylesExpr: any, keyframesExpr: any,
context: _AnimationBuilderContext) { context: _AnimationBuilderContext) {
let previousStylesValue: o.Expression = _EMPTY_ARRAY;
if (context.isExpectingFirstAnimateStep) {
previousStylesValue = _PREVIOUS_ANIMATION_PLAYERS;
context.isExpectingFirstAnimateStep = false;
}
context.totalTransitionTime += ast.duration + ast.delay; context.totalTransitionTime += ast.duration + ast.delay;
return _ANIMATION_FACTORY_RENDERER_VAR.callMethod('animate', [ return _ANIMATION_FACTORY_RENDERER_VAR.callMethod('animate', [
_ANIMATION_FACTORY_ELEMENT_VAR, startingStylesExpr, keyframesExpr, o.literal(ast.duration), _ANIMATION_FACTORY_ELEMENT_VAR, startingStylesExpr, keyframesExpr, o.literal(ast.duration),
o.literal(ast.delay), o.literal(ast.easing) o.literal(ast.delay), o.literal(ast.easing), previousStylesValue
]); ]);
} }
@ -150,6 +157,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
context.totalTransitionTime = 0; context.totalTransitionTime = 0;
context.isExpectingFirstStyleStep = true; context.isExpectingFirstStyleStep = true;
context.isExpectingFirstAnimateStep = true;
const stateChangePreconditions: o.Expression[] = []; const stateChangePreconditions: o.Expression[] = [];
@ -187,17 +195,16 @@ class _AnimationBuilder implements AnimationAstVisitor {
context.stateMap.registerState(DEFAULT_STATE, {}); context.stateMap.registerState(DEFAULT_STATE, {});
const statements: o.Statement[] = []; const statements: o.Statement[] = [];
statements.push(_ANIMATION_FACTORY_VIEW_CONTEXT statements.push(_PREVIOUS_ANIMATION_PLAYERS
.callMethod( .set(_ANIMATION_FACTORY_VIEW_CONTEXT.callMethod(
'cancelActiveAnimation', 'getAnimationPlayers',
[ [
_ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName), _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName),
_ANIMATION_NEXT_STATE_VAR.equals(o.literal(EMPTY_STATE)) _ANIMATION_NEXT_STATE_VAR.equals(o.literal(EMPTY_STATE))
]) ]))
.toStmt()); .toDeclStmt());
statements.push(_ANIMATION_COLLECTED_STYLES.set(_EMPTY_MAP).toDeclStmt());
statements.push(_ANIMATION_COLLECTED_STYLES.set(EMPTY_MAP).toDeclStmt());
statements.push(_ANIMATION_PLAYER_VAR.set(o.NULL_EXPR).toDeclStmt()); statements.push(_ANIMATION_PLAYER_VAR.set(o.NULL_EXPR).toDeclStmt());
statements.push(_ANIMATION_TIME_VAR.set(o.literal(0)).toDeclStmt()); statements.push(_ANIMATION_TIME_VAR.set(o.literal(0)).toDeclStmt());
@ -223,17 +230,6 @@ class _AnimationBuilder implements AnimationAstVisitor {
const RENDER_STYLES_FN = o.importExpr(resolveIdentifier(Identifiers.renderStyles)); const RENDER_STYLES_FN = o.importExpr(resolveIdentifier(Identifiers.renderStyles));
// before we start any animation we want to clear out the starting
// styles from the element's style property (since they were placed
// there at the end of the last animation
statements.push(RENDER_STYLES_FN
.callFn([
_ANIMATION_FACTORY_ELEMENT_VAR, _ANIMATION_FACTORY_RENDERER_VAR,
o.importExpr(resolveIdentifier(Identifiers.clearStyles))
.callFn([_ANIMATION_START_STATE_STYLES_VAR])
])
.toStmt());
ast.stateTransitions.forEach(transAst => statements.push(transAst.visit(this, context))); ast.stateTransitions.forEach(transAst => statements.push(transAst.visit(this, context)));
// this check ensures that the animation factory always returns a player // this check ensures that the animation factory always returns a player
@ -269,6 +265,22 @@ class _AnimationBuilder implements AnimationAstVisitor {
])]) ])])
.toStmt()); .toStmt());
statements.push(o.importExpr(resolveIdentifier(Identifiers.AnimationSequencePlayer))
.instantiate([_PREVIOUS_ANIMATION_PLAYERS])
.callMethod('destroy', [])
.toStmt());
// before we start any animation we want to clear out the starting
// styles from the element's style property (since they were placed
// there at the end of the last animation
statements.push(RENDER_STYLES_FN
.callFn([
_ANIMATION_FACTORY_ELEMENT_VAR, _ANIMATION_FACTORY_RENDERER_VAR,
o.importExpr(resolveIdentifier(Identifiers.clearStyles))
.callFn([_ANIMATION_START_STATE_STYLES_VAR])
])
.toStmt());
statements.push(_ANIMATION_FACTORY_VIEW_CONTEXT statements.push(_ANIMATION_FACTORY_VIEW_CONTEXT
.callMethod( .callMethod(
'queueAnimation', 'queueAnimation',
@ -304,7 +316,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
const lookupMap: any[] = []; const lookupMap: any[] = [];
Object.keys(context.stateMap.states).forEach(stateName => { Object.keys(context.stateMap.states).forEach(stateName => {
const value = context.stateMap.states[stateName]; const value = context.stateMap.states[stateName];
let variableValue = EMPTY_MAP; let variableValue = _EMPTY_MAP;
if (isPresent(value)) { if (isPresent(value)) {
const styleMap: any[] = []; const styleMap: any[] = [];
Object.keys(value).forEach(key => { styleMap.push([key, o.literal(value[key])]); }); Object.keys(value).forEach(key => { styleMap.push([key, o.literal(value[key])]); });
@ -324,6 +336,7 @@ class _AnimationBuilderContext {
stateMap = new _AnimationBuilderStateMap(); stateMap = new _AnimationBuilderStateMap();
endStateAnimateStep: AnimationStepAst = null; endStateAnimateStep: AnimationStepAst = null;
isExpectingFirstStyleStep = false; isExpectingFirstStyleStep = false;
isExpectingFirstAnimateStep = false;
totalTransitionTime = 0; totalTransitionTime = 0;
} }

View File

@ -88,7 +88,7 @@ export class AnimationGroupPlayer implements AnimationPlayer {
this._started = false; this._started = false;
} }
setPosition(p: any /** TODO #9100 */): void { setPosition(p: number): void {
this._players.forEach(player => { player.setPosition(p); }); this._players.forEach(player => { player.setPosition(p); });
} }
@ -100,4 +100,6 @@ export class AnimationGroupPlayer implements AnimationPlayer {
}); });
return min; return min;
} }
get players(): AnimationPlayer[] { return this._players; }
} }

View File

@ -56,6 +56,6 @@ export class NoOpAnimationPlayer implements AnimationPlayer {
finish(): void { this._onFinish(); } finish(): void { this._onFinish(); }
destroy(): void {} destroy(): void {}
reset(): void {} reset(): void {}
setPosition(p: any /** TODO #9100 */): void {} setPosition(p: number): void {}
getPosition(): number { return 0; } getPosition(): number { return 0; }
} }

View File

@ -104,7 +104,9 @@ export class AnimationSequencePlayer implements AnimationPlayer {
} }
} }
setPosition(p: any /** TODO #9100 */): void { this._players[0].setPosition(p); } setPosition(p: number): void { this._players[0].setPosition(p); }
getPosition(): number { return this._players[0].getPosition(); } getPosition(): number { return this._players[0].getPosition(); }
get players(): AnimationPlayer[] { return this._players; }
} }

View File

@ -84,6 +84,8 @@ export function balanceAnimationKeyframes(
firstKeyframe.styles.styles.push(extraFirstKeyframeStyles); firstKeyframe.styles.styles.push(extraFirstKeyframeStyles);
} }
collectAndResolveStyles(collectedStyles, [finalStateStyles]);
return keyframes; return keyframes;
} }

View File

@ -22,7 +22,7 @@ export class DebugDomRootRenderer implements RootRenderer {
} }
} }
export class DebugDomRenderer implements Renderer { export class DebugDomRenderer {
constructor(private _delegate: Renderer) {} constructor(private _delegate: Renderer) {}
selectRootElement(selectorOrNode: string|any, debugInfo?: RenderDebugInfo): any { selectRootElement(selectorOrNode: string|any, debugInfo?: RenderDebugInfo): any {
@ -150,7 +150,9 @@ export class DebugDomRenderer implements Renderer {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
return this._delegate.animate(element, startingStyles, keyframes, duration, delay, easing); previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
return this._delegate.animate(
element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
} }
} }

View File

@ -8,8 +8,9 @@
import {AnimationGroupPlayer} from '../animation/animation_group_player'; import {AnimationGroupPlayer} from '../animation/animation_group_player';
import {AnimationPlayer} from '../animation/animation_player'; import {AnimationPlayer} from '../animation/animation_player';
import {queueAnimation as queueAnimationGlobally} from '../animation/animation_queue'; import {queueAnimation as queueAnimationGlobally} from '../animation/animation_queue';
import {AnimationTransitionEvent} from '../animation/animation_transition_event'; import {AnimationSequencePlayer} from '../animation/animation_sequence_player';
import {ViewAnimationMap} from '../animation/view_animation_map'; import {ViewAnimationMap} from '../animation/view_animation_map';
import {ListWrapper} from '../facade/collection';
export class AnimationViewContext { export class AnimationViewContext {
private _players = new ViewAnimationMap(); private _players = new ViewAnimationMap();
@ -30,15 +31,26 @@ export class AnimationViewContext {
this._players.set(element, animationName, player); this._players.set(element, animationName, player);
} }
cancelActiveAnimation(element: any, animationName: string, removeAllAnimations: boolean = false): getAnimationPlayers(element: any, animationName: string, removeAllAnimations: boolean = false):
void { AnimationPlayer[] {
const players: AnimationPlayer[] = [];
if (removeAllAnimations) { if (removeAllAnimations) {
this._players.findAllPlayersByElement(element).forEach(player => player.destroy()); this._players.findAllPlayersByElement(element).forEach(
player => { _recursePlayers(player, players); });
} else { } else {
const player = this._players.find(element, animationName); const currentPlayer = this._players.find(element, animationName);
if (player) { if (currentPlayer) {
player.destroy(); _recursePlayers(currentPlayer, players);
} }
} }
return players;
}
}
function _recursePlayers(player: AnimationPlayer, collectedPlayers: AnimationPlayer[]) {
if ((player instanceof AnimationGroupPlayer) || (player instanceof AnimationSequencePlayer)) {
player.players.forEach(player => _recursePlayers(player, collectedPlayers));
} else {
collectedPlayers.push(player);
} }
} }

View File

@ -88,7 +88,8 @@ export abstract class Renderer {
abstract animate( abstract animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer; duration: number, delay: number, easing: string,
previousPlayers?: AnimationPlayer[]): AnimationPlayer;
} }
/** /**

View File

@ -1854,6 +1854,8 @@ function declareTests({useJit}: {useJit: boolean}) {
let animation = driver.log.pop(); let animation = driver.log.pop();
let kf = animation['keyframeLookup']; let kf = animation['keyframeLookup'];
expect(kf[1]).toEqual([1, {'background': 'green'}]); expect(kf[1]).toEqual([1, {'background': 'green'}]);
let player = animation['player'];
player.finish();
cmp.exp = 'blue'; cmp.exp = 'blue';
fixture.detectChanges(); fixture.detectChanges();
@ -1863,6 +1865,8 @@ function declareTests({useJit}: {useJit: boolean}) {
kf = animation['keyframeLookup']; kf = animation['keyframeLookup'];
expect(kf[0]).toEqual([0, {'background': 'green'}]); expect(kf[0]).toEqual([0, {'background': 'green'}]);
expect(kf[1]).toEqual([1, {'background': 'grey'}]); expect(kf[1]).toEqual([1, {'background': 'grey'}]);
player = animation['player'];
player.finish();
cmp.exp = 'red'; cmp.exp = 'red';
fixture.detectChanges(); fixture.detectChanges();
@ -1872,6 +1876,8 @@ function declareTests({useJit}: {useJit: boolean}) {
kf = animation['keyframeLookup']; kf = animation['keyframeLookup'];
expect(kf[0]).toEqual([0, {'background': 'grey'}]); expect(kf[0]).toEqual([0, {'background': 'grey'}]);
expect(kf[1]).toEqual([1, {'background': 'red'}]); expect(kf[1]).toEqual([1, {'background': 'red'}]);
player = animation['player'];
player.finish();
cmp.exp = 'orange'; cmp.exp = 'orange';
fixture.detectChanges(); fixture.detectChanges();
@ -1881,6 +1887,8 @@ function declareTests({useJit}: {useJit: boolean}) {
kf = animation['keyframeLookup']; kf = animation['keyframeLookup'];
expect(kf[0]).toEqual([0, {'background': 'red'}]); expect(kf[0]).toEqual([0, {'background': 'red'}]);
expect(kf[1]).toEqual([1, {'background': 'grey'}]); expect(kf[1]).toEqual([1, {'background': 'grey'}]);
player = animation['player'];
player.finish();
})); }));
it('should seed in the origin animation state styles into the first animation step', it('should seed in the origin animation state styles into the first animation step',
@ -1911,6 +1919,44 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(animation['startingStyles']).toEqual({'height': '100px'}); expect(animation['startingStyles']).toEqual({'height': '100px'});
})); }));
it('should seed in the previous animation styles into the transition if the previous transition was interupted midway',
fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `
<div class="target" [@status]="exp"></div>
`,
animations: [trigger(
'status',
[
state('*', style({ opacity: 0 })),
state('a', style({height: '100px', width: '200px'})),
state('b', style({height: '1000px' })),
transition('* => *', [
animate(1000, style({ fontSize: '20px' })),
animate(1000)
])
])]
}
});
const driver = TestBed.get(AnimationDriver) as MockAnimationDriver;
const fixture = TestBed.createComponent(DummyIfCmp);
const cmp = fixture.componentInstance;
cmp.exp = 'a';
fixture.detectChanges();
flushMicrotasks();
driver.log = [];
cmp.exp = 'b';
fixture.detectChanges();
flushMicrotasks();
const animation = driver.log[0];
expect(animation['previousStyles']).toEqual({opacity: '0', fontSize: '*'});
}));
it('should perform a state change even if there is no transition that is found', it('should perform a state change even if there is no transition that is found',
fakeAsync(() => { fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, { TestBed.overrideComponent(DummyIfCmp, {

View File

@ -5,8 +5,7 @@
* 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 {AUTO_STYLE, AnimationPlayer} from '@angular/core';
import {AnimationPlayer} from '@angular/core';
export class MockAnimationPlayer implements AnimationPlayer { export class MockAnimationPlayer implements AnimationPlayer {
private _onDoneFns: Function[] = []; private _onDoneFns: Function[] = [];
@ -16,8 +15,21 @@ export class MockAnimationPlayer implements AnimationPlayer {
private _started = false; private _started = false;
public parentPlayer: AnimationPlayer = null; public parentPlayer: AnimationPlayer = null;
public previousStyles: {[styleName: string]: string | number} = {};
public log: any[] /** TODO #9100 */ = []; public log: any[] = [];
constructor(
public startingStyles: {[key: string]: string | number} = {},
public keyframes: Array<[number, {[style: string]: string | number}]> = [],
previousPlayers: AnimationPlayer[] = []) {
previousPlayers.forEach(player => {
if (player instanceof MockAnimationPlayer) {
const styles = player._captureStyles();
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
}
});
}
private _onFinish(): void { private _onFinish(): void {
if (!this._finished) { if (!this._finished) {
@ -67,6 +79,32 @@ export class MockAnimationPlayer implements AnimationPlayer {
} }
} }
setPosition(p: any /** TODO #9100 */): void {} setPosition(p: number): void {}
getPosition(): number { return 0; } getPosition(): number { return 0; }
private _captureStyles(): {[styleName: string]: string | number} {
const captures: {[prop: string]: string | number} = {};
if (this.hasStarted()) {
// when assembling the captured styles, it's important that
// we build the keyframe styles in the following order:
// {startingStyles, ... other styles within keyframes, ... previousStyles }
Object.keys(this.startingStyles).forEach(prop => {
captures[prop] = this.startingStyles[prop];
});
this.keyframes.forEach(kf => {
const [offset, styles] = kf;
const newStyles: {[prop: string]: string | number} = {};
Object.keys(styles).forEach(
prop => { captures[prop] = this._finished ? styles[prop] : AUTO_STYLE; });
});
}
Object.keys(this.previousStyles).forEach(prop => {
captures[prop] = this.previousStyles[prop];
});
return captures;
}
} }

View File

@ -13,7 +13,8 @@ import {AnimationKeyframe, AnimationStyles, NoOpAnimationPlayer} from '../privat
class _NoOpAnimationDriver implements AnimationDriver { class _NoOpAnimationDriver implements AnimationDriver {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
return new NoOpAnimationPlayer(); return new NoOpAnimationPlayer();
} }
} }
@ -25,5 +26,6 @@ export abstract class AnimationDriver {
static NOOP: AnimationDriver = new _NoOpAnimationDriver(); static NOOP: AnimationDriver = new _NoOpAnimationDriver();
abstract animate( abstract animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer; duration: number, delay: number, easing: string,
previousPlayers?: AnimationPlayer[]): AnimationPlayer;
} }

View File

@ -260,9 +260,10 @@ export class DomRenderer implements Renderer {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
return this._animationDriver.animate( return this._animationDriver.animate(
element, startingStyles, keyframes, duration, delay, easing); element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
} }
} }

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AnimationPlayer} from '@angular/core';
import {isPresent} from '../facade/lang'; import {isPresent} from '../facade/lang';
import {AnimationKeyframe, AnimationStyles} from '../private_import_core'; import {AnimationKeyframe, AnimationStyles} from '../private_import_core';
@ -15,17 +16,18 @@ import {WebAnimationsPlayer} from './web_animations_player';
export class WebAnimationsDriver implements AnimationDriver { export class WebAnimationsDriver implements AnimationDriver {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): WebAnimationsPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): WebAnimationsPlayer {
let formattedSteps: {[key: string]: string | number}[] = []; let formattedSteps: {[key: string]: string | number}[] = [];
let startingStyleLookup: {[key: string]: string | number} = {}; let startingStyleLookup: {[key: string]: string | number} = {};
if (isPresent(startingStyles) && startingStyles.styles.length > 0) { if (isPresent(startingStyles) && startingStyles.styles.length > 0) {
startingStyleLookup = _populateStyles(element, startingStyles, {}); startingStyleLookup = _populateStyles(startingStyles, {});
startingStyleLookup['offset'] = 0; startingStyleLookup['offset'] = 0;
formattedSteps.push(startingStyleLookup); formattedSteps.push(startingStyleLookup);
} }
keyframes.forEach((keyframe: AnimationKeyframe) => { keyframes.forEach((keyframe: AnimationKeyframe) => {
const data = _populateStyles(element, keyframe.styles, startingStyleLookup); const data = _populateStyles(keyframe.styles, startingStyleLookup);
data['offset'] = keyframe.offset; data['offset'] = keyframe.offset;
formattedSteps.push(data); formattedSteps.push(data);
}); });
@ -52,13 +54,13 @@ export class WebAnimationsDriver implements AnimationDriver {
playerOptions['easing'] = easing; playerOptions['easing'] = easing;
} }
return new WebAnimationsPlayer(element, formattedSteps, playerOptions); return new WebAnimationsPlayer(
element, formattedSteps, playerOptions, <WebAnimationsPlayer[]>previousPlayers);
} }
} }
function _populateStyles( function _populateStyles(styles: AnimationStyles, defaultStyles: {[key: string]: string | number}):
element: any, styles: AnimationStyles, {[key: string]: string | number} {
defaultStyles: {[key: string]: string | number}): {[key: string]: string | number} {
const data: {[key: string]: string | number} = {}; const data: {[key: string]: string | number} = {};
styles.styles.forEach( styles.styles.forEach(
(entry) => { Object.keys(entry).forEach(prop => { data[prop] = entry[prop]; }); }); (entry) => { Object.keys(entry).forEach(prop => { data[prop] = entry[prop]; }); });

View File

@ -7,6 +7,8 @@
*/ */
import {AUTO_STYLE} from '@angular/core'; import {AUTO_STYLE} from '@angular/core';
import {isPresent} from '../facade/lang';
import {AnimationPlayer} from '../private_import_core'; import {AnimationPlayer} from '../private_import_core';
import {getDOM} from './dom_adapter'; import {getDOM} from './dom_adapter';
@ -21,13 +23,22 @@ export class WebAnimationsPlayer implements AnimationPlayer {
private _finished = false; private _finished = false;
private _started = false; private _started = false;
private _destroyed = false; private _destroyed = false;
private _finalKeyframe: {[key: string]: string | number};
public parentPlayer: AnimationPlayer = null; public parentPlayer: AnimationPlayer = null;
public previousStyles: {[styleName: string]: string | number};
constructor( constructor(
public element: any, public keyframes: {[key: string]: string | number}[], public element: any, public keyframes: {[key: string]: string | number}[],
public options: {[key: string]: string | number}) { public options: {[key: string]: string | number},
previousPlayers: WebAnimationsPlayer[] = []) {
this._duration = <number>options['duration']; this._duration = <number>options['duration'];
this.previousStyles = {};
previousPlayers.forEach(player => {
let styles = player._captureStyles();
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
});
} }
private _onFinish() { private _onFinish() {
@ -44,14 +55,30 @@ export class WebAnimationsPlayer implements AnimationPlayer {
const keyframes = this.keyframes.map(styles => { const keyframes = this.keyframes.map(styles => {
const formattedKeyframe: {[key: string]: string | number} = {}; const formattedKeyframe: {[key: string]: string | number} = {};
Object.keys(styles).forEach(prop => { Object.keys(styles).forEach((prop, index) => {
const value = styles[prop]; let value = styles[prop];
formattedKeyframe[prop] = value == AUTO_STYLE ? _computeStyle(this.element, prop) : value; if (value == AUTO_STYLE) {
value = _computeStyle(this.element, prop);
}
if (value != undefined) {
formattedKeyframe[prop] = value;
}
}); });
return formattedKeyframe; return formattedKeyframe;
}); });
const previousStyleProps = Object.keys(this.previousStyles);
if (previousStyleProps.length) {
let startingKeyframe = findStartingKeyframe(keyframes);
previousStyleProps.forEach(prop => {
if (isPresent(startingKeyframe[prop])) {
startingKeyframe[prop] = this.previousStyles[prop];
}
});
}
this._player = this._triggerWebAnimation(this.element, keyframes, this.options); this._player = this._triggerWebAnimation(this.element, keyframes, this.options);
this._finalKeyframe = _copyKeyframeStyles(keyframes[keyframes.length - 1]);
// this is required so that the player doesn't start to animate right away // this is required so that the player doesn't start to animate right away
this._resetDomPlayerState(); this._resetDomPlayerState();
@ -119,8 +146,47 @@ export class WebAnimationsPlayer implements AnimationPlayer {
setPosition(p: number): void { this._player.currentTime = p * this.totalTime; } setPosition(p: number): void { this._player.currentTime = p * this.totalTime; }
getPosition(): number { return this._player.currentTime / this.totalTime; } getPosition(): number { return this._player.currentTime / this.totalTime; }
private _captureStyles(): {[prop: string]: string | number} {
const styles: {[key: string]: string | number} = {};
if (this.hasStarted()) {
Object.keys(this._finalKeyframe).forEach(prop => {
if (prop != 'offset') {
styles[prop] =
this._finished ? this._finalKeyframe[prop] : _computeStyle(this.element, prop);
}
});
}
return styles;
}
} }
function _computeStyle(element: any, prop: string): string { function _computeStyle(element: any, prop: string): string {
return getDOM().getComputedStyle(element)[prop]; return getDOM().getComputedStyle(element)[prop];
} }
function _copyKeyframeStyles(styles: {[style: string]: string | number}):
{[style: string]: string | number} {
const newStyles: {[style: string]: string | number} = {};
Object.keys(styles).forEach(prop => {
if (prop != 'offset') {
newStyles[prop] = styles[prop];
}
});
return newStyles;
}
function findStartingKeyframe(keyframes: {[prop: string]: string | number}[]):
{[prop: string]: string | number} {
let startingKeyframe = keyframes[0];
// it's important that we find the LAST keyframe
// to ensure that style overidding is final.
for (let i = 1; i < keyframes.length; i++) {
const kf = keyframes[i];
const offset = kf['offset'];
if (offset !== 0) break;
startingKeyframe = kf;
}
return startingKeyframe;
}

View File

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
import {el} from '@angular/platform-browser/testing/browser_util'; import {el} from '@angular/platform-browser/testing/browser_util';
import {DomAnimatePlayer} from '../../src/dom/dom_animate_player'; import {DomAnimatePlayer} from '../../src/dom/dom_animate_player';
@ -48,8 +47,7 @@ export function main() {
it('should use a fill mode of `both`', () => { it('should use a fill mode of `both`', () => {
const startingStyles = _makeStyles({}); const startingStyles = _makeStyles({});
const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'linear', []);
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'linear');
const details = _formatOptions(player); const details = _formatOptions(player);
const options = details['options']; const options = details['options'];
expect(options['fill']).toEqual('both'); expect(options['fill']).toEqual('both');
@ -58,8 +56,7 @@ export function main() {
it('should apply the provided easing', () => { it('should apply the provided easing', () => {
const startingStyles = _makeStyles({}); const startingStyles = _makeStyles({});
const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'ease-out', []);
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'ease-out');
const details = _formatOptions(player); const details = _formatOptions(player);
const options = details['options']; const options = details['options'];
expect(options['easing']).toEqual('ease-out'); expect(options['easing']).toEqual('ease-out');
@ -68,8 +65,7 @@ export function main() {
it('should only apply the provided easing if present', () => { it('should only apply the provided easing if present', () => {
const startingStyles = _makeStyles({}); const startingStyles = _makeStyles({});
const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, null, []);
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, null);
const details = _formatOptions(player); const details = _formatOptions(player);
const options = details['options']; const options = details['options'];
const keys = Object.keys(options); const keys = Object.keys(options);

View File

@ -6,7 +6,9 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {MockAnimationPlayer, beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal'; import {AUTO_STYLE, AnimationPlayer} from '@angular/core';
import {MockAnimationPlayer} from '@angular/core/testing/testing_internal';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {el} from '@angular/platform-browser/testing/browser_util'; import {el} from '@angular/platform-browser/testing/browser_util';
import {DomAnimatePlayer} from '../../src/dom/dom_animate_player'; import {DomAnimatePlayer} from '../../src/dom/dom_animate_player';
@ -18,14 +20,16 @@ class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer {
constructor( constructor(
public element: HTMLElement, public keyframes: {[key: string]: string | number}[], public element: HTMLElement, public keyframes: {[key: string]: string | number}[],
public options: {[key: string]: string | number}) { public options: {[key: string]: string | number},
super(element, keyframes, options); public previousPlayers: WebAnimationsPlayer[] = []) {
super(element, keyframes, options, previousPlayers);
} }
get domPlayer() { return this._overriddenDomPlayer; } get domPlayer() { return this._overriddenDomPlayer; }
/** @internal */ /** @internal */
_triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer { _triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer {
this._overriddenDomPlayer._capture('trigger', {elm, keyframes, options});
return this._overriddenDomPlayer; return this._overriddenDomPlayer;
} }
} }
@ -33,7 +37,7 @@ class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer {
export function main() { export function main() {
function makePlayer(): {[key: string]: any} { function makePlayer(): {[key: string]: any} {
const someElm = el('<div></div>'); const someElm = el('<div></div>');
const player = new ExtendedWebAnimationsPlayer(someElm, [], {}); const player = new ExtendedWebAnimationsPlayer(someElm, [{}, {}], {}, []);
player.init(); player.init();
return {'captures': player.domPlayer.captures, 'player': player}; return {'captures': player.domPlayer.captures, 'player': player};
} }
@ -156,5 +160,72 @@ export function main() {
player.destroy(); player.destroy();
expect(captures['cancel'].length).toBe(1); expect(captures['cancel'].length).toBe(1);
}); });
it('should resolve auto styles based on what is computed from the provided element', () => {
const elm = el('<div></div>');
document.body.appendChild(elm); // required for getComputedStyle() to work
elm.style.opacity = '0.5';
const player = new ExtendedWebAnimationsPlayer(
elm, [{opacity: AUTO_STYLE}, {opacity: '1'}], {duration: 1000}, []);
player.init();
const data = player.domPlayer.captures['trigger'][0];
expect(data['keyframes']).toEqual([{opacity: '0.5'}, {opacity: '1'}]);
});
describe('previousStyle', () => {
it('should merge keyframe styles based on the previous styles passed in when the player has finished its operation',
() => {
const elm = el('<div></div>');
const previousStyles = {width: '100px', height: '666px'};
const previousPlayer =
new ExtendedWebAnimationsPlayer(elm, [previousStyles, previousStyles], {}, []);
previousPlayer.play();
previousPlayer.finish();
const player = new ExtendedWebAnimationsPlayer(
elm,
[
{width: '0px', height: '0px', opacity: 0, offset: 0},
{width: '0px', height: '0px', opacity: 1, offset: 1}
],
{duration: 1000}, [previousPlayer]);
player.init();
const data = player.domPlayer.captures['trigger'][0];
expect(data['keyframes']).toEqual([
{width: '100px', height: '666px', opacity: 0, offset: 0},
{width: '0px', height: '0px', opacity: 1, offset: 1}
]);
});
it('should properly calculate the previous styles for the player even when its currently playing',
() => {
if (!getDOM().supportsWebAnimation()) return;
const elm = el('<div></div>');
document.body.appendChild(elm);
const fromStyles = {width: '100px', height: '666px'};
const toStyles = {width: '50px', height: '333px'};
const previousPlayer =
new WebAnimationsPlayer(elm, [fromStyles, toStyles], {duration: 1000}, []);
previousPlayer.play();
previousPlayer.setPosition(0.5);
previousPlayer.pause();
const newStyles = {width: '0px', height: '0px'};
const player = new WebAnimationsPlayer(
elm, [newStyles, newStyles], {duration: 1000}, [previousPlayer]);
player.init();
const data = player.previousStyles;
expect(player.previousStyles).toEqual({width: '75px', height: '499.5px'});
});
});
}); });
} }

View File

@ -9,19 +9,29 @@
import {AnimationPlayer} from '@angular/core'; import {AnimationPlayer} from '@angular/core';
import {MockAnimationPlayer} from '@angular/core/testing/testing_internal'; import {MockAnimationPlayer} from '@angular/core/testing/testing_internal';
import {AnimationDriver} from '@angular/platform-browser'; import {AnimationDriver} from '@angular/platform-browser';
import {ListWrapper} from './facade/collection';
import {AnimationKeyframe, AnimationStyles} from './private_import_core'; import {AnimationKeyframe, AnimationStyles} from './private_import_core';
export class MockAnimationDriver extends AnimationDriver { export class MockAnimationDriver extends AnimationDriver {
public log: {[key: string]: any}[] = []; public log: {[key: string]: any}[] = [];
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
const player = new MockAnimationPlayer(); previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
const mockPlayers = <MockAnimationPlayer[]>previousPlayers.filter(
player => player instanceof MockAnimationPlayer);
const normalizedStartingStyles = _serializeStyles(startingStyles);
const normalizedKeyframes = _serializeKeyframes(keyframes);
const player =
new MockAnimationPlayer(normalizedStartingStyles, normalizedKeyframes, previousPlayers);
this.log.push({ this.log.push({
'element': element, 'element': element,
'startingStyles': _serializeStyles(startingStyles), 'startingStyles': normalizedStartingStyles,
'previousStyles': player.previousStyles,
'keyframes': keyframes, 'keyframes': keyframes,
'keyframeLookup': _serializeKeyframes(keyframes), 'keyframeLookup': normalizedKeyframes,
'duration': duration, 'duration': duration,
'delay': delay, 'delay': delay,
'easing': easing, 'easing': easing,

View File

@ -206,9 +206,10 @@ export class ServerRenderer implements Renderer {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
return this._animationDriver.animate( return this._animationDriver.animate(
element, startingStyles, keyframes, duration, delay, easing); element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
} }
} }

View File

@ -89,7 +89,7 @@ export class MessageBasedRenderer {
'animate', 'animate',
[ [
RenderStoreObject, RenderStoreObject, PRIMITIVE, PRIMITIVE, PRIMITIVE, PRIMITIVE, RenderStoreObject, RenderStoreObject, PRIMITIVE, PRIMITIVE, PRIMITIVE, PRIMITIVE,
PRIMITIVE, PRIMITIVE PRIMITIVE, PRIMITIVE, PRIMITIVE
], ],
this._animate.bind(this)); this._animate.bind(this));
@ -248,8 +248,14 @@ export class MessageBasedRenderer {
private _animate( private _animate(
renderer: Renderer, element: any, startingStyles: any, keyframes: any[], duration: number, renderer: Renderer, element: any, startingStyles: any, keyframes: any[], duration: number,
delay: number, easing: string, playerId: any) { delay: number, easing: string, previousPlayers: number[], playerId: any) {
const player = renderer.animate(element, startingStyles, keyframes, duration, delay, easing); let normalizedPreviousPlayers: AnimationPlayer[];
if (previousPlayers && previousPlayers.length) {
normalizedPreviousPlayers =
previousPlayers.map(playerId => this._renderStore.deserialize(playerId));
}
const player = renderer.animate(
element, startingStyles, keyframes, duration, delay, easing, normalizedPreviousPlayers);
this._renderStore.store(player, playerId); this._renderStore.store(player, playerId);
} }

View File

@ -16,6 +16,7 @@ import {MessageBus} from '../shared/message_bus';
import {EVENT_CHANNEL, RENDERER_CHANNEL} from '../shared/messaging_api'; import {EVENT_CHANNEL, RENDERER_CHANNEL} from '../shared/messaging_api';
import {RenderStore} from '../shared/render_store'; import {RenderStore} from '../shared/render_store';
import {ANIMATION_WORKER_PLAYER_PREFIX, RenderStoreObject, Serializer} from '../shared/serializer'; import {ANIMATION_WORKER_PLAYER_PREFIX, RenderStoreObject, Serializer} from '../shared/serializer';
import {deserializeGenericEvent} from './event_deserializer'; import {deserializeGenericEvent} from './event_deserializer';
@Injectable() @Injectable()
@ -239,13 +240,16 @@ export class WebWorkerRenderer implements Renderer, RenderStoreObject {
animate( animate(
renderElement: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], renderElement: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
const playerId = this._rootRenderer.allocateId(); const playerId = this._rootRenderer.allocateId();
const previousPlayerIds: number[] =
previousPlayers.map(player => this._rootRenderer.renderStore.serialize(player));
this._runOnService('animate', [ this._runOnService('animate', [
new FnArg(renderElement, RenderStoreObject), new FnArg(startingStyles, null), new FnArg(renderElement, RenderStoreObject), new FnArg(startingStyles, null),
new FnArg(keyframes, null), new FnArg(duration, null), new FnArg(delay, null), new FnArg(keyframes, null), new FnArg(duration, null), new FnArg(delay, null),
new FnArg(easing, null), new FnArg(playerId, null) new FnArg(easing, null), new FnArg(previousPlayerIds, null), new FnArg(playerId, null)
]); ]);
const player = new _AnimationWorkerRendererPlayer(this._rootRenderer, renderElement); const player = new _AnimationWorkerRendererPlayer(this._rootRenderer, renderElement);
@ -325,7 +329,7 @@ export class WebWorkerRenderNode {
animationPlayerEvents = new AnimationPlayerEmitter(); animationPlayerEvents = new AnimationPlayerEmitter();
} }
class _AnimationWorkerRendererPlayer implements AnimationPlayer, RenderStoreObject { class _AnimationWorkerRendererPlayer implements RenderStoreObject {
public parentPlayer: AnimationPlayer = null; public parentPlayer: AnimationPlayer = null;
private _destroyed: boolean = false; private _destroyed: boolean = false;

View File

@ -289,6 +289,30 @@ export function main() {
expect(player.log.indexOf('destroy') >= 0).toBe(true); expect(player.log.indexOf('destroy') >= 0).toBe(true);
})); }));
it('should properly transition to the next animation if the current one is cancelled',
fakeAsync(() => {
const fixture = TestBed.createComponent(AnimationCmp);
const cmp = fixture.componentInstance;
cmp.state = 'on';
fixture.detectChanges();
flushMicrotasks();
let player = <MockAnimationPlayer>uiDriver.log.shift()['player'];
player.finish();
player = <MockAnimationPlayer>uiDriver.log.shift()['player'];
player.setPosition(0.5);
uiDriver.log = [];
cmp.state = 'off';
fixture.detectChanges();
flushMicrotasks();
const step = uiDriver.log.shift();
expect(step['previousStyles']).toEqual({opacity: AUTO_STYLE, fontSize: AUTO_STYLE});
}));
}); });
} }

View File

@ -40,6 +40,29 @@ describe('WebWorkers Animations', function() {
browser.wait(() => boxElm.getSize().then(sizes => sizes['width'] > 750), 1000); browser.wait(() => boxElm.getSize().then(sizes => sizes['width'] > 750), 1000);
}); });
it('should cancel the animation midway and continue from where it left off', () => {
browser.ignoreSynchronization = true;
browser.get(URL);
waitForBootstrap();
const elem = element(by.css(selector + ' .box'));
const btn = element(by.css(selector + ' button'));
const getWidth = () => elem.getSize().then((sizes: any) => sizes['width']);
btn.click();
browser.sleep(250);
btn.click();
expect(getWidth()).toBeLessThan(600);
browser.sleep(500);
expect(getWidth()).toBeLessThan(50);
});
function waitForBootstrap() { function waitForBootstrap() {
browser.wait(protractor.until.elementLocated(by.css(selector + ' .box')), 5000) browser.wait(protractor.until.elementLocated(by.css(selector + ' .box')), 5000)
.then(() => {}, () => { .then(() => {}, () => {

View File

@ -756,7 +756,7 @@ export declare class RenderComponentType {
/** @experimental */ /** @experimental */
export declare abstract class Renderer { export declare abstract class Renderer {
abstract animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string): AnimationPlayer; abstract animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string, previousPlayers?: AnimationPlayer[]): AnimationPlayer;
abstract attachViewAfter(node: any, viewRootNodes: any[]): void; abstract attachViewAfter(node: any, viewRootNodes: any[]): void;
abstract createElement(parentElement: any, name: string, debugInfo?: RenderDebugInfo): any; abstract createElement(parentElement: any, name: string, debugInfo?: RenderDebugInfo): any;
abstract createTemplateAnchor(parentElement: any, debugInfo?: RenderDebugInfo): any; abstract createTemplateAnchor(parentElement: any, debugInfo?: RenderDebugInfo): any;

View File

@ -1,6 +1,6 @@
/** @experimental */ /** @experimental */
export declare abstract class AnimationDriver { export declare abstract class AnimationDriver {
abstract animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string): AnimationPlayer; abstract animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string, previousPlayers?: AnimationPlayer[]): AnimationPlayer;
static NOOP: AnimationDriver; static NOOP: AnimationDriver;
} }