407 lines
11 KiB
JavaScript
407 lines
11 KiB
JavaScript
define("dojox/css3/transition", ["dojo/_base/lang",
|
|
"dojo/_base/array",
|
|
"dojo/_base/Deferred",
|
|
"dojo/DeferredList",
|
|
"dojo/on",
|
|
"dojo/_base/sniff"],
|
|
function(lang, array, Deferred, DeferredList, on, has){
|
|
// module:
|
|
// dojox/css3/transition
|
|
|
|
//create cross platform animation/transition effects
|
|
//TODO enable opera mobile when it is hardware accelerated
|
|
//TODO enable IE when CSS3 transition is supported in IE 10
|
|
var transitionEndEventName = "transitionend";
|
|
var transitionPrefix = "t"; //by default use "t" prefix and "ransition" to make word "transition"
|
|
var translateMethodStart = "translate3d(";//Android 2.x does not support translateX in CSS Transition, we need to use translate3d in webkit browsers
|
|
var translateMethodEnd = ",0,0)";
|
|
if(has("webkit")){
|
|
transitionPrefix = "WebkitT";
|
|
transitionEndEventName = "webkitTransitionEnd";
|
|
}else if(has("mozilla")){
|
|
transitionPrefix = "MozT";
|
|
translateMethodStart = "translateX(";
|
|
translateMethodEnd = ")";
|
|
}
|
|
|
|
//TODO find a way to lock the animation and prevent animation conflict
|
|
//Use the simple object inheritance
|
|
var transition = function(/*Object?*/args){
|
|
// summary:
|
|
// This module defines the transition utilities which can be used
|
|
// to perform transition effects based on the CSS Transition standard.
|
|
// args:
|
|
// The arguments which will be mixed into this transition object.
|
|
|
|
//default config should be in animation object itself instead of its prototype
|
|
//otherwise, it might be easy for making mistake of modifying prototype
|
|
var defaultConfig = {
|
|
startState: {},
|
|
endState: {},
|
|
node: null,
|
|
duration: 250,
|
|
"in": true,
|
|
direction: 1,
|
|
autoClear: true
|
|
};
|
|
|
|
lang.mixin(this, defaultConfig);
|
|
lang.mixin(this, args);
|
|
|
|
//create the deferred object which will resolve after the animation is finished.
|
|
//We can rely on "onAfterEnd" function to notify the end of a single animation,
|
|
//but using a deferred object is easier to wait for multiple animations end.
|
|
if(!this.deferred){
|
|
this.deferred = new Deferred();
|
|
}
|
|
};
|
|
|
|
lang.extend(transition, {
|
|
|
|
play: function(){
|
|
// summary:
|
|
// Plays the transition effect defined by this transition object.
|
|
transition.groupedPlay([this]);
|
|
},
|
|
|
|
//method to apply the state of the transition
|
|
_applyState: function(state){
|
|
var style = this.node.style;
|
|
for(var property in state){
|
|
if(state.hasOwnProperty(property)){
|
|
style[property] = state[property];
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
initState: function(){
|
|
// summary:
|
|
// Method to initialize the state for a transition.
|
|
|
|
//apply the immediate style change for initial state.
|
|
this.node.style[transitionPrefix + "ransitionProperty"] = "none";
|
|
this.node.style[transitionPrefix + "ransitionDuration"] = "0ms";
|
|
this._applyState(this.startState);
|
|
|
|
},
|
|
|
|
_beforeStart: function(){
|
|
if (this.node.style.display === "none"){
|
|
this.node.style.display = "";
|
|
}
|
|
this.beforeStart();
|
|
},
|
|
|
|
_beforeClear: function(){
|
|
this.node.style[transitionPrefix + "ransitionProperty"] = null;
|
|
this.node.style[transitionPrefix + "ransitionDuration"] = null;
|
|
if(this["in"] !== true){
|
|
this.node.style.display = "none";
|
|
}
|
|
this.beforeClear();
|
|
},
|
|
|
|
_onAfterEnd: function(){
|
|
this.deferred.resolve(this.node);
|
|
if(this.node.id && transition.playing[this.node.id]===this.deferred){
|
|
delete transition.playing[this.node.id];
|
|
}
|
|
this.onAfterEnd();
|
|
},
|
|
|
|
beforeStart: function(){
|
|
// summary:
|
|
// The callback which will be called right before the start
|
|
// of the transition effect.
|
|
},
|
|
|
|
beforeClear: function(){
|
|
// summary:
|
|
// The callback which will be called right after the end
|
|
// of the transition effect and before the final state is
|
|
// cleared.
|
|
},
|
|
|
|
onAfterEnd: function(){
|
|
// summary:
|
|
// The callback which will be called right after the end
|
|
// of the transition effect and after the final state is
|
|
// cleared.
|
|
},
|
|
|
|
start: function(){
|
|
// summary:
|
|
// Method to start the transition.
|
|
this._beforeStart();
|
|
this._startTime = new Date().getTime(); // set transition start timestamp
|
|
this._cleared = false; // set clear flag to false
|
|
|
|
var self = this;
|
|
//change the transition duration
|
|
self.node.style[transitionPrefix + "ransitionProperty"] = "all";
|
|
self.node.style[transitionPrefix + "ransitionDuration"] = self.duration + "ms";
|
|
|
|
//connect to clear the transition state after the transition end.
|
|
//Since the transition is conducted asynchronously, we need to
|
|
//connect to transition end event to clear the state
|
|
on.once(self.node, transitionEndEventName, function(){
|
|
self.clear();
|
|
});
|
|
|
|
this._applyState(this.endState);
|
|
},
|
|
|
|
clear: function(){
|
|
// summary:
|
|
// Method to clear the state after a transition.
|
|
if(this._cleared) {
|
|
return;
|
|
}
|
|
this._cleared = true; // set clear flag to true
|
|
|
|
this._beforeClear();
|
|
this._removeState(this.endState);
|
|
// console.log(this.node.id + " clear.");
|
|
this._onAfterEnd();
|
|
},
|
|
|
|
//create removeState method
|
|
_removeState: function(state){
|
|
var style = this.node.style;
|
|
for(var property in state){
|
|
if(state.hasOwnProperty(property)){
|
|
style[property] = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
//TODO add the lock mechanism for all of the transition effects
|
|
// consider using only one object for one type of transition.
|
|
|
|
transition.slide = function(node, config){
|
|
// summary:
|
|
// Method which is used to create the transition object of a slide effect.
|
|
// node:
|
|
// The node that the slide transition effect will be applied on.
|
|
// config:
|
|
// The cofig arguments which will be mixed into this transition object.
|
|
|
|
//create the return object and set the startState, endState of the return
|
|
var ret = new transition(config);
|
|
ret.node = node;
|
|
|
|
var startX = "0";
|
|
var endX = "0";
|
|
|
|
if(ret["in"]){
|
|
if(ret.direction === 1){
|
|
startX = "100%";
|
|
}else{
|
|
startX = "-100%";
|
|
}
|
|
}else{
|
|
if(ret.direction === 1){
|
|
endX = "-100%";
|
|
}else{
|
|
endX = "100%";
|
|
}
|
|
}
|
|
|
|
ret.startState[transitionPrefix + "ransform"]=translateMethodStart+startX+translateMethodEnd;
|
|
|
|
ret.endState[transitionPrefix + "ransform"]=translateMethodStart+endX+translateMethodEnd;
|
|
|
|
return ret;
|
|
};
|
|
|
|
transition.fade = function(node, config){
|
|
// summary:
|
|
// Method which is used to create the transition object of fade effect.
|
|
// node:
|
|
// The node that the fade transition effect will be applied on.
|
|
// config:
|
|
// The cofig arguments which will be mixed into this transition object.
|
|
var ret = new transition(config);
|
|
ret.node = node;
|
|
|
|
var startOpacity = "0";
|
|
var endOpacity = "0";
|
|
|
|
if(ret["in"]){
|
|
endOpacity = "1";
|
|
}else{
|
|
startOpacity = "1";
|
|
}
|
|
|
|
lang.mixin(ret, {
|
|
startState:{
|
|
"opacity": startOpacity
|
|
},
|
|
endState:{
|
|
"opacity": endOpacity
|
|
}
|
|
});
|
|
|
|
return ret;
|
|
};
|
|
|
|
transition.flip = function(node, config){
|
|
// summary:
|
|
// Method which is used to create the transition object of flip effect.
|
|
// node:
|
|
// The node that the flip transition effect will be applied on.
|
|
// config:
|
|
// The cofig arguments which will be mixed into this transition object.
|
|
|
|
var ret = new transition(config);
|
|
ret.node = node;
|
|
|
|
if(ret["in"]){
|
|
//Need to set opacity here because Android 2.2 has bug that
|
|
//scale(...) in transform does not persist status
|
|
lang.mixin(ret,{
|
|
startState:{
|
|
"opacity": "0"
|
|
},
|
|
endState:{
|
|
"opacity": "1"
|
|
}
|
|
});
|
|
ret.startState[transitionPrefix + "ransform"]="scale(0,0.8) skew(0,-30deg)";
|
|
ret.endState[transitionPrefix + "ransform"]="scale(1,1) skew(0,0)";
|
|
}else{
|
|
lang.mixin(ret,{
|
|
startState:{
|
|
"opacity": "1"
|
|
},
|
|
endState:{
|
|
"opacity": "0"
|
|
}
|
|
});
|
|
ret.startState[transitionPrefix + "ransform"]="scale(1,1) skew(0,0)";
|
|
ret.endState[transitionPrefix + "ransform"]="scale(0,0.8) skew(0,30deg)";
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
var getWaitingList = function(/*Array*/ nodes){
|
|
var defs = [];
|
|
array.forEach(nodes, function(node){
|
|
//check whether the node is under other animation
|
|
if(node.id && transition.playing[node.id]){
|
|
//hook on deferred object in transition.playing
|
|
defs.push(transition.playing[node.id]);
|
|
}
|
|
|
|
});
|
|
return new DeferredList(defs);
|
|
};
|
|
|
|
transition.getWaitingList = getWaitingList;
|
|
|
|
transition.groupedPlay = function(/*Array*/args){
|
|
// summary:
|
|
// The method which groups multiple transitions and plays
|
|
// them together.
|
|
// args:
|
|
// The array of transition objects which will be played together.
|
|
|
|
var animNodes = array.filter(args, function(item){
|
|
return item.node;
|
|
});
|
|
|
|
var waitingList = getWaitingList(animNodes);
|
|
|
|
//update registry with deferred objects in animations of args.
|
|
array.forEach(args, function(item){
|
|
if(item.node.id){
|
|
transition.playing[item.node.id] = item.deferred;
|
|
}
|
|
});
|
|
|
|
//wait for all deferred object in deferred list to resolve
|
|
Deferred.when(waitingList, function(){
|
|
array.forEach(args, function(item){
|
|
//set the start state
|
|
item.initState();
|
|
});
|
|
|
|
//Assume the fps of the animation should be higher than 30 fps and
|
|
//allow the browser to use one frame's time to redraw so that
|
|
//the transition can be started
|
|
setTimeout(function(){
|
|
array.forEach(args, function(item){
|
|
item.start();
|
|
});
|
|
|
|
// check and clear node if the node not cleared.
|
|
// 1. on Android2.2/2.3, the "fade out" transitionEnd event will be lost if the soft keyboard popup, so we need to check nodes' clear status.
|
|
// 2. The "fade in" transitionEnd event will before or after "fade out" transitionEnd event and it always occurs.
|
|
// We can check fade out node status in the last "fade in" node transitionEnd event callback, if node transition timeout, we clear it.
|
|
// NOTE: the last "fade in" transitionEnd event will always fired, so we bind on this event and check other nodes.
|
|
on.once(args[args.length-1].node, transitionEndEventName, function(){
|
|
var timeout;
|
|
for(var i=0; i<args.length-1; i++){
|
|
if(args[i].deferred.fired !== 0){
|
|
timeout = new Date().getTime() - args[i]._startTime;
|
|
if(timeout >= args[i].duration){
|
|
args[i].clear();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}, 33);
|
|
});
|
|
};
|
|
|
|
transition.chainedPlay = function(/*Array*/args){
|
|
// summary:
|
|
// The method which plays multiple transitions one by one.
|
|
// args:
|
|
// The array of transition objects which will be played in a chain.
|
|
|
|
var animNodes = array.filter(args, function(item){
|
|
return item.node;
|
|
});
|
|
|
|
var waitingList = getWaitingList(animNodes);
|
|
|
|
//update registry with deferred objects in animations of args.
|
|
array.forEach(args, function(item){
|
|
if(item.node.id){
|
|
transition.playing[item.node.id] = item.deferred;
|
|
}
|
|
});
|
|
|
|
Deferred.when(waitingList, function(){
|
|
array.forEach(args, function(item){
|
|
//set the start state
|
|
item.initState();
|
|
});
|
|
|
|
//chain animations together
|
|
for (var i=1, len=args.length; i < len; i++){
|
|
args[i-1].deferred.then(lang.hitch(args[i], function(){
|
|
this.start();
|
|
}));
|
|
}
|
|
|
|
//Assume the fps of the animation should be higher than 30 fps and
|
|
//allow the browser to use one frame's time to redraw so that
|
|
//the transition can be started
|
|
setTimeout(function(){
|
|
args[0].start();
|
|
}, 33);
|
|
});
|
|
};
|
|
|
|
//TODO complete the registry mechanism for animation handling and prevent animation conflicts
|
|
transition.playing = {};
|
|
|
|
return transition;
|
|
});
|