Files
ionic-framework/js/views/scrollView.js
Andy Joslin 59c10d4f92 fix(scrollView): if bouncing past boundaries, do not stick.
Closes #482

Zynga Scroller slowly lowers the acceleration of scroll as soon as you pass the
boundaries. Once the acceleration reaches zero, zynga will 'flip' the
acceleration in the other direction and scroll back to within the
boundaries (bounce effect).

The problem is, sometimes as it slowly lowers the
acceleration it will get *near zero*, but not reach zero.  When
acceleration gets close enough to zero, zynga stops the scrolling
because it deems it 'too slow'.

Now, the scrolling acceleration will 'flip' and go back towards being
in boundaries (bounce) when the scrolling is below a certain minimum,
not just when it is below zero.
2014-02-03 12:06:33 -05:00

2025 lines
58 KiB
JavaScript

/*
* Scroller
* http://github.com/zynga/scroller
*
* Copyright 2011, Zynga Inc.
* Licensed under the MIT License.
* https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
*
* Based on the work of: Unify Project (unify-project.org)
* http://unify-project.org
* Copyright 2011, Deutsche Telekom AG
* License: MIT + Apache (V2)
*/
/**
* Generic animation class with support for dropped frames both optional easing and duration.
*
* Optional duration is useful when the lifetime is defined by another condition than time
* e.g. speed of an animating object, etc.
*
* Dropped frame logic allows to keep using the same updater logic independent from the actual
* rendering. This eases a lot of cases where it might be pretty complex to break down a state
* based on the pure time difference.
*/
(function(global) {
var time = Date.now || function() {
return +new Date();
};
var desiredFrames = 60;
var millisecondsPerSecond = 1000;
var running = {};
var counter = 1;
// Create namespaces
if (!global.core) {
global.core = { effect : {} };
} else if (!core.effect) {
core.effect = {};
}
core.effect.Animate = {
/**
* A requestAnimationFrame wrapper / polyfill.
*
* @param callback {Function} The callback to be invoked before the next repaint.
* @param root {HTMLElement} The root element for the repaint
*/
requestAnimationFrame: (function() {
// Check for request animation Frame support
var requestFrame = global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame;
var isNative = !!requestFrame;
if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) {
isNative = false;
}
if (isNative) {
return function(callback, root) {
requestFrame(callback, root)
};
}
var TARGET_FPS = 60;
var requests = {};
var requestCount = 0;
var rafHandle = 1;
var intervalHandle = null;
var lastActive = +new Date();
return function(callback, root) {
var callbackHandle = rafHandle++;
// Store callback
requests[callbackHandle] = callback;
requestCount++;
// Create timeout at first request
if (intervalHandle === null) {
intervalHandle = setInterval(function() {
var time = +new Date();
var currentRequests = requests;
// Reset data structure before executing callbacks
requests = {};
requestCount = 0;
for(var key in currentRequests) {
if (currentRequests.hasOwnProperty(key)) {
currentRequests[key](time);
lastActive = time;
}
}
// Disable the timeout when nothing happens for a certain
// period of time
if (time - lastActive > 2500) {
clearInterval(intervalHandle);
intervalHandle = null;
}
}, 1000 / TARGET_FPS);
}
return callbackHandle;
};
})(),
/**
* Stops the given animation.
*
* @param id {Integer} Unique animation ID
* @return {Boolean} Whether the animation was stopped (aka, was running before)
*/
stop: function(id) {
var cleared = running[id] != null;
if (cleared) {
running[id] = null;
}
return cleared;
},
/**
* Whether the given animation is still running.
*
* @param id {Integer} Unique animation ID
* @return {Boolean} Whether the animation is still running
*/
isRunning: function(id) {
return running[id] != null;
},
/**
* Start the animation.
*
* @param stepCallback {Function} Pointer to function which is executed on every step.
* Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }`
* @param verifyCallback {Function} Executed before every animation step.
* Signature of the method should be `function() { return continueWithAnimation; }`
* @param completedCallback {Function}
* Signature of the method should be `function(droppedFrames, finishedAnimation) {}`
* @param duration {Integer} Milliseconds to run the animation
* @param easingMethod {Function} Pointer to easing function
* Signature of the method should be `function(percent) { return modifiedValue; }`
* @param root {Element} Render root, when available. Used for internal
* usage of requestAnimationFrame.
* @return {Integer} Identifier of animation. Can be used to stop it any time.
*/
start: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
var start = time();
var lastFrame = start;
var percent = 0;
var dropCounter = 0;
var id = counter++;
if (!root) {
root = document.body;
}
// Compacting running db automatically every few new animations
if (id % 20 === 0) {
var newRunning = {};
for (var usedId in running) {
newRunning[usedId] = true;
}
running = newRunning;
}
// This is the internal step method which is called every few milliseconds
var step = function(virtual) {
// Normalize virtual value
var render = virtual !== true;
// Get current time
var now = time();
// Verification is executed before next animation step
if (!running[id] || (verifyCallback && !verifyCallback(id))) {
running[id] = null;
completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false);
return;
}
// For the current rendering to apply let's update omitted steps in memory.
// This is important to bring internal state variables up-to-date with progress in time.
if (render) {
var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1;
for (var j = 0; j < Math.min(droppedFrames, 4); j++) {
step(true);
dropCounter++;
}
}
// Compute percent value
if (duration) {
percent = (now - start) / duration;
if (percent > 1) {
percent = 1;
}
}
// Execute step callback, then...
var value = easingMethod ? easingMethod(percent) : percent;
if ((stepCallback(value, now, render) === false || percent === 1) && render) {
running[id] = null;
completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null);
} else if (render) {
lastFrame = now;
core.effect.Animate.requestAnimationFrame(step, root);
}
};
// Mark as running
running[id] = true;
// Init first step
core.effect.Animate.requestAnimationFrame(step, root);
// Return unique animation ID
return id;
}
};
})(this);
/*
* Scroller
* http://github.com/zynga/scroller
*
* Copyright 2011, Zynga Inc.
* Licensed under the MIT License.
* https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
*
* Based on the work of: Unify Project (unify-project.org)
* http://unify-project.org
* Copyright 2011, Deutsche Telekom AG
* License: MIT + Apache (V2)
*/
var Scroller;
(function(ionic) {
var NOOP = function(){};
// Easing Equations (c) 2003 Robert Penner, all rights reserved.
// Open source under the BSD License.
/**
* @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
**/
var easeOutCubic = function(pos) {
return (Math.pow((pos - 1), 3) + 1);
};
/**
* @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
**/
var easeInOutCubic = function(pos) {
if ((pos /= 0.5) < 1) {
return 0.5 * Math.pow(pos, 3);
}
return 0.5 * (Math.pow((pos - 2), 3) + 2);
};
/**
* ionic.views.Scroll
* A powerful scroll view with support for bouncing, pull to refresh, and paging.
* @param {Object} options options for the scroll view
* @class A scroll view system
* @memberof ionic.views
*/
ionic.views.Scroll = ionic.views.View.inherit({
initialize: function(options) {
var self = this;
this.__container = options.el;
this.__content = options.el.firstElementChild;
this.options = {
/** Disable scrolling on x-axis by default */
scrollingX: false,
scrollbarX: true,
/** Enable scrolling on y-axis */
scrollingY: true,
scrollbarY: true,
startX: 0,
startY: 0,
/** The minimum size the scrollbars scale to while scrolling */
minScrollbarSizeX: 5,
minScrollbarSizeY: 5,
/** Scrollbar fading after scrolling */
scrollbarsFade: true,
scrollbarFadeDelay: 300,
/** The initial fade delay when the pane is resized or initialized */
scrollbarResizeFadeDelay: 1000,
/** Enable animations for deceleration, snap back, zooming and scrolling */
animating: true,
/** duration for animations triggered by scrollTo/zoomTo */
animationDuration: 250,
/** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */
bouncing: true,
/** Enable locking to the main axis if user moves only slightly on one of them at start */
locking: true,
/** Enable pagination mode (switching between full page content panes) */
paging: false,
/** Enable snapping of content to a configured pixel grid */
snapping: false,
/** Enable zooming of content via API, fingers and mouse wheel */
zooming: false,
/** Minimum zoom level */
minZoom: 0.5,
/** Maximum zoom level */
maxZoom: 3,
/** Multiply or decrease scrolling speed **/
speedMultiplier: 1,
/** Callback that is fired on the later of touch end or deceleration end,
provided that another scrolling action has not begun. Used to know
when to fade out a scrollbar. */
scrollingComplete: NOOP,
/** This configures the amount of change applied to deceleration when reaching boundaries **/
penetrationDeceleration : 0.03,
/** This configures the amount of change applied to acceleration when reaching boundaries **/
penetrationAcceleration : 0.08,
// The ms interval for triggering scroll events
scrollEventInterval: 50
};
for (var key in options) {
this.options[key] = options[key];
}
this.hintResize = ionic.debounce(function() {
self.resize();
}, 1000, true);
this.triggerScrollEvent = ionic.throttle(function() {
ionic.trigger('scroll', {
scrollTop: self.__scrollTop,
scrollLeft: self.__scrollLeft,
target: self.__container
});
}, this.options.scrollEventInterval);
this.triggerScrollEndEvent = function() {
ionic.trigger('scrollend', {
scrollTop: self.__scrollTop,
scrollLeft: self.__scrollLeft,
target: self.__container
});
};
this.__scrollLeft = this.options.startX;
this.__scrollTop = this.options.startY;
// Get the render update function, initialize event handlers,
// and calculate the size of the scroll container
this.__callback = this.getRenderFn();
this.__initEventHandlers();
this.__createScrollbars();
},
run: function() {
this.resize();
// Fade them out
this.__fadeScrollbars('out', this.options.scrollbarResizeFadeDelay);
},
/*
---------------------------------------------------------------------------
INTERNAL FIELDS :: STATUS
---------------------------------------------------------------------------
*/
/** Whether only a single finger is used in touch handling */
__isSingleTouch: false,
/** Whether a touch event sequence is in progress */
__isTracking: false,
/** Whether a deceleration animation went to completion. */
__didDecelerationComplete: false,
/**
* Whether a gesture zoom/rotate event is in progress. Activates when
* a gesturestart event happens. This has higher priority than dragging.
*/
__isGesturing: false,
/**
* Whether the user has moved by such a distance that we have enabled
* dragging mode. Hint: It's only enabled after some pixels of movement to
* not interrupt with clicks etc.
*/
__isDragging: false,
/**
* Not touching and dragging anymore, and smoothly animating the
* touch sequence using deceleration.
*/
__isDecelerating: false,
/**
* Smoothly animating the currently configured change
*/
__isAnimating: false,
/*
---------------------------------------------------------------------------
INTERNAL FIELDS :: DIMENSIONS
---------------------------------------------------------------------------
*/
/** Available outer left position (from document perspective) */
__clientLeft: 0,
/** Available outer top position (from document perspective) */
__clientTop: 0,
/** Available outer width */
__clientWidth: 0,
/** Available outer height */
__clientHeight: 0,
/** Outer width of content */
__contentWidth: 0,
/** Outer height of content */
__contentHeight: 0,
/** Snapping width for content */
__snapWidth: 100,
/** Snapping height for content */
__snapHeight: 100,
/** Height to assign to refresh area */
__refreshHeight: null,
/** Whether the refresh process is enabled when the event is released now */
__refreshActive: false,
/** Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */
__refreshActivate: null,
/** Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */
__refreshDeactivate: null,
/** Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */
__refreshStart: null,
/** Zoom level */
__zoomLevel: 1,
/** Scroll position on x-axis */
__scrollLeft: 0,
/** Scroll position on y-axis */
__scrollTop: 0,
/** Maximum allowed scroll position on x-axis */
__maxScrollLeft: 0,
/** Maximum allowed scroll position on y-axis */
__maxScrollTop: 0,
/* Scheduled left position (final position when animating) */
__scheduledLeft: 0,
/* Scheduled top position (final position when animating) */
__scheduledTop: 0,
/* Scheduled zoom level (final scale when animating) */
__scheduledZoom: 0,
/*
---------------------------------------------------------------------------
INTERNAL FIELDS :: LAST POSITIONS
---------------------------------------------------------------------------
*/
/** Left position of finger at start */
__lastTouchLeft: null,
/** Top position of finger at start */
__lastTouchTop: null,
/** Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */
__lastTouchMove: null,
/** List of positions, uses three indexes for each state: left, top, timestamp */
__positions: null,
/*
---------------------------------------------------------------------------
INTERNAL FIELDS :: DECELERATION SUPPORT
---------------------------------------------------------------------------
*/
/** Minimum left scroll position during deceleration */
__minDecelerationScrollLeft: null,
/** Minimum top scroll position during deceleration */
__minDecelerationScrollTop: null,
/** Maximum left scroll position during deceleration */
__maxDecelerationScrollLeft: null,
/** Maximum top scroll position during deceleration */
__maxDecelerationScrollTop: null,
/** Current factor to modify horizontal scroll position with on every step */
__decelerationVelocityX: null,
/** Current factor to modify vertical scroll position with on every step */
__decelerationVelocityY: null,
/** the browser-specific property to use for transforms */
__transformProperty: null,
__perspectiveProperty: null,
/** scrollbar indicators */
__indicatorX: null,
__indicatorY: null,
/** Timeout for scrollbar fading */
__scrollbarFadeTimeout: null,
/** whether we've tried to wait for size already */
__didWaitForSize: null,
__sizerTimeout: null,
__initEventHandlers: function() {
var self = this;
// Event Handler
var container = this.__container;
if ('ontouchstart' in window) {
container.addEventListener("touchstart", function(e) {
// Don't react if initial down happens on a form element
if (e.target.tagName.match(/input|textarea|select/i)) {
return;
}
self.doTouchStart(e.touches, e.timeStamp);
e.preventDefault();
}, false);
document.addEventListener("touchmove", function(e) {
if(e.defaultPrevented) {
return;
}
self.doTouchMove(e.touches, e.timeStamp);
}, false);
document.addEventListener("touchend", function(e) {
self.doTouchEnd(e.timeStamp);
}, false);
} else {
var mousedown = false;
container.addEventListener("mousedown", function(e) {
// Don't react if initial down happens on a form element
if (e.target.tagName.match(/input|textarea|select/i)) {
return;
}
self.doTouchStart([{
pageX: e.pageX,
pageY: e.pageY
}], e.timeStamp);
mousedown = true;
}, false);
document.addEventListener("mousemove", function(e) {
if (!mousedown || e.defaultPrevented) {
return;
}
self.doTouchMove([{
pageX: e.pageX,
pageY: e.pageY
}], e.timeStamp);
mousedown = true;
}, false);
document.addEventListener("mouseup", function(e) {
if (!mousedown) {
return;
}
self.doTouchEnd(e.timeStamp);
mousedown = false;
}, false);
}
},
/** Create a scroll bar div with the given direction **/
__createScrollbar: function(direction) {
var bar = document.createElement('div'),
indicator = document.createElement('div');
indicator.className = 'scroll-bar-indicator';
if(direction == 'h') {
bar.className = 'scroll-bar scroll-bar-h';
} else {
bar.className = 'scroll-bar scroll-bar-v';
}
bar.appendChild(indicator);
return bar;
},
__createScrollbars: function() {
var indicatorX, indicatorY;
if(this.options.scrollingX) {
indicatorX = {
el: this.__createScrollbar('h'),
sizeRatio: 1
};
indicatorX.indicator = indicatorX.el.children[0];
if(this.options.scrollbarX) {
this.__container.appendChild(indicatorX.el);
}
this.__indicatorX = indicatorX;
}
if(this.options.scrollingY) {
indicatorY = {
el: this.__createScrollbar('v'),
sizeRatio: 1
};
indicatorY.indicator = indicatorY.el.children[0];
if(this.options.scrollbarY) {
this.__container.appendChild(indicatorY.el);
}
this.__indicatorY = indicatorY;
}
},
__resizeScrollbars: function() {
var self = this;
// Bring the scrollbars in to show the content change
self.__fadeScrollbars('in');
// Update horiz bar
if(self.__indicatorX) {
var width = Math.max(Math.round(self.__clientWidth * self.__clientWidth / (self.__contentWidth)), 20);
if(width > self.__contentWidth) {
width = 0;
}
self.__indicatorX.size = width;
self.__indicatorX.minScale = this.options.minScrollbarSizeX / width;
self.__indicatorX.indicator.style.width = width + 'px';
self.__indicatorX.maxPos = self.__clientWidth - width;
self.__indicatorX.sizeRatio = self.__maxScrollLeft ? self.__indicatorX.maxPos / self.__maxScrollLeft : 1;
}
// Update vert bar
if(self.__indicatorY) {
var height = Math.max(Math.round(self.__clientHeight * self.__clientHeight / (self.__contentHeight)), 20);
if(height > self.__contentHeight) {
height = 0;
}
self.__indicatorY.size = height;
self.__indicatorY.minScale = this.options.minScrollbarSizeY / height;
self.__indicatorY.maxPos = self.__clientHeight - height;
self.__indicatorY.indicator.style.height = height + 'px';
self.__indicatorY.sizeRatio = self.__maxScrollTop ? self.__indicatorY.maxPos / self.__maxScrollTop : 1;
}
},
/**
* Move and scale the scrollbars as the page scrolls.
*/
__repositionScrollbars: function() {
var self = this, width, heightScale,
widthDiff, heightDiff,
x, y,
xstop = 0, ystop = 0;
if(self.__indicatorX) {
// Handle the X scrollbar
// Don't go all the way to the right if we have a vertical scrollbar as well
if(self.__indicatorY) xstop = 10;
x = Math.round(self.__indicatorX.sizeRatio * self.__scrollLeft) || 0,
// The the difference between the last content X position, and our overscrolled one
widthDiff = self.__scrollLeft - (self.__maxScrollLeft - xstop);
if(self.__scrollLeft < 0) {
widthScale = Math.max(self.__indicatorX.minScale,
(self.__indicatorX.size - Math.abs(self.__scrollLeft)) / self.__indicatorX.size);
// Stay at left
x = 0;
// Make sure scale is transformed from the left/center origin point
self.__indicatorX.indicator.style[self.__transformOriginProperty] = 'left center';
} else if(widthDiff > 0) {
widthScale = Math.max(self.__indicatorX.minScale,
(self.__indicatorX.size - widthDiff) / self.__indicatorX.size);
// Stay at the furthest x for the scrollable viewport
x = self.__indicatorX.maxPos - xstop;
// Make sure scale is transformed from the right/center origin point
self.__indicatorX.indicator.style[self.__transformOriginProperty] = 'right center';
} else {
// Normal motion
x = Math.min(self.__maxScrollLeft, Math.max(0, x));
widthScale = 1;
}
self.__indicatorX.indicator.style[self.__transformProperty] = 'translate3d(' + x + 'px, 0, 0) scaleX(' + widthScale + ')';
}
if(self.__indicatorY) {
y = Math.round(self.__indicatorY.sizeRatio * self.__scrollTop) || 0;
// Don't go all the way to the right if we have a vertical scrollbar as well
if(self.__indicatorX) ystop = 10;
heightDiff = self.__scrollTop - (self.__maxScrollTop - ystop);
if(self.__scrollTop < 0) {
heightScale = Math.max(self.__indicatorY.minScale, (self.__indicatorY.size - Math.abs(self.__scrollTop)) / self.__indicatorY.size);
// Stay at top
y = 0;
// Make sure scale is transformed from the center/top origin point
self.__indicatorY.indicator.style[self.__transformOriginProperty] = 'center top';
} else if(heightDiff > 0) {
heightScale = Math.max(self.__indicatorY.minScale, (self.__indicatorY.size - heightDiff) / self.__indicatorY.size);
// Stay at bottom of scrollable viewport
y = self.__indicatorY.maxPos - ystop;
// Make sure scale is transformed from the center/bottom origin point
self.__indicatorY.indicator.style[self.__transformOriginProperty] = 'center bottom';
} else {
// Normal motion
y = Math.min(self.__maxScrollTop, Math.max(0, y));
heightScale = 1;
}
self.__indicatorY.indicator.style[self.__transformProperty] = 'translate3d(0,' + y + 'px, 0) scaleY(' + heightScale + ')';
}
},
__fadeScrollbars: function(direction, delay) {
var self = this;
if(!this.options.scrollbarsFade) {
return;
}
var className = 'scroll-bar-fade-out';
if(self.options.scrollbarsFade === true) {
clearTimeout(self.__scrollbarFadeTimeout);
if(direction == 'in') {
if(self.__indicatorX) { self.__indicatorX.indicator.classList.remove(className); }
if(self.__indicatorY) { self.__indicatorY.indicator.classList.remove(className); }
} else {
self.__scrollbarFadeTimeout = setTimeout(function() {
if(self.__indicatorX) { self.__indicatorX.indicator.classList.add(className); }
if(self.__indicatorY) { self.__indicatorY.indicator.classList.add(className); }
}, delay || self.options.scrollbarFadeDelay);
}
}
},
__scrollingComplete: function() {
var self = this;
self.options.scrollingComplete();
self.__fadeScrollbars('out');
},
resize: function() {
// Update Scroller dimensions for changed content
// Add padding to bottom of content
this.setDimensions(
this.__container.clientWidth,
this.__container.clientHeight,
Math.max(this.__content.scrollWidth, this.__content.offsetWidth),
Math.max(this.__content.scrollHeight, this.__content.offsetHeight+20));
},
/*
---------------------------------------------------------------------------
PUBLIC API
---------------------------------------------------------------------------
*/
getRenderFn: function() {
var self = this;
var content = this.__content;
var docStyle = document.documentElement.style;
var engine;
if ('MozAppearance' in docStyle) {
engine = 'gecko';
} else if ('WebkitAppearance' in docStyle) {
engine = 'webkit';
} else if (typeof navigator.cpuClass === 'string') {
engine = 'trident';
}
var vendorPrefix = {
trident: 'ms',
gecko: 'Moz',
webkit: 'Webkit',
presto: 'O'
}[engine];
var helperElem = document.createElement("div");
var undef;
var perspectiveProperty = vendorPrefix + "Perspective";
var transformProperty = vendorPrefix + "Transform";
var transformOriginProperty = vendorPrefix + 'TransformOrigin';
self.__perspectiveProperty = transformProperty;
self.__transformProperty = transformProperty;
self.__transformOriginProperty = transformOriginProperty;
if (helperElem.style[perspectiveProperty] !== undef) {
return function(left, top, zoom) {
content.style[transformProperty] = 'translate3d(' + (-left) + 'px,' + (-top) + 'px,0)';
self.__repositionScrollbars();
self.triggerScrollEvent();
};
} else if (helperElem.style[transformProperty] !== undef) {
return function(left, top, zoom) {
content.style[transformProperty] = 'translate(' + (-left) + 'px,' + (-top) + 'px)';
self.__repositionScrollbars();
self.triggerScrollEvent();
};
} else {
return function(left, top, zoom) {
content.style.marginLeft = left ? (-left/zoom) + 'px' : '';
content.style.marginTop = top ? (-top/zoom) + 'px' : '';
content.style.zoom = zoom || '';
self.__repositionScrollbars();
self.triggerScrollEvent();
};
}
},
/**
* Configures the dimensions of the client (outer) and content (inner) elements.
* Requires the available space for the outer element and the outer size of the inner element.
* All values which are falsy (null or zero etc.) are ignored and the old value is kept.
*
* @param clientWidth {Integer} Inner width of outer element
* @param clientHeight {Integer} Inner height of outer element
* @param contentWidth {Integer} Outer width of inner element
* @param contentHeight {Integer} Outer height of inner element
*/
setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight) {
var self = this;
// Only update values which are defined
if (clientWidth === +clientWidth) {
self.__clientWidth = clientWidth;
}
if (clientHeight === +clientHeight) {
self.__clientHeight = clientHeight;
}
if (contentWidth === +contentWidth) {
self.__contentWidth = contentWidth;
}
if (contentHeight === +contentHeight) {
self.__contentHeight = contentHeight;
}
// Refresh maximums
self.__computeScrollMax();
self.__resizeScrollbars();
// Refresh scroll position
self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
},
/**
* Sets the client coordinates in relation to the document.
*
* @param left {Integer} Left position of outer element
* @param top {Integer} Top position of outer element
*/
setPosition: function(left, top) {
var self = this;
self.__clientLeft = left || 0;
self.__clientTop = top || 0;
},
/**
* Configures the snapping (when snapping is active)
*
* @param width {Integer} Snapping width
* @param height {Integer} Snapping height
*/
setSnapSize: function(width, height) {
var self = this;
self.__snapWidth = width;
self.__snapHeight = height;
},
/**
* Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever
* the user event is released during visibility of this zone. This was introduced by some apps on iOS like
* the official Twitter client.
*
* @param height {Integer} Height of pull-to-refresh zone on top of rendered list
* @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release.
* @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled.
* @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh.
*/
activatePullToRefresh: function(height, activateCallback, deactivateCallback, startCallback) {
var self = this;
self.__refreshHeight = height;
self.__refreshActivate = activateCallback;
self.__refreshDeactivate = deactivateCallback;
self.__refreshStart = startCallback;
},
/**
* Starts pull-to-refresh manually.
*/
triggerPullToRefresh: function() {
// Use publish instead of scrollTo to allow scrolling to out of boundary position
// We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true);
if (this.__refreshStart) {
this.__refreshStart();
}
},
/**
* Signalizes that pull-to-refresh is finished.
*/
finishPullToRefresh: function() {
var self = this;
self.__refreshActive = false;
if (self.__refreshDeactivate) {
self.__refreshDeactivate();
}
self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
},
/**
* Returns the scroll position and zooming values
*
* @return {Map} `left` and `top` scroll position and `zoom` level
*/
getValues: function() {
var self = this;
return {
left: self.__scrollLeft,
top: self.__scrollTop,
zoom: self.__zoomLevel
};
},
/**
* Returns the maximum scroll values
*
* @return {Map} `left` and `top` maximum scroll values
*/
getScrollMax: function() {
var self = this;
return {
left: self.__maxScrollLeft,
top: self.__maxScrollTop
};
},
/**
* Zooms to the given level. Supports optional animation. Zooms
* the center when no coordinates are given.
*
* @param level {Number} Level to zoom to
* @param animate {Boolean} Whether to use animation
* @param originLeft {Number} Zoom in at given left coordinate
* @param originTop {Number} Zoom in at given top coordinate
*/
zoomTo: function(level, animate, originLeft, originTop) {
var self = this;
if (!self.options.zooming) {
throw new Error("Zooming is not enabled!");
}
// Stop deceleration
if (self.__isDecelerating) {
core.effect.Animate.stop(self.__isDecelerating);
self.__isDecelerating = false;
}
var oldLevel = self.__zoomLevel;
// Normalize input origin to center of viewport if not defined
if (originLeft == null) {
originLeft = self.__clientWidth / 2;
}
if (originTop == null) {
originTop = self.__clientHeight / 2;
}
// Limit level according to configuration
level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
// Recompute maximum values while temporary tweaking maximum scroll ranges
self.__computeScrollMax(level);
// Recompute left and top coordinates based on new zoom level
var left = ((originLeft + self.__scrollLeft) * level / oldLevel) - originLeft;
var top = ((originTop + self.__scrollTop) * level / oldLevel) - originTop;
// Limit x-axis
if (left > self.__maxScrollLeft) {
left = self.__maxScrollLeft;
} else if (left < 0) {
left = 0;
}
// Limit y-axis
if (top > self.__maxScrollTop) {
top = self.__maxScrollTop;
} else if (top < 0) {
top = 0;
}
// Push values out
self.__publish(left, top, level, animate);
},
/**
* Zooms the content by the given factor.
*
* @param factor {Number} Zoom by given factor
* @param animate {Boolean} Whether to use animation
* @param originLeft {Number} Zoom in at given left coordinate
* @param originTop {Number} Zoom in at given top coordinate
*/
zoomBy: function(factor, animate, originLeft, originTop) {
var self = this;
self.zoomTo(self.__zoomLevel * factor, animate, originLeft, originTop);
},
/**
* Scrolls to the given position. Respect limitations and snapping automatically.
*
* @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code>
* @param top {Number} Vertical scroll position, keeps current if value is <code>null</code>
* @param animate {Boolean} Whether the scrolling should happen using an animation
* @param zoom {Number} Zoom level to go to
*/
scrollTo: function(left, top, animate, zoom) {
var self = this;
// Stop deceleration
if (self.__isDecelerating) {
core.effect.Animate.stop(self.__isDecelerating);
self.__isDecelerating = false;
}
// Correct coordinates based on new zoom level
if (zoom != null && zoom !== self.__zoomLevel) {
if (!self.options.zooming) {
throw new Error("Zooming is not enabled!");
}
left *= zoom;
top *= zoom;
// Recompute maximum values while temporary tweaking maximum scroll ranges
self.__computeScrollMax(zoom);
} else {
// Keep zoom when not defined
zoom = self.__zoomLevel;
}
if (!self.options.scrollingX) {
left = self.__scrollLeft;
} else {
if (self.options.paging) {
left = Math.round(left / self.__clientWidth) * self.__clientWidth;
} else if (self.options.snapping) {
left = Math.round(left / self.__snapWidth) * self.__snapWidth;
}
}
if (!self.options.scrollingY) {
top = self.__scrollTop;
} else {
if (self.options.paging) {
top = Math.round(top / self.__clientHeight) * self.__clientHeight;
} else if (self.options.snapping) {
top = Math.round(top / self.__snapHeight) * self.__snapHeight;
}
}
// Limit for allowed ranges
left = Math.max(Math.min(self.__maxScrollLeft, left), 0);
top = Math.max(Math.min(self.__maxScrollTop, top), 0);
// Don't animate when no change detected, still call publish to make sure
// that rendered position is really in-sync with internal data
if (left === self.__scrollLeft && top === self.__scrollTop) {
animate = false;
}
// Publish new values
self.__publish(left, top, zoom, animate);
},
/**
* Scroll by the given offset
*
* @param left {Number} Scroll x-axis by given offset
* @param top {Number} Scroll x-axis by given offset
* @param animate {Boolean} Whether to animate the given change
*/
scrollBy: function(left, top, animate) {
var self = this;
var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;
self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
},
/*
---------------------------------------------------------------------------
EVENT CALLBACKS
---------------------------------------------------------------------------
*/
/**
* Mouse wheel handler for zooming support
*/
doMouseZoom: function(wheelDelta, timeStamp, pageX, pageY) {
var self = this;
var change = wheelDelta > 0 ? 0.97 : 1.03;
return self.zoomTo(self.__zoomLevel * change, false, pageX - self.__clientLeft, pageY - self.__clientTop);
},
/**
* Touch start handler for scrolling support
*/
doTouchStart: function(touches, timeStamp) {
this.hintResize();
// Array-like check is enough here
if (touches.length == null) {
throw new Error("Invalid touch list: " + touches);
}
if (timeStamp instanceof Date) {
timeStamp = timeStamp.valueOf();
}
if (typeof timeStamp !== "number") {
throw new Error("Invalid timestamp value: " + timeStamp);
}
var self = this;
self.__fadeScrollbars('in');
// Reset interruptedAnimation flag
self.__interruptedAnimation = true;
// Stop deceleration
if (self.__isDecelerating) {
core.effect.Animate.stop(self.__isDecelerating);
self.__isDecelerating = false;
self.__interruptedAnimation = true;
}
// Stop animation
if (self.__isAnimating) {
core.effect.Animate.stop(self.__isAnimating);
self.__isAnimating = false;
self.__interruptedAnimation = true;
}
// Use center point when dealing with two fingers
var currentTouchLeft, currentTouchTop;
var isSingleTouch = touches.length === 1;
if (isSingleTouch) {
currentTouchLeft = touches[0].pageX;
currentTouchTop = touches[0].pageY;
} else {
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
}
// Store initial positions
self.__initialTouchLeft = currentTouchLeft;
self.__initialTouchTop = currentTouchTop;
// Store current zoom level
self.__zoomLevelStart = self.__zoomLevel;
// Store initial touch positions
self.__lastTouchLeft = currentTouchLeft;
self.__lastTouchTop = currentTouchTop;
// Store initial move time stamp
self.__lastTouchMove = timeStamp;
// Reset initial scale
self.__lastScale = 1;
// Reset locking flags
self.__enableScrollX = !isSingleTouch && self.options.scrollingX;
self.__enableScrollY = !isSingleTouch && self.options.scrollingY;
// Reset tracking flag
self.__isTracking = true;
// Reset deceleration complete flag
self.__didDecelerationComplete = false;
// Dragging starts directly with two fingers, otherwise lazy with an offset
self.__isDragging = !isSingleTouch;
// Some features are disabled in multi touch scenarios
self.__isSingleTouch = isSingleTouch;
// Clearing data structure
self.__positions = [];
},
/**
* Touch move handler for scrolling support
*/
doTouchMove: function(touches, timeStamp, scale) {
// Array-like check is enough here
if (touches.length == null) {
throw new Error("Invalid touch list: " + touches);
}
if (timeStamp instanceof Date) {
timeStamp = timeStamp.valueOf();
}
if (typeof timeStamp !== "number") {
throw new Error("Invalid timestamp value: " + timeStamp);
}
var self = this;
// Ignore event when tracking is not enabled (event might be outside of element)
if (!self.__isTracking) {
return;
}
var currentTouchLeft, currentTouchTop;
// Compute move based around of center of fingers
if (touches.length === 2) {
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
} else {
currentTouchLeft = touches[0].pageX;
currentTouchTop = touches[0].pageY;
}
var positions = self.__positions;
// Are we already is dragging mode?
if (self.__isDragging) {
// Compute move distance
var moveX = currentTouchLeft - self.__lastTouchLeft;
var moveY = currentTouchTop - self.__lastTouchTop;
// Read previous scroll position and zooming
var scrollLeft = self.__scrollLeft;
var scrollTop = self.__scrollTop;
var level = self.__zoomLevel;
// Work with scaling
if (scale != null && self.options.zooming) {
var oldLevel = level;
// Recompute level based on previous scale and new scale
level = level / self.__lastScale * scale;
// Limit level according to configuration
level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
// Only do further compution when change happened
if (oldLevel !== level) {
// Compute relative event position to container
var currentTouchLeftRel = currentTouchLeft - self.__clientLeft;
var currentTouchTopRel = currentTouchTop - self.__clientTop;
// Recompute left and top coordinates based on new zoom level
scrollLeft = ((currentTouchLeftRel + scrollLeft) * level / oldLevel) - currentTouchLeftRel;
scrollTop = ((currentTouchTopRel + scrollTop) * level / oldLevel) - currentTouchTopRel;
// Recompute max scroll values
self.__computeScrollMax(level);
}
}
if (self.__enableScrollX) {
scrollLeft -= moveX * this.options.speedMultiplier;
var maxScrollLeft = self.__maxScrollLeft;
if (scrollLeft > maxScrollLeft || scrollLeft < 0) {
// Slow down on the edges
if (self.options.bouncing) {
scrollLeft += (moveX / 2 * this.options.speedMultiplier);
} else if (scrollLeft > maxScrollLeft) {
scrollLeft = maxScrollLeft;
} else {
scrollLeft = 0;
}
}
}
// Compute new vertical scroll position
if (self.__enableScrollY) {
scrollTop -= moveY * this.options.speedMultiplier;
var maxScrollTop = self.__maxScrollTop;
if (scrollTop > maxScrollTop || scrollTop < 0) {
// Slow down on the edges
if (self.options.bouncing || (self.__refreshHeight && scrollTop < 0)) {
scrollTop += (moveY / 2 * this.options.speedMultiplier);
// Support pull-to-refresh (only when only y is scrollable)
if (!self.__enableScrollX && self.__refreshHeight != null) {
if (!self.__refreshActive && scrollTop <= -self.__refreshHeight) {
self.__refreshActive = true;
if (self.__refreshActivate) {
self.__refreshActivate();
}
} else if (self.__refreshActive && scrollTop > -self.__refreshHeight) {
self.__refreshActive = false;
if (self.__refreshDeactivate) {
self.__refreshDeactivate();
}
}
}
} else if (scrollTop > maxScrollTop) {
scrollTop = maxScrollTop;
} else {
scrollTop = 0;
}
}
}
// Keep list from growing infinitely (holding min 10, max 20 measure points)
if (positions.length > 60) {
positions.splice(0, 30);
}
// Track scroll movement for decleration
positions.push(scrollLeft, scrollTop, timeStamp);
// Sync scroll position
self.__publish(scrollLeft, scrollTop, level);
// Otherwise figure out whether we are switching into dragging mode now.
} else {
var minimumTrackingForScroll = self.options.locking ? 3 : 0;
var minimumTrackingForDrag = 5;
var distanceX = Math.abs(currentTouchLeft - self.__initialTouchLeft);
var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop);
self.__enableScrollX = self.options.scrollingX && distanceX >= minimumTrackingForScroll;
self.__enableScrollY = self.options.scrollingY && distanceY >= minimumTrackingForScroll;
positions.push(self.__scrollLeft, self.__scrollTop, timeStamp);
self.__isDragging = (self.__enableScrollX || self.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag);
if (self.__isDragging) {
self.__interruptedAnimation = false;
}
}
// Update last touch positions and time stamp for next event
self.__lastTouchLeft = currentTouchLeft;
self.__lastTouchTop = currentTouchTop;
self.__lastTouchMove = timeStamp;
self.__lastScale = scale;
},
/**
* Touch end handler for scrolling support
*/
doTouchEnd: function(timeStamp) {
if (timeStamp instanceof Date) {
timeStamp = timeStamp.valueOf();
}
if (typeof timeStamp !== "number") {
throw new Error("Invalid timestamp value: " + timeStamp);
}
var self = this;
// Ignore event when tracking is not enabled (no touchstart event on element)
// This is required as this listener ('touchmove') sits on the document and not on the element itself.
if (!self.__isTracking) {
return;
}
// Not touching anymore (when two finger hit the screen there are two touch end events)
self.__isTracking = false;
// Be sure to reset the dragging flag now. Here we also detect whether
// the finger has moved fast enough to switch into a deceleration animation.
if (self.__isDragging) {
// Reset dragging flag
self.__isDragging = false;
// Start deceleration
// Verify that the last move detected was in some relevant time frame
if (self.__isSingleTouch && self.options.animating && (timeStamp - self.__lastTouchMove) <= 100) {
// Then figure out what the scroll position was about 100ms ago
var positions = self.__positions;
var endPos = positions.length - 1;
var startPos = endPos;
// Move pointer to position measured 100ms ago
for (var i = endPos; i > 0 && positions[i] > (self.__lastTouchMove - 100); i -= 3) {
startPos = i;
}
// If start and stop position is identical in a 100ms timeframe,
// we cannot compute any useful deceleration.
if (startPos !== endPos) {
// Compute relative movement between these two points
var timeOffset = positions[endPos] - positions[startPos];
var movedLeft = self.__scrollLeft - positions[startPos - 2];
var movedTop = self.__scrollTop - positions[startPos - 1];
// Based on 50ms compute the movement to apply for each render step
self.__decelerationVelocityX = movedLeft / timeOffset * (1000 / 60);
self.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60);
// How much velocity is required to start the deceleration
var minVelocityToStartDeceleration = self.options.paging || self.options.snapping ? 4 : 1;
// Verify that we have enough velocity to start deceleration
if (Math.abs(self.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration) {
// Deactivate pull-to-refresh when decelerating
if (!self.__refreshActive) {
self.__startDeceleration(timeStamp);
}
}
} else {
self.__scrollingComplete();
}
} else if ((timeStamp - self.__lastTouchMove) > 100) {
self.__scrollingComplete();
}
}
// If this was a slower move it is per default non decelerated, but this
// still means that we want snap back to the bounds which is done here.
// This is placed outside the condition above to improve edge case stability
// e.g. touchend fired without enabled dragging. This should normally do not
// have modified the scroll positions or even showed the scrollbars though.
if (!self.__isDecelerating) {
if (self.__refreshActive && self.__refreshStart) {
// Use publish instead of scrollTo to allow scrolling to out of boundary position
// We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
self.__publish(self.__scrollLeft, -self.__refreshHeight, self.__zoomLevel, true);
if (self.__refreshStart) {
self.__refreshStart();
}
} else {
if (self.__interruptedAnimation || self.__isDragging) {
self.__scrollingComplete();
}
self.scrollTo(self.__scrollLeft, self.__scrollTop, true, self.__zoomLevel);
// Directly signalize deactivation (nothing todo on refresh?)
if (self.__refreshActive) {
self.__refreshActive = false;
if (self.__refreshDeactivate) {
self.__refreshDeactivate();
}
}
}
}
// Fully cleanup list
self.__positions.length = 0;
},
/*
---------------------------------------------------------------------------
PRIVATE API
---------------------------------------------------------------------------
*/
/**
* Applies the scroll position to the content element
*
* @param left {Number} Left scroll position
* @param top {Number} Top scroll position
* @param animate {Boolean} Whether animation should be used to move to the new coordinates
*/
__publish: function(left, top, zoom, animate) {
var self = this;
// Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
var wasAnimating = self.__isAnimating;
if (wasAnimating) {
core.effect.Animate.stop(wasAnimating);
self.__isAnimating = false;
}
if (animate && self.options.animating) {
// Keep scheduled positions for scrollBy/zoomBy functionality
self.__scheduledLeft = left;
self.__scheduledTop = top;
self.__scheduledZoom = zoom;
var oldLeft = self.__scrollLeft;
var oldTop = self.__scrollTop;
var oldZoom = self.__zoomLevel;
var diffLeft = left - oldLeft;
var diffTop = top - oldTop;
var diffZoom = zoom - oldZoom;
var step = function(percent, now, render) {
if (render) {
self.__scrollLeft = oldLeft + (diffLeft * percent);
self.__scrollTop = oldTop + (diffTop * percent);
self.__zoomLevel = oldZoom + (diffZoom * percent);
// Push values out
if (self.__callback) {
self.__callback(self.__scrollLeft, self.__scrollTop, self.__zoomLevel);
}
}
};
var verify = function(id) {
return self.__isAnimating === id;
};
var completed = function(renderedFramesPerSecond, animationId, wasFinished) {
if (animationId === self.__isAnimating) {
self.__isAnimating = false;
}
if (self.__didDecelerationComplete || wasFinished) {
self.__scrollingComplete();
}
if (self.options.zooming) {
self.__computeScrollMax();
}
};
// When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
self.__isAnimating = core.effect.Animate.start(step, verify, completed, self.options.animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic);
} else {
self.__scheduledLeft = self.__scrollLeft = left;
self.__scheduledTop = self.__scrollTop = top;
self.__scheduledZoom = self.__zoomLevel = zoom;
// Push values out
if (self.__callback) {
self.__callback(left, top, zoom);
}
// Fix max scroll ranges
if (self.options.zooming) {
self.__computeScrollMax();
}
}
},
/**
* Recomputes scroll minimum values based on client dimensions and content dimensions.
*/
__computeScrollMax: function(zoomLevel) {
var self = this;
if (zoomLevel == null) {
zoomLevel = self.__zoomLevel;
}
self.__maxScrollLeft = Math.max((self.__contentWidth * zoomLevel) - self.__clientWidth, 0);
self.__maxScrollTop = Math.max((self.__contentHeight * zoomLevel) - self.__clientHeight, 0);
if(!self.__didWaitForSize && self.__maxScrollLeft == 0 && self.__maxScrollTop == 0) {
self.__didWaitForSize = true;
self.__waitForSize();
}
},
/**
* If the scroll view isn't sized correctly on start, wait until we have at least some size
*/
__waitForSize: function() {
var self = this;
clearTimeout(self.__sizerTimeout);
var sizer = function() {
self.resize();
if((self.options.scrollingX && self.__maxScrollLeft == 0) || (self.options.scrollingY && self.__maxScrollTop == 0)) {
//self.__sizerTimeout = setTimeout(sizer, 1000);
}
};
sizer();
self.__sizerTimeout = setTimeout(sizer, 1000);
},
/*
---------------------------------------------------------------------------
ANIMATION (DECELERATION) SUPPORT
---------------------------------------------------------------------------
*/
/**
* Called when a touch sequence end and the speed of the finger was high enough
* to switch into deceleration mode.
*/
__startDeceleration: function(timeStamp) {
var self = this;
if (self.options.paging) {
var scrollLeft = Math.max(Math.min(self.__scrollLeft, self.__maxScrollLeft), 0);
var scrollTop = Math.max(Math.min(self.__scrollTop, self.__maxScrollTop), 0);
var clientWidth = self.__clientWidth;
var clientHeight = self.__clientHeight;
// We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area.
// Each page should have exactly the size of the client area.
self.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth;
self.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight;
self.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth;
self.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight;
} else {
self.__minDecelerationScrollLeft = 0;
self.__minDecelerationScrollTop = 0;
self.__maxDecelerationScrollLeft = self.__maxScrollLeft;
self.__maxDecelerationScrollTop = self.__maxScrollTop;
}
// Wrap class method
var step = function(percent, now, render) {
self.__stepThroughDeceleration(render);
};
// How much velocity is required to keep the deceleration running
var minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.1;
// Detect whether it's still worth to continue animating steps
// If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
var verify = function() {
var shouldContinue = Math.abs(self.__decelerationVelocityX) >= minVelocityToKeepDecelerating || Math.abs(self.__decelerationVelocityY) >= minVelocityToKeepDecelerating;
if (!shouldContinue) {
self.__didDecelerationComplete = true;
}
return shouldContinue;
};
var completed = function(renderedFramesPerSecond, animationId, wasFinished) {
self.__isDecelerating = false;
if (self.__didDecelerationComplete) {
self.__scrollingComplete();
}
// Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
if(self.options.paging) {
self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping);
}
};
// Start animation and switch on flag
self.__isDecelerating = core.effect.Animate.start(step, verify, completed);
},
/**
* Called on every step of the animation
*
* @param inMemory {Boolean} Whether to not render the current step, but keep it in memory only. Used internally only!
*/
__stepThroughDeceleration: function(render) {
var self = this;
//
// COMPUTE NEXT SCROLL POSITION
//
// Add deceleration to scroll position
var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX;
var scrollTop = self.__scrollTop + self.__decelerationVelocityY;
//
// HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE
//
if (!self.options.bouncing) {
var scrollLeftFixed = Math.max(Math.min(self.__maxDecelerationScrollLeft, scrollLeft), self.__minDecelerationScrollLeft);
if (scrollLeftFixed !== scrollLeft) {
scrollLeft = scrollLeftFixed;
self.__decelerationVelocityX = 0;
}
var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop);
if (scrollTopFixed !== scrollTop) {
scrollTop = scrollTopFixed;
self.__decelerationVelocityY = 0;
}
}
//
// UPDATE SCROLL POSITION
//
if (render) {
self.__publish(scrollLeft, scrollTop, self.__zoomLevel);
} else {
self.__scrollLeft = scrollLeft;
self.__scrollTop = scrollTop;
}
//
// SLOW DOWN
//
// Slow down velocity on every iteration
if (!self.options.paging) {
// This is the factor applied to every iteration of the animation
// to slow down the process. This should emulate natural behavior where
// objects slow down when the initiator of the movement is removed
var frictionFactor = 0.95;
self.__decelerationVelocityX *= frictionFactor;
self.__decelerationVelocityY *= frictionFactor;
}
//
// BOUNCING SUPPORT
//
if (self.options.bouncing) {
var scrollOutsideX = 0;
var scrollOutsideY = 0;
// This configures the amount of change applied to deceleration/acceleration when reaching boundaries
var penetrationDeceleration = self.options.penetrationDeceleration;
var penetrationAcceleration = self.options.penetrationAcceleration;
// Check limits
if (scrollLeft < self.__minDecelerationScrollLeft) {
scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft;
} else if (scrollLeft > self.__maxDecelerationScrollLeft) {
scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft;
}
if (scrollTop < self.__minDecelerationScrollTop) {
scrollOutsideY = self.__minDecelerationScrollTop - scrollTop;
} else if (scrollTop > self.__maxDecelerationScrollTop) {
scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop;
}
// Slow down until slow enough, then flip back to snap position
if (scrollOutsideX !== 0) {
if (scrollOutsideX * self.__decelerationVelocityX <= self.__minDecelerationScrollLeft) {
self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration;
} else {
self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration;
}
}
if (scrollOutsideY !== 0) {
if (scrollOutsideY * self.__decelerationVelocityY <= self.__minDecelerationScrollTop) {
self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration;
} else {
self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration;
}
}
}
}
});
})(ionic);