feat(core): introduce support for animations

Closes #8734
This commit is contained in:
Matias Niemelä
2016-05-25 12:46:22 -07:00
parent 6c6b316bd9
commit 5e0f8cf3f0
83 changed files with 5294 additions and 756 deletions

View File

@ -147,6 +147,7 @@ export abstract class DomAdapter {
abstract setGlobalVar(name: string, value: any);
abstract requestAnimationFrame(callback): number;
abstract cancelAnimationFrame(id);
abstract supportsWebAnimation(): boolean;
abstract performanceNow(): number;
abstract getAnimationPrefix(): string;
abstract getTransitionEnd(): string;

View File

@ -0,0 +1,9 @@
export interface DomAnimatePlayer {
cancel(): void;
play(): void;
pause(): void;
finish(): void;
onfinish: Function;
position: number;
currentTime: number;
}

View File

@ -8,7 +8,6 @@ import {
ViewEncapsulation
} from '@angular/core';
import {RenderDebugInfo} from '../../core_private';
import {AnimationBuilder} from '../animate/animation_builder';
import {
isPresent,
isBlank,
@ -24,6 +23,14 @@ import {StringMapWrapper} from '../../src/facade/collection';
import {BaseException} from '../../src/facade/exceptions';
import {DomSharedStylesHost} from './shared_styles_host';
import {
AnimationKeyframe,
AnimationStyles,
AnimationPlayer,
AnimationDriver
} from '../../core_private';
import {EventManager} from './events/event_manager';
import {DOCUMENT} from './dom_tokens';
import {getDOM} from './dom_adapter';
@ -38,12 +45,13 @@ export abstract class DomRootRenderer implements RootRenderer {
protected registeredComponents: Map<string, DomRenderer> = new Map<string, DomRenderer>();
constructor(public document: any, public eventManager: EventManager,
public sharedStylesHost: DomSharedStylesHost, public animate: AnimationBuilder) {}
public sharedStylesHost: DomSharedStylesHost,
public animationDriver: AnimationDriver) {}
renderComponent(componentProto: RenderComponentType): Renderer {
var renderer = this.registeredComponents.get(componentProto.id);
if (isBlank(renderer)) {
renderer = new DomRenderer(this, componentProto);
renderer = new DomRenderer(this, componentProto, this.animationDriver);
this.registeredComponents.set(componentProto.id, renderer);
}
return renderer;
@ -53,8 +61,9 @@ export abstract class DomRootRenderer implements RootRenderer {
@Injectable()
export class DomRootRenderer_ extends DomRootRenderer {
constructor(@Inject(DOCUMENT) _document: any, _eventManager: EventManager,
sharedStylesHost: DomSharedStylesHost, animate: AnimationBuilder) {
super(_document, _eventManager, sharedStylesHost, animate);
sharedStylesHost: DomSharedStylesHost,
animationDriver: AnimationDriver) {
super(_document, _eventManager, sharedStylesHost, animationDriver);
}
}
@ -63,7 +72,8 @@ export class DomRenderer implements Renderer {
private _hostAttr: string;
private _styles: string[];
constructor(private _rootRenderer: DomRootRenderer, private componentProto: RenderComponentType) {
constructor(private _rootRenderer: DomRootRenderer, private componentProto: RenderComponentType,
private _animationDriver: AnimationDriver) {
this._styles = _flattenStyles(componentProto.id, componentProto.styles, []);
if (componentProto.encapsulation !== ViewEncapsulation.Native) {
this._rootRenderer.sharedStylesHost.addStyles(this._styles);
@ -145,14 +155,11 @@ export class DomRenderer implements Renderer {
attachViewAfter(node: any, viewRootNodes: any[]) {
moveNodesAfterSibling(node, viewRootNodes);
for (let i = 0; i < viewRootNodes.length; i++) this.animateNodeEnter(viewRootNodes[i]);
}
detachView(viewRootNodes: any[]) {
for (var i = 0; i < viewRootNodes.length; i++) {
var node = viewRootNodes[i];
getDOM().remove(node);
this.animateNodeLeave(node);
getDOM().remove(viewRootNodes[i]);
}
}
@ -240,39 +247,11 @@ export class DomRenderer implements Renderer {
setText(renderNode: any, text: string): void { getDOM().setText(renderNode, text); }
/**
* Performs animations if necessary
* @param node
*/
animateNodeEnter(node: Node) {
if (getDOM().isElementNode(node) && getDOM().hasClass(node, 'ng-animate')) {
getDOM().addClass(node, 'ng-enter');
this._rootRenderer.animate.css()
.addAnimationClass('ng-enter-active')
.start(<HTMLElement>node)
.onComplete(() => { getDOM().removeClass(node, 'ng-enter'); });
}
}
/**
* If animations are necessary, performs animations then removes the element; otherwise, it just
* removes the element.
* @param node
*/
animateNodeLeave(node: Node) {
if (getDOM().isElementNode(node) && getDOM().hasClass(node, 'ng-animate')) {
getDOM().addClass(node, 'ng-leave');
this._rootRenderer.animate.css()
.addAnimationClass('ng-leave-active')
.start(<HTMLElement>node)
.onComplete(() => {
getDOM().removeClass(node, 'ng-leave');
getDOM().remove(node);
});
} else {
getDOM().remove(node);
}
animate(element: any,
startingStyles: AnimationStyles,
keyframes: AnimationKeyframe[], duration: number, delay: number,
easing: string): AnimationPlayer {
return this._animationDriver.animate(element, startingStyles, keyframes, duration, delay, easing);
}
}

View File

@ -0,0 +1,136 @@
import {StringMapWrapper} from '../facade/collection';
import {isPresent, isNumber, StringWrapper} from '../facade/lang';
import {BaseException, AUTO_STYLE} from '@angular/core';
import {
AnimationDriver,
AnimationPlayer,
NoOpAnimationPlayer,
AnimationKeyframe,
AnimationStyles
} from '../../core_private';
import {WebAnimationsPlayer} from './web_animations_player';
import {getDOM} from './dom_adapter';
export class WebAnimationsDriver implements AnimationDriver {
animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number,
easing: string): AnimationPlayer {
var anyElm = <any>element;
var formattedSteps = [];
var startingStyleLookup: {[key: string]: string|number}= {};
if (isPresent(startingStyles) && startingStyles.styles.length > 0) {
startingStyleLookup = _populateStyles(anyElm, startingStyles, {});
startingStyleLookup['offset'] = 0;
formattedSteps.push(startingStyleLookup);
}
keyframes.forEach((keyframe: AnimationKeyframe) => {
let data = _populateStyles(anyElm, keyframe.styles, startingStyleLookup);
data['offset'] = keyframe.offset;
formattedSteps.push(data);
});
// this is a special case when only styles are applied as an
// animation. When this occurs we want to animate from start to
// end with the same values. Removing the offset and having only
// start/end values is suitable enough for the web-animations API
if (formattedSteps.length == 1) {
var start = formattedSteps[0];
start.offset = null;
formattedSteps = [start, start];
}
var player = anyElm.animate(
formattedSteps,
{'duration': duration, 'delay': delay, 'easing': easing, 'fill': 'forwards'});
return new WebAnimationsPlayer(player, duration);
}
}
function _populateStyles(element: any, styles: AnimationStyles, defaultStyles: {[key: string]: string|number}) {
var data = {};
styles.styles.forEach((entry) => {
StringMapWrapper.forEach(entry, (val, prop) => {
data[prop] = val == AUTO_STYLE
? _computeStyle(element, prop)
: val.toString() + _resolveStyleUnit(val, prop);
});
});
StringMapWrapper.forEach(defaultStyles, (value, prop) => {
if (!isPresent(data[prop])) {
data[prop] = value;
}
});
return data;
}
function _resolveStyleUnit(val: string | number, prop: string): string {
var unit = '';
if (_isPixelDimensionStyle(prop) && val != 0 && val != '0') {
if (isNumber(val)) {
unit = 'px';
} else if (_findDimensionalSuffix(val.toString()).length == 0) {
throw new BaseException('Please provide a CSS unit value for ' + prop + ':' + val);
}
}
return unit;
}
const _$0 = 48;
const _$9 = 57;
const _$PERIOD = 46;
function _findDimensionalSuffix(value: string): string {
for (var i = 0; i < value.length; i++) {
var c = StringWrapper.charCodeAt(value, i);
if ((c >= _$0 && c <= _$9) || c == _$PERIOD) continue;
return value.substring(i, value.length);
}
return '';
}
function _isPixelDimensionStyle(prop: string): boolean {
switch (prop) {
case 'width':
case 'height':
case 'min-width':
case 'min-height':
case 'max-width':
case 'max-height':
case 'left':
case 'top':
case 'bottom':
case 'right':
case 'font-size':
case 'outline-width':
case 'outline-offset':
case 'padding-top':
case 'padding-left':
case 'padding-bottom':
case 'padding-right':
case 'margin-top':
case 'margin-left':
case 'margin-bottom':
case 'margin-right':
case 'border-radius':
case 'border-width':
case 'border-top-width':
case 'border-left-width':
case 'border-right-width':
case 'border-bottom-width':
case 'text-indent':
return true;
default:
return false;
}
}
function _computeStyle(element: any, prop: string): string {
return getDOM().getComputedStyle(element)[prop];
}

View File

@ -0,0 +1,57 @@
import {isPresent} from '../facade/lang';
import {AnimationPlayer} from '../../core_private';
import {DomAnimatePlayer} from './dom_animate_player';
export class WebAnimationsPlayer implements AnimationPlayer {
private _subscriptions: Function[] = [];
private _finished = false;
public parentPlayer: AnimationPlayer = null;
constructor(private _player: DomAnimatePlayer, public totalTime: number) {
// this is required to make the player startable at a later time
this.reset();
this._player.onfinish = () => this._onFinish();
}
private _onFinish() {
if (!this._finished) {
this._finished = true;
if (!isPresent(this.parentPlayer)) {
this.destroy();
}
this._subscriptions.forEach(fn => fn());
this._subscriptions = [];
}
}
onDone(fn: Function): void { this._subscriptions.push(fn); }
play(): void { this._player.play(); }
pause(): void { this._player.pause(); }
finish(): void {
this._onFinish();
this._player.finish();
}
reset(): void { this._player.cancel(); }
restart(): void {
this.reset();
this.play();
}
destroy(): void {
this.reset();
this._onFinish();
}
setPosition(p): void {
this._player.currentTime = p * this.totalTime;
}
getPosition(): number {
return this._player.currentTime / this.totalTime;
}
}