feat(animations): make sure animation callback reports the totalTime (#11022)

Closes #11022
This commit is contained in:
Matias Niemelä 2016-08-24 16:55:00 -07:00 committed by Victor Berchet
parent 8b782818f5
commit 4f8f8cfc66
6 changed files with 145 additions and 36 deletions

View File

@ -87,6 +87,7 @@ var _ANIMATION_FACTORY_RENDERER_VAR = _ANIMATION_FACTORY_VIEW_VAR.prop('renderer
var _ANIMATION_CURRENT_STATE_VAR = o.variable('currentState'); var _ANIMATION_CURRENT_STATE_VAR = o.variable('currentState');
var _ANIMATION_NEXT_STATE_VAR = o.variable('nextState'); var _ANIMATION_NEXT_STATE_VAR = o.variable('nextState');
var _ANIMATION_PLAYER_VAR = o.variable('player'); var _ANIMATION_PLAYER_VAR = o.variable('player');
var _ANIMATION_TIME_VAR = o.variable('totalTime');
var _ANIMATION_START_STATE_STYLES_VAR = o.variable('startStateStyles'); var _ANIMATION_START_STATE_STYLES_VAR = o.variable('startStateStyles');
var _ANIMATION_END_STATE_STYLES_VAR = o.variable('endStateStyles'); var _ANIMATION_END_STATE_STYLES_VAR = o.variable('endStateStyles');
var _ANIMATION_COLLECTED_STYLES = o.variable('collectedStyles'); var _ANIMATION_COLLECTED_STYLES = o.variable('collectedStyles');
@ -137,7 +138,8 @@ class _AnimationBuilder implements AnimationAstVisitor {
var startingStylesExpr = ast.startingStyles.visit(this, context); var startingStylesExpr = ast.startingStyles.visit(this, context);
var keyframeExpressions = var keyframeExpressions =
ast.keyframes.map(keyframeEntry => keyframeEntry.visit(this, context)); ast.keyframes.map(keyframeEntry => keyframeEntry.visit(this, context));
return this._callAnimateMethod(ast, startingStylesExpr, o.literalArr(keyframeExpressions)); return this._callAnimateMethod(
ast, startingStylesExpr, o.literalArr(keyframeExpressions), context);
} }
/** @internal */ /** @internal */
@ -149,11 +151,14 @@ class _AnimationBuilder implements AnimationAstVisitor {
o.literalArr(keyframeExpressions) o.literalArr(keyframeExpressions)
]); ]);
return this._callAnimateMethod(ast, startingStylesExpr, keyframesExpr); return this._callAnimateMethod(ast, startingStylesExpr, keyframesExpr, context);
} }
/** @internal */ /** @internal */
_callAnimateMethod(ast: AnimationStepAst, startingStylesExpr: any, keyframesExpr: any) { _callAnimateMethod(
ast: AnimationStepAst, startingStylesExpr: any, keyframesExpr: any,
context: _AnimationBuilderContext) {
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)
@ -189,6 +194,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
context.endStateAnimateStep = <AnimationStepAst>lastStep; context.endStateAnimateStep = <AnimationStepAst>lastStep;
} }
context.totalTransitionTime = 0;
context.isExpectingFirstStyleStep = true; context.isExpectingFirstStyleStep = true;
var stateChangePreconditions: o.Expression[] = []; var stateChangePreconditions: o.Expression[] = [];
@ -213,7 +219,10 @@ class _AnimationBuilder implements AnimationAstVisitor {
var precondition = var precondition =
_ANIMATION_PLAYER_VAR.equals(o.NULL_EXPR).and(reducedStateChangesPrecondition); _ANIMATION_PLAYER_VAR.equals(o.NULL_EXPR).and(reducedStateChangesPrecondition);
return new o.IfStmt(precondition, [_ANIMATION_PLAYER_VAR.set(animationPlayerExpr).toStmt()]); var animationStmt = _ANIMATION_PLAYER_VAR.set(animationPlayerExpr).toStmt();
var totalTimeStmt = _ANIMATION_TIME_VAR.set(o.literal(context.totalTransitionTime)).toStmt();
return new o.IfStmt(precondition, [animationStmt, totalTimeStmt]);
} }
visitAnimationEntry(ast: AnimationEntryAst, context: _AnimationBuilderContext): any { visitAnimationEntry(ast: AnimationEntryAst, context: _AnimationBuilderContext): any {
@ -236,6 +245,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
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( statements.push(
_ANIMATION_DEFAULT_STATE_VAR.set(this._statesMapVar.key(o.literal(DEFAULT_STATE))) _ANIMATION_DEFAULT_STATE_VAR.set(this._statesMapVar.key(o.literal(DEFAULT_STATE)))
@ -297,15 +307,15 @@ class _AnimationBuilder implements AnimationAstVisitor {
.toStmt()])]) .toStmt()])])
.toStmt()); .toStmt());
statements.push( statements.push(_ANIMATION_FACTORY_VIEW_VAR
_ANIMATION_FACTORY_VIEW_VAR .callMethod(
.callMethod( 'queueAnimation',
'queueAnimation', [
[ _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName),
_ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName), _ANIMATION_PLAYER_VAR, _ANIMATION_TIME_VAR,
_ANIMATION_PLAYER_VAR, _ANIMATION_CURRENT_STATE_VAR, _ANIMATION_NEXT_STATE_VAR _ANIMATION_CURRENT_STATE_VAR, _ANIMATION_NEXT_STATE_VAR
]) ])
.toStmt()); .toStmt());
return o.fn( return o.fn(
[ [
@ -348,6 +358,7 @@ class _AnimationBuilderContext {
stateMap = new _AnimationBuilderStateMap(); stateMap = new _AnimationBuilderStateMap();
endStateAnimateStep: AnimationStepAst = null; endStateAnimateStep: AnimationStepAst = null;
isExpectingFirstStyleStep = false; isExpectingFirstStyleStep = false;
totalTransitionTime = 0;
} }
class _AnimationBuilderStateMap { class _AnimationBuilderStateMap {

View File

@ -34,6 +34,7 @@ export {ExceptionHandler, WrappedException, BaseException} from './src/facade/ex
export * from './private_export'; export * from './private_export';
export * from './src/animation/metadata'; export * from './src/animation/metadata';
export {AnimationTransitionEvent} from './src/animation/animation_transition_event';
export {AnimationPlayer} from './src/animation/animation_player'; export {AnimationPlayer} from './src/animation/animation_player';
export {SanitizationService, SecurityContext} from './src/security'; export {SanitizationService, SecurityContext} from './src/security';

View File

@ -0,0 +1,51 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
*/
/**
* An instance of this class is returned as an event parameter when an animation
* callback is captured for an animation either during the start or done phase.
*
* ```typescript
* @Component({
* host: {
* '[@myAnimationTrigger]': 'someExpression',
* '(@myAnimationTrigger.start)': 'captureStartEvent($event)',
* '(@myAnimationTrigger.done)': 'captureDoneEvent($event)',
* },
* animations: [
* trigger("myAnimationTrigger", [
* // ...
* ])
* ]
* })
* class MyComponent {
* someExpression: any = false;
* captureStartEvent(event: AnimationTransitionEvent) {
* // the toState, fromState and totalTime data is accessible from the event variable
* }
*
* captureDoneEvent(event: AnimationTransitionEvent) {
* // the toState, fromState and totalTime data is accessible from the event variable
* }
* }
* ```
*
* @experimental Animation support is experimental.
*/
export class AnimationTransitionEvent {
public fromState: string;
public toState: string;
public totalTime: number;
constructor({fromState, toState,
totalTime}: {fromState: string, toState: string, totalTime: number}) {
this.fromState = fromState;
this.toState = toState;
this.totalTime = totalTime;
}
}

View File

@ -9,6 +9,7 @@
import {AnimationGroupPlayer} from '../animation/animation_group_player'; import {AnimationGroupPlayer} from '../animation/animation_group_player';
import {AnimationOutput} from '../animation/animation_output'; import {AnimationOutput} from '../animation/animation_output';
import {AnimationPlayer, NoOpAnimationPlayer} from '../animation/animation_player'; import {AnimationPlayer, NoOpAnimationPlayer} from '../animation/animation_player';
import {AnimationTransitionEvent} from '../animation/animation_transition_event';
import {ViewAnimationMap} from '../animation/view_animation_map'; import {ViewAnimationMap} from '../animation/view_animation_map';
import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection'; import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection';
import {Injector} from '../di/injector'; import {Injector} from '../di/injector';
@ -81,22 +82,17 @@ export abstract class AppView<T> {
} }
queueAnimation( queueAnimation(
element: any, animationName: string, player: AnimationPlayer, fromState: string, element: any, animationName: string, player: AnimationPlayer, totalTime: number,
toState: string): void { fromState: string, toState: string): void {
var actualAnimationDetected = !(player instanceof NoOpAnimationPlayer); var event = new AnimationTransitionEvent(
var animationData = { {'fromState': fromState, 'toState': toState, 'totalTime': totalTime});
'fromState': fromState,
'toState': toState,
'running': actualAnimationDetected
};
this.animationPlayers.set(element, animationName, player); this.animationPlayers.set(element, animationName, player);
player.onDone(() => { player.onDone(() => {
// TODO: make this into a datastructure for done|start // TODO: make this into a datastructure for done|start
this.triggerAnimationOutput(element, animationName, 'done', animationData); this.triggerAnimationOutput(element, animationName, 'done', event);
this.animationPlayers.remove(element, animationName); this.animationPlayers.remove(element, animationName);
}); });
player.onStart( player.onStart(() => { this.triggerAnimationOutput(element, animationName, 'start', event); });
() => { this.triggerAnimationOutput(element, animationName, 'start', animationData); });
} }
triggerQueuedAnimations() { triggerQueuedAnimations() {
@ -108,7 +104,7 @@ export abstract class AppView<T> {
} }
triggerAnimationOutput( triggerAnimationOutput(
element: any, animationName: string, phase: string, animationData: {[key: string]: any}) { element: any, animationName: string, phase: string, event: AnimationTransitionEvent) {
var listeners = this._animationListeners.get(element); var listeners = this._animationListeners.get(element);
if (isPresent(listeners) && listeners.length) { if (isPresent(listeners) && listeners.length) {
for (let i = 0; i < listeners.length; i++) { for (let i = 0; i < listeners.length; i++) {
@ -116,7 +112,7 @@ export abstract class AppView<T> {
// we check for both the name in addition to the phase in the event // we check for both the name in addition to the phase in the event
// that there may be more than one @trigger on the same element // that there may be more than one @trigger on the same element
if (listener.output.name == animationName && listener.output.phase == phase) { if (listener.output.name == animationName && listener.output.phase == phase) {
listener.handler(animationData); listener.handler(event);
break; break;
} }
} }

View File

@ -10,11 +10,13 @@ import {CommonModule} from '@angular/common';
import {AnimationDriver} from '@angular/platform-browser/src/dom/animation_driver'; import {AnimationDriver} from '@angular/platform-browser/src/dom/animation_driver';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {MockAnimationDriver} from '@angular/platform-browser/testing/mock_animation_driver'; import {MockAnimationDriver} from '@angular/platform-browser/testing/mock_animation_driver';
import {Component} from '../../index'; import {Component} from '../../index';
import {DEFAULT_STATE} from '../../src/animation/animation_constants'; import {DEFAULT_STATE} from '../../src/animation/animation_constants';
import {AnimationKeyframe} from '../../src/animation/animation_keyframe'; import {AnimationKeyframe} from '../../src/animation/animation_keyframe';
import {AnimationPlayer} from '../../src/animation/animation_player'; import {AnimationPlayer} from '../../src/animation/animation_player';
import {AnimationStyles} from '../../src/animation/animation_styles'; import {AnimationStyles} from '../../src/animation/animation_styles';
import {AnimationTransitionEvent} from '../../src/animation/animation_transition_event';
import {AUTO_STYLE, animate, group, keyframes, sequence, state, style, transition, trigger} from '../../src/animation/metadata'; import {AUTO_STYLE, animate, group, keyframes, sequence, state, style, transition, trigger} from '../../src/animation/metadata';
import {isPresent} from '../../src/facade/lang'; import {isPresent} from '../../src/facade/lang';
import {TestBed, fakeAsync, flushMicrotasks} from '../../testing'; import {TestBed, fakeAsync, flushMicrotasks} from '../../testing';
@ -980,8 +982,8 @@ function declareTests({useJit}: {useJit: boolean}) {
var isAnimationRunning = false; var isAnimationRunning = false;
var calls = 0; var calls = 0;
var cmp = fixture.debugElement.componentInstance; var cmp = fixture.debugElement.componentInstance;
cmp.callback = (e: any) => { cmp.callback = (e: AnimationTransitionEvent) => {
isAnimationRunning = e['running']; isAnimationRunning = e.totalTime > 0;
calls++; calls++;
}; };
@ -1016,8 +1018,8 @@ function declareTests({useJit}: {useJit: boolean}) {
var isAnimationRunning = false; var isAnimationRunning = false;
var calls = 0; var calls = 0;
var cmp = fixture.debugElement.componentInstance; var cmp = fixture.debugElement.componentInstance;
cmp.callback = (e: any) => { cmp.callback = (e: AnimationTransitionEvent) => {
isAnimationRunning = e['running']; isAnimationRunning = e.totalTime > 0;
calls++; calls++;
}; };
cmp.exp = 'one'; cmp.exp = 'one';
@ -1056,20 +1058,56 @@ function declareTests({useJit}: {useJit: boolean}) {
const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver; const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver;
let fixture = TestBed.createComponent(DummyIfCmp); let fixture = TestBed.createComponent(DummyIfCmp);
var eventData: any = {}; var eventData: AnimationTransitionEvent = null;
var cmp = fixture.debugElement.componentInstance; var cmp = fixture.debugElement.componentInstance;
cmp.callback = (e: any) => { eventData = e; }; cmp.callback = (e: AnimationTransitionEvent) => { eventData = e; };
cmp.exp = 'one'; cmp.exp = 'one';
fixture.detectChanges(); fixture.detectChanges();
flushMicrotasks(); flushMicrotasks();
expect(eventData['fromState']).toEqual('void'); expect(eventData.fromState).toEqual('void');
expect(eventData['toState']).toEqual('one'); expect(eventData.toState).toEqual('one');
cmp.exp = 'two'; cmp.exp = 'two';
fixture.detectChanges(); fixture.detectChanges();
flushMicrotasks(); flushMicrotasks();
expect(eventData['fromState']).toEqual('one'); expect(eventData.fromState).toEqual('one');
expect(eventData['toState']).toEqual('two'); expect(eventData.toState).toEqual('two');
}));
it('should emit the `totalTime` values for an animation callback', fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `
<div [@trigger]="exp" (@trigger.start)="callback1($event)"></div>
<div [@noTrigger]="exp2" (@noTrigger.start)="callback2($event)"></div>
`,
animations: [
trigger(
'trigger',
[transition(
'* => *',
[animate('1s 750ms', style({})), animate('2000ms 0ms', style({}))])]),
trigger('noTrigger', [])
]
}
});
const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver;
let fixture = TestBed.createComponent(DummyIfCmp);
var eventData1: AnimationTransitionEvent = null;
var eventData2: AnimationTransitionEvent = null;
var cmp = fixture.debugElement.componentInstance;
cmp.callback1 = (e: AnimationTransitionEvent) => { eventData1 = e; };
cmp.callback2 = (e: AnimationTransitionEvent) => { eventData2 = e; };
cmp.exp = 'one';
fixture.detectChanges();
flushMicrotasks();
expect(eventData1.totalTime).toEqual(3750);
cmp.exp2 = 'two';
fixture.detectChanges();
flushMicrotasks();
expect(eventData2.totalTime).toEqual(0);
})); }));
it('should throw an error if an animation output is referenced is not defined within the component', it('should throw an error if an animation output is referenced is not defined within the component',

View File

@ -113,6 +113,18 @@ export declare class AnimationStyleMetadata extends AnimationMetadata {
}>, offset?: number); }>, offset?: number);
} }
/** @experimental */
export declare class AnimationTransitionEvent {
fromState: string;
toState: string;
totalTime: number;
constructor({fromState, toState, totalTime}: {
fromState: string;
toState: string;
totalTime: number;
});
}
/** @experimental */ /** @experimental */
export declare abstract class AnimationWithStepsMetadata extends AnimationMetadata { export declare abstract class AnimationWithStepsMetadata extends AnimationMetadata {
steps: AnimationMetadata[]; steps: AnimationMetadata[];