Skip to content

Commit

Permalink
fix(animations): retain styling when transition destinations are chan…
Browse files Browse the repository at this point in the history
…ged (#12208)

Closes #9661
Closes #12208
  • Loading branch information
matsko authored and chuckjaz committed Nov 17, 2016
1 parent e02c180 commit 5c46c49
Show file tree
Hide file tree
Showing 24 changed files with 404 additions and 80 deletions.
55 changes: 34 additions & 21 deletions modules/@angular/compiler/src/animation/animation_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
]);
}

Expand Down Expand Up @@ -150,6 +157,7 @@ class _AnimationBuilder implements AnimationAstVisitor {

context.totalTransitionTime = 0;
context.isExpectingFirstStyleStep = true;
context.isExpectingFirstAnimateStep = true;

const stateChangePreconditions: o.Expression[] = [];

Expand Down Expand Up @@ -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());

Expand All @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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])]); });
Expand All @@ -324,6 +336,7 @@ class _AnimationBuilderContext {
stateMap = new _AnimationBuilderStateMap();
endStateAnimateStep: AnimationStepAst = null;
isExpectingFirstStyleStep = false;
isExpectingFirstAnimateStep = false;
totalTransitionTime = 0;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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); });
}

Expand All @@ -100,4 +100,6 @@ export class AnimationGroupPlayer implements AnimationPlayer {
});
return min;
}

get players(): AnimationPlayer[] { return this._players; }
}
2 changes: 1 addition & 1 deletion modules/@angular/core/src/animation/animation_player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
2 changes: 2 additions & 0 deletions modules/@angular/core/src/animation/animation_style_util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ export function balanceAnimationKeyframes(
firstKeyframe.styles.styles.push(extraFirstKeyframeStyles);
}

collectAndResolveStyles(collectedStyles, [finalStateStyles]);

return keyframes;
}

Expand Down
8 changes: 5 additions & 3 deletions modules/@angular/core/src/debug/debug_renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
26 changes: 19 additions & 7 deletions modules/@angular/core/src/linker/animation_view_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}
}
3 changes: 2 additions & 1 deletion modules/@angular/core/src/render/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
46 changes: 46 additions & 0 deletions modules/@angular/core/test/animation/animation_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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',
Expand Down Expand Up @@ -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, {
Expand Down
48 changes: 43 additions & 5 deletions modules/@angular/core/testing/mock_animation_player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand All @@ -16,8 +15,21 @@ export class MockAnimationPlayer implements AnimationPlayer {
private _started = false;

public parentPlayer: AnimationPlayer = null;

public log: any[] /** TODO #9100 */ = [];
public previousStyles: {[styleName: string]: string | number} = {};

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) {
Expand Down Expand Up @@ -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;
}
}
Loading

0 comments on commit 5c46c49

Please sign in to comment.