/** * Provide a simple Flick plugin, which can be used along with the "flick" gesture event, to * animate the motion of the host node in response to a (mouse or touch) flick gesture. * * <p>The current implementation is designed to move the node, relative to the bounds of a parent node and is suitable * for scroll/carousel type implementations. Future versions will remove that constraint, to allow open ended movement within * the document.</p> * * @module node-flick */ var HOST = "host", PARENT_NODE = "parentNode", BOUNDING_BOX = "boundingBox", OFFSET_HEIGHT = "offsetHeight", OFFSET_WIDTH = "offsetWidth", SCROLL_HEIGHT = "scrollHeight", SCROLL_WIDTH = "scrollWidth", BOUNCE = "bounce", MIN_DISTANCE = "minDistance", MIN_VELOCITY = "minVelocity", BOUNCE_DISTANCE = "bounceDistance", DECELERATION = "deceleration", STEP = "step", DURATION = "duration", EASING = "easing", FLICK = "flick", getClassName = Y.ClassNameManager.getClassName; /** * A plugin class which can be used to animate the motion of a node, in response to a flick gesture. * * @class Flick * @namespace Plugin * @param {Object} config The initial attribute values for the plugin */ function Flick(config) { Flick.superclass.constructor.apply(this, arguments); } Flick.ATTRS = { /** * Drag coefficent for inertial scrolling. The closer to 1 this * value is, the less friction during scrolling. * * @attribute deceleration * @default 0.98 */ deceleration : { value: 0.98 }, /** * Drag coefficient for intertial scrolling at the upper * and lower boundaries of the scrollview. Set to 0 to * disable "rubber-banding". * * @attribute bounce * @type Number * @default 0.7 */ bounce : { value: 0.7 }, /** * The bounce distance in pixels * * @attribute bounceDistance * @type Number * @default 150 */ bounceDistance : { value: 150 }, /** * The minimum flick gesture velocity (px/ms) at which to trigger the flick response * * @attribute minVelocity * @type Number * @default 0 */ minVelocity : { value: 0 }, /** * The minimum flick gesture distance (px) for which to trigger the flick response * * @attribute minVelocity * @type Number * @default 10 */ minDistance : { value: 10 }, /** * The constraining box relative to which the flick animation and bounds should be calculated. * * @attribute boundingBox * @type Node * @default parentNode */ boundingBox : { valueFn : function() { return this.get(HOST).get(PARENT_NODE); } }, /** * Time between flick animation frames. * * @attribute step * @type Number * @default 10 */ step : { value:10 }, /** * The custom duration to apply to the flick animation. By default, * the animation duration is controlled by the deceleration factor. * * @attribute duration * @type Number * @default null */ duration : { value:null }, /** * The custom transition easing to use for the flick animation. If not * provided defaults to internally to Flick.EASING, or Flick.SNAP_EASING based * on whether or not we're animating the flick or bounce step. * * @attribute easing * @type String * @default null */ easing : { value:null } }; /** * The NAME of the Flick class. Used to prefix events generated * by the plugin. * * @property NAME * @static * @type String * @default "pluginFlick" */ Flick.NAME = "pluginFlick"; /** * The namespace for the plugin. This will be the property on the node, which will * reference the plugin instance, when it's plugged in. * * @property NS * @static * @type String * @default "flick" */ Flick.NS = "flick"; Y.extend(Flick, Y.Plugin.Base, { /** * The initializer lifecycle implementation. * * @method initializer * @param {Object} config The user configuration for the plugin */ initializer : function(config) { this._node = this.get(HOST); this._renderClasses(); this.setBounds(); this._node.on(FLICK, Y.bind(this._onFlick, this), { minDistance : this.get(MIN_DISTANCE), minVelocity : this.get(MIN_VELOCITY) }); }, /** * Sets the min/max boundaries for the flick animation, * based on the boundingBox dimensions. * * @method setBounds */ setBounds : function () { var box = this.get(BOUNDING_BOX), node = this._node, boxHeight = box.get(OFFSET_HEIGHT), boxWidth = box.get(OFFSET_WIDTH), contentHeight = node.get(SCROLL_HEIGHT), contentWidth = node.get(SCROLL_WIDTH); if (contentHeight > boxHeight) { this._maxY = contentHeight - boxHeight; this._minY = 0; this._scrollY = true; } if (contentWidth > boxWidth) { this._maxX = contentWidth - boxWidth; this._minX = 0; this._scrollX = true; } this._x = this._y = 0; node.set("top", this._y + "px"); node.set("left", this._x + "px"); }, /** * Adds the CSS classes, necessary to set up overflow/position properties on the * node and boundingBox. * * @method _renderClasses * @protected */ _renderClasses : function() { this.get(BOUNDING_BOX).addClass(Flick.CLASS_NAMES.box); this._node.addClass(Flick.CLASS_NAMES.content); }, /** * The flick event listener. Kicks off the flick animation. * * @method _onFlick * @param e {EventFacade} The flick event facade, containing e.flick.distance, e.flick.velocity etc. * @protected */ _onFlick: function(e) { this._v = e.flick.velocity; this._flick = true; this._flickAnim(); }, /** * Executes a single frame in the flick animation * * @method _flickFrame * @protected */ _flickAnim: function() { var y = this._y, x = this._x, maxY = this._maxY, minY = this._minY, maxX = this._maxX, minX = this._minX, velocity = this._v, step = this.get(STEP), deceleration = this.get(DECELERATION), bounce = this.get(BOUNCE); this._v = (velocity * deceleration); this._snapToEdge = false; if (this._scrollX) { x = x - (velocity * step); } if (this._scrollY) { y = y - (velocity * step); } if (Math.abs(velocity).toFixed(4) <= Flick.VELOCITY_THRESHOLD) { this._flick = false; this._killTimer(!(this._exceededYBoundary || this._exceededXBoundary)); if (this._scrollX) { if (x < minX) { this._snapToEdge = true; this._setX(minX); } else if (x > maxX) { this._snapToEdge = true; this._setX(maxX); } } if (this._scrollY) { if (y < minY) { this._snapToEdge = true; this._setY(minY); } else if (y > maxY) { this._snapToEdge = true; this._setY(maxY); } } } else { if (this._scrollX && (x < minX || x > maxX)) { this._exceededXBoundary = true; this._v *= bounce; } if (this._scrollY && (y < minY || y > maxY)) { this._exceededYBoundary = true; this._v *= bounce; } if (this._scrollX) { this._setX(x); } if (this._scrollY) { this._setY(y); } this._flickTimer = Y.later(step, this, this._flickAnim); } }, /** * Internal utility method to set the X offset position * * @method _setX * @param {Number} val * @private */ _setX : function(val) { this._move(val, null, this.get(DURATION), this.get(EASING)); }, /** * Internal utility method to set the Y offset position * * @method _setY * @param {Number} val * @private */ _setY : function(val) { this._move(null, val, this.get(DURATION), this.get(EASING)); }, /** * Internal utility method to move the node to a given XY position, * using transitions, if specified. * * @method _move * @param {Number} x The X offset position * @param {Number} y The Y offset position * @param {Number} duration The duration to use for the transition animation * @param {String} easing The easing to use for the transition animation. * * @private */ _move: function(x, y, duration, easing) { if (x !== null) { x = this._bounce(x); } else { x = this._x; } if (y !== null) { y = this._bounce(y); } else { y = this._y; } duration = duration || this._snapToEdge ? Flick.SNAP_DURATION : 0; easing = easing || this._snapToEdge ? Flick.SNAP_EASING : Flick.EASING; this._x = x; this._y = y; this._anim(x, y, duration, easing); }, /** * Internal utility method to perform the transition step * * @method _anim * @param {Number} x The X offset position * @param {Number} y The Y offset position * @param {Number} duration The duration to use for the transition animation * @param {String} easing The easing to use for the transition animation. * * @private */ _anim : function(x, y, duration, easing) { var xn = x * -1, yn = y * -1, transition = { duration : duration / 1000, easing : easing }; Y.log("Transition: duration, easing:" + transition.duration, transition.easing, "node-flick"); if (Y.Transition.useNative) { transition.transform = 'translate('+ (xn) + 'px,' + (yn) +'px)'; } else { transition.left = xn + 'px'; transition.top = yn + 'px'; } this._node.transition(transition); }, /** * Internal utility method to constrain the offset value * based on the bounce criteria. * * @method _bounce * @param {Number} x The offset value to constrain. * @param {Number} max The max offset value. * * @private */ _bounce : function(val, max) { var bounce = this.get(BOUNCE), dist = this.get(BOUNCE_DISTANCE), min = bounce ? -dist : 0; max = bounce ? max + dist : max; if(!bounce) { if(val < min) { val = min; } else if(val > max) { val = max; } } return val; }, /** * Stop the animation timer * * @method _killTimer * @private */ _killTimer: function() { if(this._flickTimer) { this._flickTimer.cancel(); } } }, { /** * The threshold used to determine when the decelerated velocity of the node * is practically 0. * * @property VELOCITY_THRESHOLD * @static * @type Number * @default 0.015 */ VELOCITY_THRESHOLD : 0.015, /** * The duration to use for the bounce snap-back transition * * @property SNAP_DURATION * @static * @type Number * @default 400 */ SNAP_DURATION : 400, /** * The default easing to use for the main flick movement transition * * @property EASING * @static * @type String * @default 'cubic-bezier(0, 0.1, 0, 1.0)' */ EASING : 'cubic-bezier(0, 0.1, 0, 1.0)', /** * The default easing to use for the bounce snap-back transition * * @property SNAP_EASING * @static * @type String * @default 'ease-out' */ SNAP_EASING : 'ease-out', /** * The default CSS class names used by the plugin * * @property CLASS_NAMES * @static * @type Object */ CLASS_NAMES : { box: getClassName(Flick.NS), content: getClassName(Flick.NS, "content") } }); Y.Plugin.Flick = Flick;