/**
* @private
*
* The abstract class. Sub-classes are expected, at the very least, to implement translation logics
* inside the 'translate' method
*/
Ext.define('Ext.util.translatable.Abstract', {
extend: 'Ext.Evented',
mixins: [
'Ext.mixin.Factoryable'
],
factoryConfig: {
type: 'translatable',
defaultType: 'csstransform'
},
requires: ['Ext.fx.easing.Linear'],
config: {
easing: null,
easingX: {
duration: 300
},
easingY: {
duration: 300
},
offsetX: 0,
offsetY: 0
},
/**
* @event animationstart
* Fires whenever the animation is started
* @param {Ext.util.translatable.Abstract} this
* @param {Number} x The current translation on the x axis
* @param {Number} y The current translation on the y axis
*/
/**
* @event animationframe
* Fires for each animation frame
* @param {Ext.util.translatable.Abstract} this
* @param {Number} x The new translation on the x axis
* @param {Number} y The new translation on the y axis
*/
/**
* @event animationend
* Fires whenever the animation is ended
* @param {Ext.util.translatable.Abstract} this
* @param {Number} x The current translation on the x axis
* @param {Number} y The current translation on the y axis
*/
/**
* @property {Number} x
* @private
* The last translated x value
*/
x: 0,
/**
* @property {Number} y
* @private
* The last translated y value
*/
y: 0,
activeEasingX: null,
activeEasingY: null,
isAnimating: false,
isTranslatable: true,
constructor: function(config) {
this.callParent([config]);
// this.position is simply an internal reusable object for GC purposes and should
// not be accessed directly as it's values are not kept in sync. always use
// getPosition() to get the position
this.position = { x: 0, y: 0 };
},
factoryEasing: function(easing) {
return Ext.factory(easing, Ext.fx.easing.Linear, null, 'easing');
},
applyEasing: function(easing) {
if (!this.getEasingX()) {
this.setEasingX(this.factoryEasing(easing));
}
if (!this.getEasingY()) {
this.setEasingY(this.factoryEasing(easing));
}
},
applyEasingX: function(easing) {
return this.factoryEasing(easing);
},
applyEasingY: function(easing) {
return this.factoryEasing(easing);
},
updateOffsetX: function() {
var me = this;
if (!me.isConfiguring && !me.isAnimating) {
me.translateXY(me.x, me.y);
}
},
updateOffsetY: function() {
var me = this;
if (!me.isConfiguring && !me.isAnimating) {
me.translateXY(me.x, me.y);
}
},
translate: function(x, y, animation) {
var me = this;
if (animation) {
return me.translateAnimated(x, y, animation);
}
if (me.isAnimating) {
me.stopAnimation();
}
if (!isNaN(x) && typeof x === 'number') {
me.x = x;
}
else {
x = me.x;
}
if (!isNaN(y) && typeof y === 'number') {
me.y = y;
}
else {
y = me.y;
}
me.translateXY(x, y);
},
translateXY: function(x, y) {
var me = this;
if (!me.destroyed) {
me.doTranslate(x + me.getOffsetX(), y + me.getOffsetY());
if (me.hasListeners.translate) {
me.fireEvent('translate', me, x, y);
}
}
},
translateAxis: function(axis, value, animation) {
var x, y;
if (axis === 'x') {
x = value;
}
else {
y = value;
}
return this.translate(x, y, animation);
},
/**
* Returns the translatable object's current position.
* @return {Object} position An object with x and y properties
*/
getPosition: function() {
var me = this,
position = me.position;
position.x = -me.x;
position.y = -me.y;
return position;
},
animate: function(easingX, easingY) {
var me = this;
me.activeEasingX = easingX;
me.activeEasingY = easingY;
me.isAnimating = true;
if (me.ownerCmp) {
me.ownerCmp.isTranslating = true;
}
me.lastX = null;
me.lastY = null;
Ext.AnimationQueue.start(me.doAnimationFrame, me);
me.fireEvent('animationstart', me, me.x, me.y);
return me;
},
translateAnimated: function(x, y, animation) {
var me = this,
now, easing, easingX, easingY;
if (!Ext.isObject(animation)) {
animation = {};
}
if (me.isAnimating) {
me.stopAnimation();
}
// Callback must be called in stopAnimation
me.callback = animation.callback;
me.callbackScope = animation.scope;
now = Ext.Date.now();
easing = animation.easing;
easingX = (typeof x === 'number')
? (animation.easingX || easing || me.getEasingX() || true)
: null;
easingY = (typeof y === 'number')
? (animation.easingY || easing || me.getEasingY() || true)
: null;
if (easingX) {
easingX = me.factoryEasing(easingX);
easingX.setStartTime(now);
easingX.setStartValue(me.x);
easingX.setEndValue(x);
if ('duration' in animation) {
easingX.setDuration(animation.duration);
}
}
if (easingY) {
easingY = me.factoryEasing(easingY);
easingY.setStartTime(now);
easingY.setStartValue(me.y);
easingY.setEndValue(y);
if ('duration' in animation) {
easingY.setDuration(animation.duration);
}
}
return me.animate(easingX, easingY);
},
doAnimationFrame: function() {
var me = this,
easingX = me.activeEasingX,
easingY = me.activeEasingY,
now = Date.now(),
x, y;
if (!me.isAnimating) {
return;
}
me.lastRun = now;
if (easingX === null && easingY === null) {
me.stopAnimation();
return;
}
if (easingX !== null) {
me.x = x = Math.round(easingX.getValue());
if (easingX.isEnded) {
me.activeEasingX = null;
me.fireEvent('axisanimationend', me, 'x', x);
}
}
else {
x = me.x;
}
if (easingY !== null) {
me.y = y = Math.round(easingY.getValue());
if (easingY.isEnded) {
me.activeEasingY = null;
me.fireEvent('axisanimationend', me, 'y', y);
}
}
else {
y = me.y;
}
if (me.lastX !== x || me.lastY !== y) {
me.translateXY(x, y);
me.lastX = x;
me.lastY = y;
}
me.fireEvent('animationframe', me, x, y);
},
stopAnimation: function() {
var me = this;
if (!me.isAnimating) {
return;
}
me.activeEasingX = null;
me.activeEasingY = null;
me.isAnimating = false;
if (me.ownerCmp) {
me.ownerCmp.isTranslating = false;
}
Ext.AnimationQueue.stop(me.doAnimationFrame, me);
me.fireEvent('animationend', me, me.x, me.y);
if (me.callback) {
me.callback.call(me.callbackScope);
me.callback = null;
}
},
refresh: function() {
this.translate(this.x, this.y);
},
resolveListenerScope: function() {
var ownerCmp = this.ownerCmp,
a = arguments;
if (ownerCmp) {
return ownerCmp.resolveListenerScope.apply(ownerCmp, a);
}
return this.callParent(a);
},
destroy: function() {
var me = this;
me.destroying = true;
if (me.isAnimating) {
me.stopAnimation();
}
me.callParent();
// This just makes it hard to ask "was destroy() called?":
// me.destroying = false; // removed in 7.0
me.destroyed = true;
}
});