fix(animations): retain styling when transition destinations are changed (#12208)
Closes #9661 Closes #12208
This commit is contained in:
parent
46023e4792
commit
9de76ebfa5
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -84,6 +84,8 @@ export function balanceAnimationKeyframes(
|
|||
firstKeyframe.styles.styles.push(extraFirstKeyframeStyles);
|
||||
}
|
||||
|
||||
collectAndResolveStyles(collectedStyles, [finalStateStyles]);
|
||||
|
||||
return keyframes;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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: `
|
||||
<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',
|
||||
fakeAsync(() => {
|
||||
TestBed.overrideComponent(DummyIfCmp, {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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, <WebAnimationsPlayer[]>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]; }); });
|
||||
|
|
|
@ -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 = <number>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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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('<div></div>');
|
||||
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('<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'});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 = <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({
|
||||
'element': element,
|
||||
'startingStyles': _serializeStyles(startingStyles),
|
||||
'startingStyles': normalizedStartingStyles,
|
||||
'previousStyles': player.previousStyles,
|
||||
'keyframes': keyframes,
|
||||
'keyframeLookup': _serializeKeyframes(keyframes),
|
||||
'keyframeLookup': normalizedKeyframes,
|
||||
'duration': duration,
|
||||
'delay': delay,
|
||||
'easing': easing,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = <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});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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(() => {}, () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue