Форк Rambox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

374 lines
14 KiB

/**
* @class Ext.fx.Manager
* Animation Manager which keeps track of all current animations and manages them on a frame by frame basis.
* @private
* @singleton
*/
Ext.define('Ext.fx.Manager', {
/* Begin Definitions */
singleton: true,
requires: [
'Ext.util.MixedCollection',
'Ext.util.TaskRunner',
'Ext.fx.target.Element',
'Ext.fx.target.ElementCSS',
'Ext.fx.target.CompositeElement',
'Ext.fx.target.CompositeElementCSS',
'Ext.fx.target.Sprite',
'Ext.fx.target.CompositeSprite',
'Ext.fx.target.Component'
],
mixins: {
queue: 'Ext.fx.Queue'
},
/* End Definitions */
/**
* @private
*/
constructor: function() {
var me = this;
me.items = new Ext.util.MixedCollection();
me.targetArr = {};
me.mixins.queue.constructor.call(me);
// Do not use fireIdleEvent: false. Each tick of the TaskRunner needs to fire the idleEvent
// in case an animation callback/listener adds a listener.
me.taskRunner = new Ext.util.TaskRunner();
},
/**
* @cfg {Number} interval Default interval in miliseconds to calculate each frame. Defaults to 16ms (~60fps)
*/
interval: 16,
/**
* @cfg {Boolean} forceJS Force the use of JavaScript-based animation instead of CSS3 animation, even when CSS3
* animation is supported by the browser. This defaults to true currently, as CSS3 animation support is still
* considered experimental at this time, and if used should be thouroughly tested across all targeted browsers.
* @protected
*/
forceJS: true,
// @private Target factory
createTarget: function(target) {
var me = this,
useCSS3 = !me.forceJS && Ext.supports.Transitions,
targetObj;
me.useCSS3 = useCSS3;
if (target) {
// dom element, string or fly
if (target.tagName || Ext.isString(target) || target.isFly) {
target = Ext.get(target);
targetObj = new Ext.fx.target['Element' + (useCSS3 ? 'CSS' : '')](target);
}
// Element
else if (target.dom) {
targetObj = new Ext.fx.target['Element' + (useCSS3 ? 'CSS' : '')](target);
}
// Element Composite
else if (target.isComposite) {
targetObj = new Ext.fx.target['CompositeElement' + (useCSS3 ? 'CSS' : '')](target);
}
// Draw Sprite
else if (target.isSprite) {
targetObj = new Ext.fx.target.Sprite(target);
}
// Draw Sprite Composite
else if (target.isCompositeSprite) {
targetObj = new Ext.fx.target.CompositeSprite(target);
}
// Component
else if (target.isComponent) {
targetObj = new Ext.fx.target.Component(target);
}
else if (target.isAnimTarget) {
return target;
}
else {
return null;
}
me.targets.add(targetObj);
return targetObj;
}
else {
return null;
}
},
/**
* Add an Anim to the manager. This is done automatically when an Anim instance is created.
* @param {Ext.fx.Anim} anim
*/
addAnim: function(anim) {
var me = this,
items = me.items,
task = me.task;
// Make sure we use the anim's id, not the anim target's id here. The anim id will be unique on
// each call to addAnim. `anim.target` is the DOM element being targeted, and since multiple animations
// can target a single DOM node concurrently, the target id cannot be assumned to be unique.
items.add(anim.id, anim);
//Ext.log('+ added anim ', anim.id, ', target: ', anim.target.getId(), ', duration: ', anim.duration);
// Start the timer if not already running
if (!task && items.length) {
task = me.task = {
run: me.runner,
interval: me.interval,
scope: me
};
//Ext.log('--->> Starting task');
me.taskRunner.start(task);
}
},
/**
* Remove an Anim from the manager. This is done automatically when an Anim ends.
* @param {Ext.fx.Anim} anim
*/
removeAnim: function(anim) {
var me = this,
items = me.items,
task = me.task;
items.removeAtKey(anim.id);
//Ext.log(' X removed anim ', anim.id, ', target: ', anim.target.getId(), ', frames: ', anim.frameCount, ', item count: ', items.length);
// Stop the timer if there are no more managed Anims
if (task && !items.length) {
//Ext.log('[]--- Stopping task');
me.taskRunner.stop(task);
delete me.task;
}
},
/**
* @private
* Runner function being called each frame
*/
runner: function() {
var me = this,
items = me.items.getRange(),
i = 0,
len = items.length,
anim;
//Ext.log(' executing anim runner task with ', len, ' items');
me.targetArr = {};
// Single timestamp for all animations this interval
me.timestamp = new Date();
// Loop to start any new animations first before looping to
// execute running animations (which will also include all animations
// started in this loop). This is a subtle difference from simply
// iterating in one loop and starting then running each animation,
// but separating the loops is necessary to ensure that all new animations
// actually kick off prior to existing ones regardless of array order.
// Otherwise in edge cases when there is excess latency in overall
// performance, allowing existing animations to run before new ones can
// lead to dropped frames and subtle race conditions when they are
// interdependent, which is often the case with certain Element fx.
for (; i < len; i++) {
anim = items[i];
if (anim.isReady()) {
//Ext.log(' starting anim ', anim.id, ', target: ', anim.target.id);
me.startAnim(anim);
}
}
for (i = 0; i < len; i++) {
anim = items[i];
if (anim.isRunning()) {
//Ext.log(' running anim ', anim.target.id);
me.runAnim(anim);
}
//<debug>
//else if (!me.useCSS3) {
// When using CSS3 transitions the animations get paused since they are not
// needed once the transition is handed over to the browser, so we can
// ignore this case. However if we are doing JS animations and something is
// paused here it's possibly unintentional.
//Ext.log(' (i) anim ', anim.id, ' is active but not running...');
//}
//</debug>
}
// Apply all the pending changes to their targets
me.applyPendingAttrs();
},
/**
* @private
* Start the individual animation (initialization)
*/
startAnim: function(anim) {
anim.start(this.timestamp);
},
/**
* @private
* Run the individual animation for this frame
*/
runAnim: function(anim, forceEnd) {
if (!anim) {
return;
}
var me = this,
useCSS3 = me.useCSS3 && anim.target.type === 'element',
elapsedTime = me.timestamp - anim.startTime,
lastFrame = (elapsedTime >= anim.duration),
target, o;
if (forceEnd) {
elapsedTime = anim.duration;
lastFrame = true;
}
target = this.collectTargetData(anim, elapsedTime, useCSS3, lastFrame);
// For CSS3 animation, we need to immediately set the first frame's attributes without any transition
// to get a good initial state, then add the transition properties and set the final attributes.
if (useCSS3) {
//Ext.log(' (i) using CSS3 transitions');
// Flush the collected attributes, without transition
anim.target.setAttr(target.anims[anim.id].attributes, true);
// Add the end frame data
me.collectTargetData(anim, anim.duration, useCSS3, lastFrame);
// Pause the animation so runAnim doesn't keep getting called
anim.paused = true;
target = anim.target.target;
// We only want to attach an event on the last element in a composite
if (anim.target.isComposite) {
target = anim.target.target.last();
}
// Listen for the transitionend event
o = {};
o[Ext.supports.CSS3TransitionEnd] = anim.lastFrame;
o.scope = anim;
o.single = true;
target.on(o);
}
return target;
},
jumpToEnd: function(anim) {
var target = this.runAnim(anim, true);
this.applyAnimAttrs(target, target.anims[anim.id]);
},
/**
* @private
* Collect target attributes for the given Anim object at the given timestamp
* @param {Ext.fx.Anim} anim The Anim instance
* @param {Number} timestamp Time after the anim's start time
* @param {Boolean} [useCSS3=false] True if using CSS3-based animation, else false
* @param {Boolean} [isLastFrame=false] True if this is the last frame of animation to be run, else false
* @return {Object} The animation target wrapper object containing the passed animation along with the
* new attributes to set on the target's element in the next animation frame.
*/
collectTargetData: function(anim, elapsedTime, useCSS3, isLastFrame) {
var targetId = anim.target.getId(),
target = this.targetArr[targetId];
if (!target) {
// Create a thin wrapper around the target so that we can create a link between the
// target element and its associated animations. This is important later when applying
// attributes to the target so that each animation can be independently run with its own
// duration and stopped at any point without affecting other animations for the same target.
target = this.targetArr[targetId] = {
id: targetId,
el: anim.target,
anims: {}
};
}
// This is a wrapper for the animation so that we can also save state along with it,
// including the current elapsed time and lastFrame status. Even though this method only
// adds a single anim object per call, each target element could have multiple animations
// associated with it, which is why the anim is added to the target's `anims` hash by id.
target.anims[anim.id] = {
id: anim.id,
anim: anim,
elapsed: elapsedTime,
isLastFrame: isLastFrame,
// This is the object that gets applied to the target element below in applyPendingAttrs():
attributes: [{
duration: anim.duration,
easing: (useCSS3 && anim.reverse) ? anim.easingFn.reverse().toCSS3() : anim.easing,
// This is where the magic happens. The anim calculates what its new attributes should
// be based on the current frame and returns those as a hash of values.
attrs: anim.runAnim(elapsedTime)
}]
};
return target;
},
// Duplicating this code for performance reasons. We only want to apply the anims
// to a single animation because we're hitting the end. It may be out of sequence from
// the runner timer.
applyAnimAttrs: function(target, animWrap) {
var anim = animWrap.anim;
if (animWrap.attributes && anim.isRunning()) {
target.el.setAttr(animWrap.attributes, false, animWrap.isLastFrame);
// If this particular anim is at the last frame end it
if (animWrap.isLastFrame) {
anim.lastFrame();
}
}
},
/**
* @private
* Apply all pending attribute changes to their targets
*/
applyPendingAttrs: function() {
var targetArr = this.targetArr,
target, targetId, animWrap, anim, animId;
// Loop through each target
for (targetId in targetArr) {
if (targetArr.hasOwnProperty(targetId)) {
target = targetArr[targetId];
// Each target could have multiple associated animations, so iterate those
for (animId in target.anims) {
if (target.anims.hasOwnProperty(animId)) {
animWrap = target.anims[animId];
anim = animWrap.anim;
// If the animation has valid attributes, set them on the target
if (animWrap.attributes && anim.isRunning()) {
//Ext.log(' > applying attributes for anim ', animWrap.id, ', target: ', target.id, ', elapsed: ', animWrap.elapsed);
target.el.setAttr(animWrap.attributes, false, animWrap.isLastFrame);
// If this particular anim is at the last frame end it
if (animWrap.isLastFrame) {
//Ext.log(' running last frame for ', animWrap.id, ', target: ', targetId);
anim.lastFrame();
}
}
}
}
}
}
}
});