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_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,8 @@ export function balanceAnimationKeyframes(
|
||||||
firstKeyframe.styles.styles.push(extraFirstKeyframeStyles);
|
firstKeyframe.styles.styles.push(extraFirstKeyframeStyles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
collectAndResolveStyles(collectedStyles, [finalStateStyles]);
|
||||||
|
|
||||||
return keyframes;
|
return keyframes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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, {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]; }); });
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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'});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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});
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(() => {}, () => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue