UNPKG

easyscroller

Version:

Mobile-friendly accelerated panning and zooming for DOM and Canvas based on Zynga Scroller.

1,013 lines (1,012 loc) 46.4 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; Object.defineProperty(exports, "__esModule", { value: true }); var Animate_1 = require("./Animate"); /** * A pure logic 'component' for 'virtual' scrolling/zooming. */ var Scroller = /** @class */ (function () { function Scroller(callback, options) { /* --------------------------------------------------------------------------- INTERNAL FIELDS :: STATUS --------------------------------------------------------------------------- */ /** {Boolean} Whether only a single finger is used in touch handling */ this.__isSingleTouch = false; /** {Boolean} Whether a touch event sequence is in progress */ this.__isTracking = false; /** {Boolean} Whether a deceleration animation went to completion. */ this.__didDecelerationComplete = false; /** * {Boolean} Whether a gesture zoom/rotate event is in progress. Activates when * a gesturestart event happens. This has higher priority than dragging. */ this.__isGesturing = false; /** * {Boolean} 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. */ this.__isDragging = false; /** * {Boolean} Not touching and dragging anymore, and smoothly animating the * touch sequence using deceleration. */ this.__isDecelerating = false; /** * {Boolean} Smoothly animating the currently configured change */ this.__isAnimating = false; this.__interruptedAnimation = false; this.__initialTouchLeft = null; this.__initialTouchTop = null; this.__initialTouches = null; this.__lastScale = 1; this.__enableScrollX = true; this.__enableScrollY = true; /* --------------------------------------------------------------------------- INTERNAL FIELDS :: DIMENSIONS --------------------------------------------------------------------------- */ /** {Integer} Available outer left position (from document perspective) */ this.__clientLeft = 0; /** {Integer} Available outer top position (from document perspective) */ this.__clientTop = 0; /** {Integer} Available outer width */ this.__clientWidth = 0; /** {Integer} Available outer height */ this.__clientHeight = 0; /** {Integer} Outer width of content */ this.__contentWidth = 0; /** {Integer} Outer height of content */ this.__contentHeight = 0; /** {Integer} Snapping width for content */ this.__snapWidth = 100; /** {Integer} Snapping height for content */ this.__snapHeight = 100; /** {Integer} Height to assign to refresh area */ this.__refreshHeight = null; /** {Boolean} Whether the refresh process is enabled when the event is released now */ this.__refreshActive = false; /** {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */ this.__refreshActivate = null; /** {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */ this.__refreshDeactivate = null; /** {Function} Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */ this.__refreshStart = null; /** {Number} Zoom level */ this.__zoomLevel = 1; this.__zoomLevelStart = this.__zoomLevel; /** {Number} Scroll position on x-axis */ this.__scrollLeft = 0; /** {Number} Scroll position on y-axis */ this.__scrollTop = 0; /** {Integer} Maximum allowed scroll position on x-axis */ this.__maxScrollLeft = 0; /** {Integer} Maximum allowed scroll position on y-axis */ this.__maxScrollTop = 0; /* {Number} Scheduled left position (final position when animating) */ this.__scheduledLeft = 0; /* {Number} Scheduled top position (final position when animating) */ this.__scheduledTop = 0; /* {Number} Scheduled zoom level (final scale when animating) */ this.__scheduledZoom = 0; /* --------------------------------------------------------------------------- INTERNAL FIELDS :: LAST POSITIONS --------------------------------------------------------------------------- */ /** {Number} Left position of finger at start */ this.__lastTouchLeft = null; /** {Number} Top position of finger at start */ this.__lastTouchTop = null; /** {Date} Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */ this.__lastTouchMove = null; /** {Array} List of positions, uses three indexes for each state: left, top, timestamp */ this.__positions = null; /* --------------------------------------------------------------------------- INTERNAL FIELDS :: DECELERATION SUPPORT --------------------------------------------------------------------------- */ /** {Integer} Minimum left scroll position during deceleration */ this.__minDecelerationScrollLeft = null; /** {Integer} Minimum top scroll position during deceleration */ this.__minDecelerationScrollTop = null; /** {Integer} Maximum left scroll position during deceleration */ this.__maxDecelerationScrollLeft = null; /** {Integer} Maximum top scroll position during deceleration */ this.__maxDecelerationScrollTop = null; /** {Number} Current factor to modify horizontal scroll position with on every step */ this.__decelerationVelocityX = null; /** {Number} Current factor to modify vertical scroll position with on every step */ this.__decelerationVelocityY = null; this.__callback = function (scrollLeft, scrollTop, zoomLevel) { }; this.__zoomComplete = function () { }; this.options = { scrollingX: true, scrollingY: true, animating: true, animationDuration: 250, bouncing: true, locking: true, paging: false, snapping: false, zooming: false, zoomLevel: 1, minZoom: 0.5, maxZoom: 3, speedMultiplier: 1, scrollingComplete: function () { }, penetrationDeceleration: 0.03, penetrationAcceleration: 0.08, }; this.__callback = callback; this.options = __assign({}, this.options, options); if (options.zoomLevel && options.zoomLevel >= this.options.minZoom && options.zoomLevel <= this.options.maxZoom) { this.__zoomLevel = options.zoomLevel; this.__zoomLevelStart = this.__zoomLevel; } } // 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) **/ Scroller.prototype.easeOutCubic = function (pos) { return Math.pow(pos - 1, 3) + 1; }; /** * @param pos {Number} position between 0 (start of effect) and 1 (end of effect) **/ Scroller.prototype.easeInOutCubic = function (pos) { if ((pos /= 0.5) < 1) { return 0.5 * Math.pow(pos, 3); } return 0.5 * (Math.pow(pos - 2, 3) + 2); }; /* --------------------------------------------------------------------------- PUBLIC API --------------------------------------------------------------------------- */ /** * 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 ? null} Inner width of outer element * @param clientHeight {Integer ? null} Inner height of outer element * @param contentWidth {Integer ? null} Outer width of inner element * @param contentHeight {Integer ? null} Outer height of inner element */ Scroller.prototype.setDimensions = function (clientWidth, clientHeight, contentWidth, contentHeight) { // Only update values which are defined if (clientWidth === +clientWidth) { this.__clientWidth = clientWidth; } if (clientHeight === +clientHeight) { this.__clientHeight = clientHeight; } if (contentWidth === +contentWidth) { this.__contentWidth = contentWidth; } if (contentHeight === +contentHeight) { this.__contentHeight = contentHeight; } // Refresh maximums this.__computeScrollMax(); // Refresh scroll position this.scrollTo(this.__scrollLeft, this.__scrollTop, true); }; /** * Sets the client coordinates in relation to the document. * * @param left {Integer ? 0} Left position of outer element * @param top {Integer ? 0} Top position of outer element */ Scroller.prototype.setPosition = function (left, top) { this.__clientLeft = left || 0; this.__clientTop = top || 0; }; /** * Configures the snapping (when snapping is active) * * @param width {Integer} Snapping width * @param height {Integer} Snapping height */ Scroller.prototype.setSnapSize = function (width, height) { this.__snapWidth = width; this.__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. */ Scroller.prototype.activatePullToRefresh = function (height, activateCallback, deactivateCallback, startCallback) { this.__refreshHeight = height; this.__refreshActivate = activateCallback; this.__refreshDeactivate = deactivateCallback; this.__refreshStart = startCallback; }; /** * Starts pull-to-refresh manually. */ Scroller.prototype.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. */ Scroller.prototype.finishPullToRefresh = function () { this.__refreshActive = false; if (this.__refreshDeactivate) { this.__refreshDeactivate(); } this.scrollTo(this.__scrollLeft, this.__scrollTop, true); }; /** * Returns the scroll position and zooming values * * @return {Map} `left` and `top` scroll position and `zoom` level */ Scroller.prototype.getValues = function () { return { left: this.__scrollLeft, top: this.__scrollTop, zoom: this.__zoomLevel, }; }; /** * Returns the maximum scroll values * * @return {Map} `left` and `top` maximum scroll values */ Scroller.prototype.getScrollMax = function () { return { left: this.__maxScrollLeft, top: this.__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 ? false} Whether to use animation * @param originLeft {Number ? null} Zoom in at given left coordinate * @param originTop {Number ? null} Zoom in at given top coordinate * @param callback {Function ? null} A callback that gets fired when the zoom is complete. */ Scroller.prototype.zoomTo = function (level, animate, originLeft, originTop, callback) { if (!this.options.zooming) { throw new Error('Zooming is not enabled!'); } // Add callback if exists if (callback) { this.__zoomComplete = callback; } // Stop deceleration if (this.__isDecelerating) { Animate_1.core.effect.Animate.stop(this.__isDecelerating); this.__isDecelerating = false; } var oldLevel = this.__zoomLevel; // Normalize input origin to center of viewport if not defined if (originLeft == null) { originLeft = this.__clientWidth / 2; } if (originTop == null) { originTop = this.__clientHeight / 2; } // Limit level according to configuration level = Math.max(Math.min(level, this.options.maxZoom), this.options.minZoom); // Recompute maximum values while temporary tweaking maximum scroll ranges this.__computeScrollMax(level); // Recompute left and top coordinates based on new zoom level var left = ((originLeft + this.__scrollLeft) * level) / oldLevel - originLeft; var top = ((originTop + this.__scrollTop) * level) / oldLevel - originTop; // Limit x-axis if (left > this.__maxScrollLeft) { left = this.__maxScrollLeft; } else if (left < 0) { left = 0; } // Limit y-axis if (top > this.__maxScrollTop) { top = this.__maxScrollTop; } else if (top < 0) { top = 0; } // Push values out this.__publish(left, top, level, animate); }; /** * Zooms the content by the given factor. * * @param factor {Number} Zoom by given factor * @param animate {Boolean ? false} Whether to use animation * @param originLeft {Number ? 0} Zoom in at given left coordinate * @param originTop {Number ? 0} Zoom in at given top coordinate * @param callback {Function ? null} A callback that gets fired when the zoom is complete. */ Scroller.prototype.zoomBy = function (factor, animate, originLeft, originTop, callback) { this.zoomTo(this.__zoomLevel * factor, animate, originLeft, originTop, callback); }; /** * Scrolls to the given position. Respect limitations and snapping automatically. * * @param left {Number?null} Horizontal scroll position, keeps current if value is <code>null</code> * @param top {Number?null} Vertical scroll position, keeps current if value is <code>null</code> * @param animate {Boolean?false} Whether the scrolling should happen using an animation * @param zoom {Number?null} Zoom level to go to */ Scroller.prototype.scrollTo = function (left, top, animate, zoom) { // Stop deceleration if (this.__isDecelerating) { Animate_1.core.effect.Animate.stop(this.__isDecelerating); this.__isDecelerating = false; } // Correct coordinates based on new zoom level if (zoom != null && zoom !== this.__zoomLevel) { if (!this.options.zooming) { throw new Error('Zooming is not enabled!'); } left *= zoom; top *= zoom; // Recompute maximum values while temporary tweaking maximum scroll ranges this.__computeScrollMax(zoom); } else { // Keep zoom when not defined zoom = this.__zoomLevel; } if (!this.options.scrollingX) { left = this.__scrollLeft; } else { if (this.options.paging) { left = Math.round(left / this.__clientWidth) * this.__clientWidth; } else if (this.options.snapping) { left = Math.round(left / this.__snapWidth) * this.__snapWidth; } } if (!this.options.scrollingY) { top = this.__scrollTop; } else { if (this.options.paging) { top = Math.round(top / this.__clientHeight) * this.__clientHeight; } else if (this.options.snapping) { top = Math.round(top / this.__snapHeight) * this.__snapHeight; } } // Limit for allowed ranges left = Math.max(Math.min(this.__maxScrollLeft, left), 0); top = Math.max(Math.min(this.__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 === this.__scrollLeft && top === this.__scrollTop) { animate = false; } // Publish new values if (!this.__isTracking) { this.__publish(left, top, zoom, animate); } }; /** * Scroll by the given offset * * @param left {Number ? 0} Scroll x-axis by given offset * @param top {Number ? 0} Scroll x-axis by given offset * @param animate {Boolean ? false} Whether to animate the given change */ Scroller.prototype.scrollBy = function (left, top, animate) { var startLeft = this.__isAnimating ? this.__scheduledLeft : this.__scrollLeft; var startTop = this.__isAnimating ? this.__scheduledTop : this.__scrollTop; this.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate); }; /* --------------------------------------------------------------------------- EVENT CALLBACKS --------------------------------------------------------------------------- */ /** * Mouse wheel handler for zooming support */ Scroller.prototype.doMouseZoom = function (wheelDelta, timeStamp, pageX, pageY) { var change = wheelDelta > 0 ? 0.97 : 1.03; return this.zoomTo(this.__zoomLevel * change, false, pageX - this.__clientLeft, pageY - this.__clientTop); }; /** * Touch start handler for scrolling support */ Scroller.prototype.doTouchStart = function (touches, timeStamp) { // 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); } // Reset interruptedAnimation flag this.__interruptedAnimation = true; // Stop deceleration if (this.__isDecelerating) { Animate_1.core.effect.Animate.stop(this.__isDecelerating); this.__isDecelerating = false; this.__interruptedAnimation = true; } // Stop animation if (this.__isAnimating) { Animate_1.core.effect.Animate.stop(this.__isAnimating); this.__isAnimating = false; this.__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 this.__initialTouchLeft = currentTouchLeft; this.__initialTouchTop = currentTouchTop; // Store initial touchList for scale calculation this.__initialTouches = touches; // Store current zoom level this.__zoomLevelStart = this.__zoomLevel; // Store initial touch positions this.__lastTouchLeft = currentTouchLeft; this.__lastTouchTop = currentTouchTop; // Store initial move time stamp this.__lastTouchMove = timeStamp; // Reset initial scale this.__lastScale = 1; // Reset locking flags this.__enableScrollX = !isSingleTouch && this.options.scrollingX; this.__enableScrollY = !isSingleTouch && this.options.scrollingY; // Reset tracking flag this.__isTracking = true; // Reset deceleration complete flag this.__didDecelerationComplete = false; // Dragging starts directly with two fingers, otherwise lazy with an offset this.__isDragging = !isSingleTouch; // Some features are disabled in multi touch scenarios this.__isSingleTouch = isSingleTouch; // Clearing data structure this.__positions = []; }; /** * Touch move handler for scrolling support */ Scroller.prototype.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); } // Ignore event when tracking is not enabled (event might be outside of element) if (!this.__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; // Calculate scale when not present and only when touches are used if (!scale && this.options.zooming) { scale = this.__getScale(this.__initialTouches, touches); } } else { currentTouchLeft = touches[0].pageX; currentTouchTop = touches[0].pageY; } var positions = this.__positions; // Are we already is dragging mode? if (this.__isDragging) { // Compute move distance var moveX = currentTouchLeft - this.__lastTouchLeft; var moveY = currentTouchTop - this.__lastTouchTop; // Read previous scroll position and zooming var scrollLeft = this.__scrollLeft; var scrollTop = this.__scrollTop; var level = this.__zoomLevel; // Work with scaling if (scale != null && this.options.zooming) { var oldLevel = level; // Recompute level based on previous scale and new scale level = (level / this.__lastScale) * scale; // Limit level according to configuration level = Math.max(Math.min(level, this.options.maxZoom), this.options.minZoom); // Only do further compution when change happened if (oldLevel !== level) { // Compute relative event position to container var currentTouchLeftRel = currentTouchLeft - this.__clientLeft; var currentTouchTopRel = currentTouchTop - this.__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 this.__computeScrollMax(level); } } if (this.__enableScrollX) { scrollLeft -= moveX * this.options.speedMultiplier; var maxScrollLeft = this.__maxScrollLeft; if (scrollLeft > maxScrollLeft || scrollLeft < 0) { // Slow down on the edges if (this.options.bouncing) { scrollLeft += (moveX / 2) * this.options.speedMultiplier; } else if (scrollLeft > maxScrollLeft) { scrollLeft = maxScrollLeft; } else { scrollLeft = 0; } } } // Compute new vertical scroll position if (this.__enableScrollY) { scrollTop -= moveY * this.options.speedMultiplier; var maxScrollTop = this.__maxScrollTop; if (scrollTop > maxScrollTop || scrollTop < 0) { // Slow down on the edges if (this.options.bouncing) { scrollTop += (moveY / 2) * this.options.speedMultiplier; // Support pull-to-refresh (only when only y is scrollable) if (!this.__enableScrollX && this.__refreshHeight != null) { if (!this.__refreshActive && scrollTop <= -this.__refreshHeight) { this.__refreshActive = true; if (this.__refreshActivate) { this.__refreshActivate(); } } else if (this.__refreshActive && scrollTop > -this.__refreshHeight) { this.__refreshActive = false; if (this.__refreshDeactivate) { this.__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 this.__publish(scrollLeft, scrollTop, level); // Otherwise figure out whether we are switching into dragging mode now. } else { var minimumTrackingForScroll = this.options.locking ? 3 : 0; var minimumTrackingForDrag = 5; var distanceX = Math.abs(currentTouchLeft - this.__initialTouchLeft); var distanceY = Math.abs(currentTouchTop - this.__initialTouchTop); this.__enableScrollX = this.options.scrollingX && distanceX >= minimumTrackingForScroll; this.__enableScrollY = this.options.scrollingY && distanceY >= minimumTrackingForScroll; positions.push(this.__scrollLeft, this.__scrollTop, timeStamp); this.__isDragging = (this.__enableScrollX || this.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag); if (this.__isDragging) { this.__interruptedAnimation = false; } } // Update last touch positions and time stamp for next event this.__lastTouchLeft = currentTouchLeft; this.__lastTouchTop = currentTouchTop; this.__lastTouchMove = timeStamp; this.__lastScale = scale; }; /** * Touch end handler for scrolling support */ Scroller.prototype.doTouchEnd = function (timeStamp) { if (timeStamp instanceof Date) { timeStamp = timeStamp.valueOf(); } if (typeof timeStamp !== 'number') { throw new Error('Invalid timestamp value: ' + timeStamp); } // 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 itthis. if (!this.__isTracking) { return; } // Not touching anymore (when two finger hit the screen there are two touch end events) this.__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 (this.__isDragging) { // Reset dragging flag this.__isDragging = false; // Start deceleration // Verify that the last move detected was in some relevant time frame if (this.__isSingleTouch && this.options.animating && timeStamp - this.__lastTouchMove <= 100) { // Then figure out what the scroll position was about 100ms ago var positions = this.__positions; var endPos = positions.length - 1; var startPos = endPos; // Move pointer to position measured 100ms ago for (var i = endPos; i > 0 && positions[i] > this.__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 = this.__scrollLeft - positions[startPos - 2]; var movedTop = this.__scrollTop - positions[startPos - 1]; // Based on 50ms compute the movement to apply for each render step this.__decelerationVelocityX = (movedLeft / timeOffset) * (1000 / 60); this.__decelerationVelocityY = (movedTop / timeOffset) * (1000 / 60); // How much velocity is required to start the deceleration var minVelocityToStartDeceleration = this.options.paging || this.options.snapping ? 4 : 1; // Verify that we have enough velocity to start deceleration if (Math.abs(this.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(this.__decelerationVelocityY) > minVelocityToStartDeceleration) { // Deactivate pull-to-refresh when decelerating if (!this.__refreshActive) { this.__startDeceleration(timeStamp); } } else { this.options.scrollingComplete(); } } else { this.options.scrollingComplete(); } } else if (timeStamp - this.__lastTouchMove > 100) { this.options.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 (!this.__isDecelerating) { if (this.__refreshActive && this.__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 this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true); if (this.__refreshStart) { this.__refreshStart(); } } else { if (this.__interruptedAnimation || this.__isDragging) { this.options.scrollingComplete(); } this.scrollTo(this.__scrollLeft, this.__scrollTop, true, this.__zoomLevel); // Directly signalize deactivation (nothing todo on refresh?) if (this.__refreshActive) { this.__refreshActive = false; if (this.__refreshDeactivate) { this.__refreshDeactivate(); } } } } // Fully cleanup list this.__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?false} Whether animation should be used to move to the new coordinates */ Scroller.prototype.__publish = function (left, top, zoom, animate) { var _this = this; // Remember whether we had an animation, then we try to continue based on the current "drive" of the animation var wasAnimating = this.__isAnimating; if (wasAnimating) { Animate_1.core.effect.Animate.stop(wasAnimating); this.__isAnimating = false; } if (animate && this.options.animating) { // Keep scheduled positions for scrollBy/zoomBy functionality this.__scheduledLeft = left; this.__scheduledTop = top; this.__scheduledZoom = zoom; var oldLeft = this.__scrollLeft; var oldTop = this.__scrollTop; var oldZoom = this.__zoomLevel; var diffLeft = left - oldLeft; var diffTop = top - oldTop; var diffZoom = zoom - oldZoom; var step = function (percent, now, render) { if (render) { _this.__scrollLeft = oldLeft + diffLeft * percent; _this.__scrollTop = oldTop + diffTop * percent; _this.__zoomLevel = oldZoom + diffZoom * percent; // Push values out if (_this.__callback) { _this.__callback(_this.__scrollLeft, _this.__scrollTop, _this.__zoomLevel); } } }; var verify = function (id) { return _this.__isAnimating === id; }; var completed = function (renderedFramesPerSecond, animationId, wasFinished) { if (animationId === _this.__isAnimating) { _this.__isAnimating = false; } if (_this.__didDecelerationComplete || wasFinished) { _this.options.scrollingComplete(); } if (_this.options.zooming) { _this.__computeScrollMax(); if (_this.__zoomComplete) { _this.__zoomComplete(); _this.__zoomComplete = null; } } }; // When continuing based on previous animation we choose an ease-out animation instead of ease-in-out this.__isAnimating = Animate_1.core.effect.Animate.start(step, verify, completed, this.options.animationDuration, wasAnimating ? this.easeOutCubic : this.easeInOutCubic); } else { this.__scheduledLeft = this.__scrollLeft = left; this.__scheduledTop = this.__scrollTop = top; this.__scheduledZoom = this.__zoomLevel = zoom; // Push values out if (this.__callback) { this.__callback(left, top, zoom); } // Fix max scroll ranges if (this.options.zooming) { this.__computeScrollMax(); if (this.__zoomComplete) { this.__zoomComplete(); this.__zoomComplete = null; } } } }; /** * Recomputes scroll minimum values based on client dimensions and content dimensions. */ Scroller.prototype.__computeScrollMax = function (zoomLevel) { if (zoomLevel === void 0) { zoomLevel = this.__zoomLevel; } this.__maxScrollLeft = Math.max(this.__contentWidth * zoomLevel - this.__clientWidth, 0); this.__maxScrollTop = Math.max(this.__contentHeight * zoomLevel - this.__clientHeight, 0); }; /* --------------------------------------------------------------------------- ANIMATION (DECELERATION) SUPPORT --------------------------------------------------------------------------- */ /** * Called when a touch sequence end and the speed of the finger was high enough * to switch into deceleration mode. */ Scroller.prototype.__startDeceleration = function (timeStamp) { var _this = this; if (this.options.paging) { var scrollLeft = Math.max(Math.min(this.__scrollLeft, this.__maxScrollLeft), 0); var scrollTop = Math.max(Math.min(this.__scrollTop, this.__maxScrollTop), 0); var clientWidth = this.__clientWidth; var clientHeight = this.__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. this.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth; this.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight; this.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth; this.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight; } else { this.__minDecelerationScrollLeft = 0; this.__minDecelerationScrollTop = 0; this.__maxDecelerationScrollLeft = this.__maxScrollLeft; this.__maxDecelerationScrollTop = this.__maxScrollTop; } // Wrap class method var step = function (percent, now, render) { _this.__stepThroughDeceleration(render); }; // How much velocity is required to keep the deceleration running var minVelocityToKeepDecelerating = this.options.snapping ? 4 : 0.001; // 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(_this.__decelerationVelocityX) >= minVelocityToKeepDecelerating || Math.abs(_this.__decelerationVelocityY) >= minVelocityToKeepDecelerating; if (!shouldContinue) { _this.__didDecelerationComplete = true; } return shouldContinue; }; var completed = function (renderedFramesPerSecond, animationId, wasFinished) { _this.__isDecelerating = false; if (_this.__didDecelerationComplete) { _this.options.scrollingComplete(); } // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions _this.scrollTo(_this.__scrollLeft, _this.__scrollTop, _this.options.snapping); }; // Start animation and switch on flag this.__isDecelerating = Animate_1.core.effect.Animate.start(step, verify, completed); }; /** * Called on every step of the animation * * @param inMemory {Boolean?false} Whether to not render the current step, but keep it in memory only. Used internally only! */ Scroller.prototype.__stepThroughDeceleration = function (render) { // // COMPUTE NEXT SCROLL POSITION // // Add deceleration to scroll position var scrollLeft = this.__scrollLeft + this.__decelerationVelocityX; var scrollTop = this.__scrollTop + this.__decelerationVelocityY; // // HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE // if (!this.options.bouncing) { var scrollLeftFixed = Math.max(Math.min(this.__maxDecelerationScrollLeft, scrollLeft), this.__minDecelerationScrollLeft); if (scrollLeftFixed !== scrollLeft) { scrollLeft = scrollLeftFixed; this.__decelerationVelocityX = 0; } var scrollTopFixed = Math.max(Math.min(this.__maxDecelerationScrollTop, scrollTop), this.__minDecelerationScrollTop); if (scrollTopFixed !== scrollTop) { scrollTop = scrollTopFixed; this.__decelerationVelocityY = 0; } } // // UPDATE SCROLL POSITION // if (render) { this.__publish(scrollLeft, scrollTop, this.__zoomLevel); } else { this.__scrollLeft = scrollLeft; this.__scrollTop = scrollTop; } // // SLOW DOWN // // Slow down velocity on every iteration if (!this.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; this.__decelerationVelocityX *= frictionFactor; this.__decelerationVelocityY *= frictionFactor; } // // BOUNCING SUPPORT // if (this.options.bouncing) { var scrollOutsideX = 0; var scrollOutsideY = 0; // This configures the amount of change applied to deceleration/acceleration when reaching boundaries var penetrationDeceleration = this.options.penetrationDeceleration; var penetrationAcceleration = this.options.penetrationAcceleration; // Check limits if (scrollLeft < this.__minDecelerationScrollLeft) { scrollOutsideX = this.__minDecelerationScrollLeft - scrollLeft; } else if (scrollLeft > this.__maxDecelerationScrollLeft) { scrollOutsideX = this.__maxDecelerationScrollLeft - scrollLeft; } if (scrollTop < this.__minDecelerationScrollTop) { scrollOutsideY = this.__minDecelerationScrollTop - scrollTop; } else if (scrollTop > this.__maxDecelerationScrollTop) { scrollOutsideY = this.__maxDecelerationScrollTop - scrollTop; } // Slow down until slow enough, then flip back to snap position if (scrollOutsideX !== 0) { if (scrollOutsideX * this.__decelerationVelocityX <= 0) { this.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration; } else { this.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration; } } if (scrollOutsideY !== 0) { if (scrollOutsideY * this.__decelerationVelocityY <= 0) { this.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration; } else { this.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration; } } } }; /** * calculate the distance between two touches * @param {Touch} touch1 * @param {Touch} touch2 * @returns {Number} distance */ Scroller.prototype.__getDistance = function (touch1, touch2) { var x = touch2.pageX - touch1.pageX, y = touch2.pageY - touch1.pageY; return Math.sqrt(x * x + y * y); }; /** * calculate the scale factor between two touchLists (fingers) * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out * @param {Array} start * @param {Array} end * @returns {Number} scale */ Scroller.prototype.__getScale = function (start, end) { // need two fingers... if (start.length >= 2 && end.length >= 2) { return this.__getDistance(end[0], end[1]) / this.__getDistance(start[0], start[1]); } return 1; }; return Scroller; }()); exports.Scroller = Scroller;