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

Closes #9661
Closes #12208
This commit is contained in:
Matias Niemelä
2016-11-14 16:59:06 -08:00
committed by Chuck Jazdzewski
parent e02c18049d
commit 5c46c493f2
24 changed files with 403 additions and 79 deletions

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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]; }); });

View File

@ -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;
}

View File

@ -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);

View File

@ -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'});
});
});
});
}

View File

@ -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,