From 9de76ebfa545ad0a786c63f166b2b966b996e64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 14 Nov 2016 16:59:06 -0800 Subject: [PATCH] fix(animations): retain styling when transition destinations are changed (#12208) Closes #9661 Closes #12208 --- .../src/animation/animation_compiler.ts | 55 ++++++++----- .../src/animation/animation_group_player.ts | 4 +- .../core/src/animation/animation_player.ts | 2 +- .../animation/animation_sequence_player.ts | 4 +- .../src/animation/animation_style_util.ts | 2 + .../@angular/core/src/debug/debug_renderer.ts | 8 +- .../core/src/linker/animation_view_context.ts | 26 ++++-- modules/@angular/core/src/render/api.ts | 3 +- .../animation/animation_integration_spec.ts | 46 +++++++++++ .../core/testing/mock_animation_player.ts | 46 ++++++++++- .../src/dom/animation_driver.ts | 6 +- .../platform-browser/src/dom/dom_renderer.ts | 5 +- .../src/dom/web_animations_driver.ts | 16 ++-- .../src/dom/web_animations_player.ts | 74 ++++++++++++++++- .../test/dom/web_animations_driver_spec.ts | 10 +-- .../test/dom/web_animations_player_spec.ts | 79 ++++++++++++++++++- .../testing/mock_animation_driver.ts | 18 ++++- .../platform-server/src/server_renderer.ts | 5 +- .../src/web_workers/ui/renderer.ts | 12 ++- .../src/web_workers/worker/renderer.ts | 10 ++- .../renderer_animation_integration_spec.ts | 24 ++++++ .../web_workers/animations/animations_spec.ts | 23 ++++++ tools/public_api_guard/core/index.d.ts | 2 +- .../platform-browser/index.d.ts | 2 +- 24 files changed, 403 insertions(+), 79 deletions(-) diff --git a/modules/@angular/compiler/src/animation/animation_compiler.ts b/modules/@angular/compiler/src/animation/animation_compiler.ts index c68ac228e8..0b91ac7004 100644 --- a/modules/@angular/compiler/src/animation/animation_compiler.ts +++ b/modules/@angular/compiler/src/animation/animation_compiler.ts @@ -41,7 +41,9 @@ const _ANIMATION_TIME_VAR = o.variable('totalTime'); const _ANIMATION_START_STATE_STYLES_VAR = o.variable('startStateStyles'); const _ANIMATION_END_STATE_STYLES_VAR = o.variable('endStateStyles'); 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 { private _fnVarName: string; @@ -110,10 +112,15 @@ class _AnimationBuilder implements AnimationAstVisitor { _callAnimateMethod( ast: AnimationStepAst, startingStylesExpr: any, keyframesExpr: any, context: _AnimationBuilderContext) { + let previousStylesValue: o.Expression = _EMPTY_ARRAY; + if (context.isExpectingFirstAnimateStep) { + previousStylesValue = _PREVIOUS_ANIMATION_PLAYERS; + context.isExpectingFirstAnimateStep = false; + } context.totalTransitionTime += ast.duration + ast.delay; return _ANIMATION_FACTORY_RENDERER_VAR.callMethod('animate', [ _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.isExpectingFirstStyleStep = true; + context.isExpectingFirstAnimateStep = true; const stateChangePreconditions: o.Expression[] = []; @@ -187,17 +195,16 @@ class _AnimationBuilder implements AnimationAstVisitor { context.stateMap.registerState(DEFAULT_STATE, {}); const statements: o.Statement[] = []; - statements.push(_ANIMATION_FACTORY_VIEW_CONTEXT - .callMethod( - 'cancelActiveAnimation', + statements.push(_PREVIOUS_ANIMATION_PLAYERS + .set(_ANIMATION_FACTORY_VIEW_CONTEXT.callMethod( + 'getAnimationPlayers', [ _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName), _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_TIME_VAR.set(o.literal(0)).toDeclStmt()); @@ -223,17 +230,6 @@ class _AnimationBuilder implements AnimationAstVisitor { 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))); // this check ensures that the animation factory always returns a player @@ -269,6 +265,22 @@ class _AnimationBuilder implements AnimationAstVisitor { ])]) .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 .callMethod( 'queueAnimation', @@ -304,7 +316,7 @@ class _AnimationBuilder implements AnimationAstVisitor { const lookupMap: any[] = []; Object.keys(context.stateMap.states).forEach(stateName => { const value = context.stateMap.states[stateName]; - let variableValue = EMPTY_MAP; + let variableValue = _EMPTY_MAP; if (isPresent(value)) { const styleMap: any[] = []; Object.keys(value).forEach(key => { styleMap.push([key, o.literal(value[key])]); }); @@ -324,6 +336,7 @@ class _AnimationBuilderContext { stateMap = new _AnimationBuilderStateMap(); endStateAnimateStep: AnimationStepAst = null; isExpectingFirstStyleStep = false; + isExpectingFirstAnimateStep = false; totalTransitionTime = 0; } diff --git a/modules/@angular/core/src/animation/animation_group_player.ts b/modules/@angular/core/src/animation/animation_group_player.ts index a3a637ba60..97bef23b95 100644 --- a/modules/@angular/core/src/animation/animation_group_player.ts +++ b/modules/@angular/core/src/animation/animation_group_player.ts @@ -88,7 +88,7 @@ export class AnimationGroupPlayer implements AnimationPlayer { this._started = false; } - setPosition(p: any /** TODO #9100 */): void { + setPosition(p: number): void { this._players.forEach(player => { player.setPosition(p); }); } @@ -100,4 +100,6 @@ export class AnimationGroupPlayer implements AnimationPlayer { }); return min; } + + get players(): AnimationPlayer[] { return this._players; } } diff --git a/modules/@angular/core/src/animation/animation_player.ts b/modules/@angular/core/src/animation/animation_player.ts index ed22477cd7..c086bf09fb 100644 --- a/modules/@angular/core/src/animation/animation_player.ts +++ b/modules/@angular/core/src/animation/animation_player.ts @@ -56,6 +56,6 @@ export class NoOpAnimationPlayer implements AnimationPlayer { finish(): void { this._onFinish(); } destroy(): void {} reset(): void {} - setPosition(p: any /** TODO #9100 */): void {} + setPosition(p: number): void {} getPosition(): number { return 0; } } diff --git a/modules/@angular/core/src/animation/animation_sequence_player.ts b/modules/@angular/core/src/animation/animation_sequence_player.ts index 3c2e26e7af..c12e31e77b 100644 --- a/modules/@angular/core/src/animation/animation_sequence_player.ts +++ b/modules/@angular/core/src/animation/animation_sequence_player.ts @@ -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(); } + + get players(): AnimationPlayer[] { return this._players; } } diff --git a/modules/@angular/core/src/animation/animation_style_util.ts b/modules/@angular/core/src/animation/animation_style_util.ts index ea97194bca..e426ce4547 100644 --- a/modules/@angular/core/src/animation/animation_style_util.ts +++ b/modules/@angular/core/src/animation/animation_style_util.ts @@ -84,6 +84,8 @@ export function balanceAnimationKeyframes( firstKeyframe.styles.styles.push(extraFirstKeyframeStyles); } + collectAndResolveStyles(collectedStyles, [finalStateStyles]); + return keyframes; } diff --git a/modules/@angular/core/src/debug/debug_renderer.ts b/modules/@angular/core/src/debug/debug_renderer.ts index 0fee0a25af..8350747696 100644 --- a/modules/@angular/core/src/debug/debug_renderer.ts +++ b/modules/@angular/core/src/debug/debug_renderer.ts @@ -22,7 +22,7 @@ export class DebugDomRootRenderer implements RootRenderer { } } -export class DebugDomRenderer implements Renderer { +export class DebugDomRenderer { constructor(private _delegate: Renderer) {} selectRootElement(selectorOrNode: string|any, debugInfo?: RenderDebugInfo): any { @@ -150,7 +150,9 @@ export class DebugDomRenderer implements Renderer { animate( element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], - duration: number, delay: number, easing: string): AnimationPlayer { - return this._delegate.animate(element, startingStyles, keyframes, duration, delay, easing); + duration: number, delay: number, easing: string, + previousPlayers: AnimationPlayer[] = []): AnimationPlayer { + return this._delegate.animate( + element, startingStyles, keyframes, duration, delay, easing, previousPlayers); } } diff --git a/modules/@angular/core/src/linker/animation_view_context.ts b/modules/@angular/core/src/linker/animation_view_context.ts index c690f31181..77a294e792 100644 --- a/modules/@angular/core/src/linker/animation_view_context.ts +++ b/modules/@angular/core/src/linker/animation_view_context.ts @@ -8,8 +8,9 @@ import {AnimationGroupPlayer} from '../animation/animation_group_player'; import {AnimationPlayer} from '../animation/animation_player'; 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 {ListWrapper} from '../facade/collection'; export class AnimationViewContext { private _players = new ViewAnimationMap(); @@ -30,15 +31,26 @@ export class AnimationViewContext { this._players.set(element, animationName, player); } - cancelActiveAnimation(element: any, animationName: string, removeAllAnimations: boolean = false): - void { + getAnimationPlayers(element: any, animationName: string, removeAllAnimations: boolean = false): + AnimationPlayer[] { + const players: AnimationPlayer[] = []; if (removeAllAnimations) { - this._players.findAllPlayersByElement(element).forEach(player => player.destroy()); + this._players.findAllPlayersByElement(element).forEach( + player => { _recursePlayers(player, players); }); } else { - const player = this._players.find(element, animationName); - if (player) { - player.destroy(); + const currentPlayer = this._players.find(element, animationName); + if (currentPlayer) { + _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); } } diff --git a/modules/@angular/core/src/render/api.ts b/modules/@angular/core/src/render/api.ts index baffd7a372..cb70bdb3c9 100644 --- a/modules/@angular/core/src/render/api.ts +++ b/modules/@angular/core/src/render/api.ts @@ -88,7 +88,8 @@ export abstract class Renderer { abstract animate( element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], - duration: number, delay: number, easing: string): AnimationPlayer; + duration: number, delay: number, easing: string, + previousPlayers?: AnimationPlayer[]): AnimationPlayer; } /** diff --git a/modules/@angular/core/test/animation/animation_integration_spec.ts b/modules/@angular/core/test/animation/animation_integration_spec.ts index 2908365702..bfd13c9964 100644 --- a/modules/@angular/core/test/animation/animation_integration_spec.ts +++ b/modules/@angular/core/test/animation/animation_integration_spec.ts @@ -1854,6 +1854,8 @@ function declareTests({useJit}: {useJit: boolean}) { let animation = driver.log.pop(); let kf = animation['keyframeLookup']; expect(kf[1]).toEqual([1, {'background': 'green'}]); + let player = animation['player']; + player.finish(); cmp.exp = 'blue'; fixture.detectChanges(); @@ -1863,6 +1865,8 @@ function declareTests({useJit}: {useJit: boolean}) { kf = animation['keyframeLookup']; expect(kf[0]).toEqual([0, {'background': 'green'}]); expect(kf[1]).toEqual([1, {'background': 'grey'}]); + player = animation['player']; + player.finish(); cmp.exp = 'red'; fixture.detectChanges(); @@ -1872,6 +1876,8 @@ function declareTests({useJit}: {useJit: boolean}) { kf = animation['keyframeLookup']; expect(kf[0]).toEqual([0, {'background': 'grey'}]); expect(kf[1]).toEqual([1, {'background': 'red'}]); + player = animation['player']; + player.finish(); cmp.exp = 'orange'; fixture.detectChanges(); @@ -1881,6 +1887,8 @@ function declareTests({useJit}: {useJit: boolean}) { kf = animation['keyframeLookup']; expect(kf[0]).toEqual([0, {'background': 'red'}]); 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', @@ -1911,6 +1919,44 @@ function declareTests({useJit}: {useJit: boolean}) { 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: ` +
+ `, + 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', fakeAsync(() => { TestBed.overrideComponent(DummyIfCmp, { diff --git a/modules/@angular/core/testing/mock_animation_player.ts b/modules/@angular/core/testing/mock_animation_player.ts index 7eb777ca61..c16a00e661 100644 --- a/modules/@angular/core/testing/mock_animation_player.ts +++ b/modules/@angular/core/testing/mock_animation_player.ts @@ -5,8 +5,7 @@ * 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 */ - -import {AnimationPlayer} from '@angular/core'; +import {AUTO_STYLE, AnimationPlayer} from '@angular/core'; export class MockAnimationPlayer implements AnimationPlayer { private _onDoneFns: Function[] = []; @@ -16,8 +15,21 @@ export class MockAnimationPlayer implements AnimationPlayer { private _started = false; 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 { 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; } + + 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; + } } diff --git a/modules/@angular/platform-browser/src/dom/animation_driver.ts b/modules/@angular/platform-browser/src/dom/animation_driver.ts index 915c9a884d..7967a08eb9 100644 --- a/modules/@angular/platform-browser/src/dom/animation_driver.ts +++ b/modules/@angular/platform-browser/src/dom/animation_driver.ts @@ -13,7 +13,8 @@ import {AnimationKeyframe, AnimationStyles, NoOpAnimationPlayer} from '../privat class _NoOpAnimationDriver implements AnimationDriver { animate( 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(); } } @@ -25,5 +26,6 @@ export abstract class AnimationDriver { static NOOP: AnimationDriver = new _NoOpAnimationDriver(); abstract animate( element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], - duration: number, delay: number, easing: string): AnimationPlayer; + duration: number, delay: number, easing: string, + previousPlayers?: AnimationPlayer[]): AnimationPlayer; } diff --git a/modules/@angular/platform-browser/src/dom/dom_renderer.ts b/modules/@angular/platform-browser/src/dom/dom_renderer.ts index bb96fbb7e1..bf64552c87 100644 --- a/modules/@angular/platform-browser/src/dom/dom_renderer.ts +++ b/modules/@angular/platform-browser/src/dom/dom_renderer.ts @@ -260,9 +260,10 @@ export class DomRenderer implements Renderer { animate( 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( - element, startingStyles, keyframes, duration, delay, easing); + element, startingStyles, keyframes, duration, delay, easing, previousPlayers); } } diff --git a/modules/@angular/platform-browser/src/dom/web_animations_driver.ts b/modules/@angular/platform-browser/src/dom/web_animations_driver.ts index 57f6e004e7..7486333667 100644 --- a/modules/@angular/platform-browser/src/dom/web_animations_driver.ts +++ b/modules/@angular/platform-browser/src/dom/web_animations_driver.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {AnimationPlayer} from '@angular/core'; import {isPresent} from '../facade/lang'; import {AnimationKeyframe, AnimationStyles} from '../private_import_core'; @@ -15,17 +16,18 @@ import {WebAnimationsPlayer} from './web_animations_player'; export class WebAnimationsDriver implements AnimationDriver { animate( 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 startingStyleLookup: {[key: string]: string | number} = {}; if (isPresent(startingStyles) && startingStyles.styles.length > 0) { - startingStyleLookup = _populateStyles(element, startingStyles, {}); + startingStyleLookup = _populateStyles(startingStyles, {}); startingStyleLookup['offset'] = 0; formattedSteps.push(startingStyleLookup); } keyframes.forEach((keyframe: AnimationKeyframe) => { - const data = _populateStyles(element, keyframe.styles, startingStyleLookup); + const data = _populateStyles(keyframe.styles, startingStyleLookup); data['offset'] = keyframe.offset; formattedSteps.push(data); }); @@ -52,13 +54,13 @@ export class WebAnimationsDriver implements AnimationDriver { playerOptions['easing'] = easing; } - return new WebAnimationsPlayer(element, formattedSteps, playerOptions); + return new WebAnimationsPlayer( + element, formattedSteps, playerOptions, previousPlayers); } } -function _populateStyles( - element: any, styles: AnimationStyles, - defaultStyles: {[key: string]: string | number}): {[key: string]: string | number} { +function _populateStyles(styles: AnimationStyles, defaultStyles: {[key: string]: string | number}): + {[key: string]: string | number} { const data: {[key: string]: string | number} = {}; styles.styles.forEach( (entry) => { Object.keys(entry).forEach(prop => { data[prop] = entry[prop]; }); }); diff --git a/modules/@angular/platform-browser/src/dom/web_animations_player.ts b/modules/@angular/platform-browser/src/dom/web_animations_player.ts index e57c82d145..627a8055c6 100644 --- a/modules/@angular/platform-browser/src/dom/web_animations_player.ts +++ b/modules/@angular/platform-browser/src/dom/web_animations_player.ts @@ -7,6 +7,8 @@ */ import {AUTO_STYLE} from '@angular/core'; + +import {isPresent} from '../facade/lang'; import {AnimationPlayer} from '../private_import_core'; import {getDOM} from './dom_adapter'; @@ -21,13 +23,22 @@ export class WebAnimationsPlayer implements AnimationPlayer { private _finished = false; private _started = false; private _destroyed = false; + private _finalKeyframe: {[key: string]: string | number}; public parentPlayer: AnimationPlayer = null; + public previousStyles: {[styleName: string]: string | number}; constructor( 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 = options['duration']; + + this.previousStyles = {}; + previousPlayers.forEach(player => { + let styles = player._captureStyles(); + Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]); + }); } private _onFinish() { @@ -44,14 +55,30 @@ export class WebAnimationsPlayer implements AnimationPlayer { const keyframes = this.keyframes.map(styles => { const formattedKeyframe: {[key: string]: string | number} = {}; - Object.keys(styles).forEach(prop => { - const value = styles[prop]; - formattedKeyframe[prop] = value == AUTO_STYLE ? _computeStyle(this.element, prop) : value; + Object.keys(styles).forEach((prop, index) => { + let value = styles[prop]; + if (value == AUTO_STYLE) { + value = _computeStyle(this.element, prop); + } + if (value != undefined) { + formattedKeyframe[prop] = value; + } }); 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._finalKeyframe = _copyKeyframeStyles(keyframes[keyframes.length - 1]); // this is required so that the player doesn't start to animate right away this._resetDomPlayerState(); @@ -119,8 +146,47 @@ export class WebAnimationsPlayer implements AnimationPlayer { setPosition(p: number): void { this._player.currentTime = p * 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 { 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; +} diff --git a/modules/@angular/platform-browser/test/dom/web_animations_driver_spec.ts b/modules/@angular/platform-browser/test/dom/web_animations_driver_spec.ts index e0505c631f..a608fe54fb 100644 --- a/modules/@angular/platform-browser/test/dom/web_animations_driver_spec.ts +++ b/modules/@angular/platform-browser/test/dom/web_animations_driver_spec.ts @@ -6,7 +6,6 @@ * 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 {DomAnimatePlayer} from '../../src/dom/dom_animate_player'; @@ -48,8 +47,7 @@ export function main() { it('should use a fill mode of `both`', () => { const startingStyles = _makeStyles({}); 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 options = details['options']; expect(options['fill']).toEqual('both'); @@ -58,8 +56,7 @@ export function main() { it('should apply the provided easing', () => { const startingStyles = _makeStyles({}); 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 options = details['options']; expect(options['easing']).toEqual('ease-out'); @@ -68,8 +65,7 @@ export function main() { it('should only apply the provided easing if present', () => { const startingStyles = _makeStyles({}); 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 options = details['options']; const keys = Object.keys(options); diff --git a/modules/@angular/platform-browser/test/dom/web_animations_player_spec.ts b/modules/@angular/platform-browser/test/dom/web_animations_player_spec.ts index 48b427781d..8072279501 100644 --- a/modules/@angular/platform-browser/test/dom/web_animations_player_spec.ts +++ b/modules/@angular/platform-browser/test/dom/web_animations_player_spec.ts @@ -6,7 +6,9 @@ * 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 {DomAnimatePlayer} from '../../src/dom/dom_animate_player'; @@ -18,14 +20,16 @@ class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer { constructor( public element: HTMLElement, public keyframes: {[key: string]: string | number}[], - public options: {[key: string]: string | number}) { - super(element, keyframes, options); + public options: {[key: string]: string | number}, + public previousPlayers: WebAnimationsPlayer[] = []) { + super(element, keyframes, options, previousPlayers); } get domPlayer() { return this._overriddenDomPlayer; } /** @internal */ _triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer { + this._overriddenDomPlayer._capture('trigger', {elm, keyframes, options}); return this._overriddenDomPlayer; } } @@ -33,7 +37,7 @@ class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer { export function main() { function makePlayer(): {[key: string]: any} { const someElm = el('
'); - const player = new ExtendedWebAnimationsPlayer(someElm, [], {}); + const player = new ExtendedWebAnimationsPlayer(someElm, [{}, {}], {}, []); player.init(); return {'captures': player.domPlayer.captures, 'player': player}; } @@ -156,5 +160,72 @@ export function main() { player.destroy(); expect(captures['cancel'].length).toBe(1); }); + + it('should resolve auto styles based on what is computed from the provided element', () => { + const elm = el('
'); + 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('
'); + 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('
'); + 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'}); + }); + }); }); } diff --git a/modules/@angular/platform-browser/testing/mock_animation_driver.ts b/modules/@angular/platform-browser/testing/mock_animation_driver.ts index 91c4129957..50f5e86a01 100644 --- a/modules/@angular/platform-browser/testing/mock_animation_driver.ts +++ b/modules/@angular/platform-browser/testing/mock_animation_driver.ts @@ -9,19 +9,29 @@ import {AnimationPlayer} from '@angular/core'; import {MockAnimationPlayer} from '@angular/core/testing/testing_internal'; import {AnimationDriver} from '@angular/platform-browser'; + +import {ListWrapper} from './facade/collection'; import {AnimationKeyframe, AnimationStyles} from './private_import_core'; export class MockAnimationDriver extends AnimationDriver { public log: {[key: string]: any}[] = []; animate( element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], - duration: number, delay: number, easing: string): AnimationPlayer { - const player = new MockAnimationPlayer(); + duration: number, delay: number, easing: string, + previousPlayers: AnimationPlayer[] = []): AnimationPlayer { + const mockPlayers = previousPlayers.filter( + player => player instanceof MockAnimationPlayer); + const normalizedStartingStyles = _serializeStyles(startingStyles); + const normalizedKeyframes = _serializeKeyframes(keyframes); + const player = + new MockAnimationPlayer(normalizedStartingStyles, normalizedKeyframes, previousPlayers); + this.log.push({ 'element': element, - 'startingStyles': _serializeStyles(startingStyles), + 'startingStyles': normalizedStartingStyles, + 'previousStyles': player.previousStyles, 'keyframes': keyframes, - 'keyframeLookup': _serializeKeyframes(keyframes), + 'keyframeLookup': normalizedKeyframes, 'duration': duration, 'delay': delay, 'easing': easing, diff --git a/modules/@angular/platform-server/src/server_renderer.ts b/modules/@angular/platform-server/src/server_renderer.ts index 265c4c7b3d..5a09ff65c4 100644 --- a/modules/@angular/platform-server/src/server_renderer.ts +++ b/modules/@angular/platform-server/src/server_renderer.ts @@ -206,9 +206,10 @@ export class ServerRenderer implements Renderer { animate( 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( - element, startingStyles, keyframes, duration, delay, easing); + element, startingStyles, keyframes, duration, delay, easing, previousPlayers); } } diff --git a/modules/@angular/platform-webworker/src/web_workers/ui/renderer.ts b/modules/@angular/platform-webworker/src/web_workers/ui/renderer.ts index 5561062dd1..31f76473fd 100644 --- a/modules/@angular/platform-webworker/src/web_workers/ui/renderer.ts +++ b/modules/@angular/platform-webworker/src/web_workers/ui/renderer.ts @@ -89,7 +89,7 @@ export class MessageBasedRenderer { 'animate', [ RenderStoreObject, RenderStoreObject, PRIMITIVE, PRIMITIVE, PRIMITIVE, PRIMITIVE, - PRIMITIVE, PRIMITIVE + PRIMITIVE, PRIMITIVE, PRIMITIVE ], this._animate.bind(this)); @@ -248,8 +248,14 @@ export class MessageBasedRenderer { private _animate( renderer: Renderer, element: any, startingStyles: any, keyframes: any[], duration: number, - delay: number, easing: string, playerId: any) { - const player = renderer.animate(element, startingStyles, keyframes, duration, delay, easing); + delay: number, easing: string, previousPlayers: number[], playerId: any) { + 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); } diff --git a/modules/@angular/platform-webworker/src/web_workers/worker/renderer.ts b/modules/@angular/platform-webworker/src/web_workers/worker/renderer.ts index 63ecca3ea6..314f196f2f 100644 --- a/modules/@angular/platform-webworker/src/web_workers/worker/renderer.ts +++ b/modules/@angular/platform-webworker/src/web_workers/worker/renderer.ts @@ -16,6 +16,7 @@ import {MessageBus} from '../shared/message_bus'; import {EVENT_CHANNEL, RENDERER_CHANNEL} from '../shared/messaging_api'; import {RenderStore} from '../shared/render_store'; import {ANIMATION_WORKER_PLAYER_PREFIX, RenderStoreObject, Serializer} from '../shared/serializer'; + import {deserializeGenericEvent} from './event_deserializer'; @Injectable() @@ -239,13 +240,16 @@ export class WebWorkerRenderer implements Renderer, RenderStoreObject { animate( 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 previousPlayerIds: number[] = + previousPlayers.map(player => this._rootRenderer.renderStore.serialize(player)); this._runOnService('animate', [ new FnArg(renderElement, RenderStoreObject), new FnArg(startingStyles, 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); @@ -325,7 +329,7 @@ export class WebWorkerRenderNode { animationPlayerEvents = new AnimationPlayerEmitter(); } -class _AnimationWorkerRendererPlayer implements AnimationPlayer, RenderStoreObject { +class _AnimationWorkerRendererPlayer implements RenderStoreObject { public parentPlayer: AnimationPlayer = null; private _destroyed: boolean = false; diff --git a/modules/@angular/platform-webworker/test/web_workers/worker/renderer_animation_integration_spec.ts b/modules/@angular/platform-webworker/test/web_workers/worker/renderer_animation_integration_spec.ts index 52c2d916be..418c497713 100644 --- a/modules/@angular/platform-webworker/test/web_workers/worker/renderer_animation_integration_spec.ts +++ b/modules/@angular/platform-webworker/test/web_workers/worker/renderer_animation_integration_spec.ts @@ -289,6 +289,30 @@ export function main() { 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 = uiDriver.log.shift()['player']; + player.finish(); + player = 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}); + })); }); } diff --git a/modules/playground/e2e_test/web_workers/animations/animations_spec.ts b/modules/playground/e2e_test/web_workers/animations/animations_spec.ts index 681cec60ce..f0fc711344 100644 --- a/modules/playground/e2e_test/web_workers/animations/animations_spec.ts +++ b/modules/playground/e2e_test/web_workers/animations/animations_spec.ts @@ -40,6 +40,29 @@ describe('WebWorkers Animations', function() { 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() { browser.wait(protractor.until.elementLocated(by.css(selector + ' .box')), 5000) .then(() => {}, () => { diff --git a/tools/public_api_guard/core/index.d.ts b/tools/public_api_guard/core/index.d.ts index 82641226ca..3c3f95b1b2 100644 --- a/tools/public_api_guard/core/index.d.ts +++ b/tools/public_api_guard/core/index.d.ts @@ -756,7 +756,7 @@ export declare class RenderComponentType { /** @experimental */ 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 createElement(parentElement: any, name: string, debugInfo?: RenderDebugInfo): any; abstract createTemplateAnchor(parentElement: any, debugInfo?: RenderDebugInfo): any; diff --git a/tools/public_api_guard/platform-browser/index.d.ts b/tools/public_api_guard/platform-browser/index.d.ts index 919937f148..e2557aa4a5 100644 --- a/tools/public_api_guard/platform-browser/index.d.ts +++ b/tools/public_api_guard/platform-browser/index.d.ts @@ -1,6 +1,6 @@ /** @experimental */ 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; }