feat(animations): make sure animation callback reports the totalTime (#11022)
Closes #11022
This commit is contained in:
parent
8b782818f5
commit
4f8f8cfc66
|
@ -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 {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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[];
|
||||||
|
|
Loading…
Reference in New Issue