UNPKG

39.1 kBJavaScriptView Raw
1"use strict";
2
3/*
4 * Scroller
5 * http://github.com/zynga/scroller
6 *
7 * Copyright 2011, Zynga Inc.
8 * Licensed under the MIT License.
9 * https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
10 *
11 * Based on the work of: Unify Project (unify-project.org)
12 * http://unify-project.org
13 * Copyright 2011, Deutsche Telekom AG
14 * License: MIT + Apache (V2)
15 */
16
17require('./Animate');
18
19var Scroller;
20
21(function () {
22 var NOOP = function NOOP() {};
23
24 /**
25 * A pure logic 'component' for 'virtual' scrolling/zooming.
26 */
27 Scroller = function Scroller(callback, options) {
28
29 this.__callback = callback;
30
31 this.options = {
32
33 /** Enable scrolling on x-axis */
34 scrollingX: true,
35
36 /** Enable scrolling on y-axis */
37 scrollingY: true,
38
39 /** Enable animations for deceleration, snap back, zooming and scrolling */
40 animating: true,
41
42 /** duration for animations triggered by scrollTo/zoomTo */
43 animationDuration: 250,
44
45 /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */
46 bouncing: true,
47
48 /** Enable locking to the main axis if user moves only slightly on one of them at start */
49 locking: true,
50
51 /** Enable pagination mode (switching between full page content panes) */
52 paging: false,
53
54 /** Enable snapping of content to a configured pixel grid */
55 snapping: false,
56
57 /** Enable zooming of content via API, fingers and mouse wheel */
58 zooming: false,
59
60 /** Minimum zoom level */
61 minZoom: 0.5,
62
63 /** Maximum zoom level */
64 maxZoom: 3,
65
66 /** Multiply or decrease scrolling speed **/
67 speedMultiplier: 1,
68
69 /** Callback that is fired on the later of touch end or deceleration end,
70 provided that another scrolling action has not begun. Used to know
71 when to fade out a scrollbar. */
72 scrollingComplete: NOOP,
73
74 /** This configures the amount of change applied to deceleration when reaching boundaries **/
75 penetrationDeceleration: 0.03,
76
77 /** This configures the amount of change applied to acceleration when reaching boundaries **/
78 penetrationAcceleration: 0.08
79
80 };
81
82 for (var key in options) {
83 this.options[key] = options[key];
84 }
85 };
86
87 // Easing Equations (c) 2003 Robert Penner, all rights reserved.
88 // Open source under the BSD License.
89
90 /**
91 * @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
92 **/
93 var easeOutCubic = function easeOutCubic(pos) {
94 return Math.pow(pos - 1, 3) + 1;
95 };
96
97 /**
98 * @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
99 **/
100 var easeInOutCubic = function easeInOutCubic(pos) {
101 if ((pos /= 0.5) < 1) {
102 return 0.5 * Math.pow(pos, 3);
103 }
104
105 return 0.5 * (Math.pow(pos - 2, 3) + 2);
106 };
107
108 var members = {
109
110 /*
111 ---------------------------------------------------------------------------
112 INTERNAL FIELDS :: STATUS
113 ---------------------------------------------------------------------------
114 */
115
116 /** {Boolean} Whether only a single finger is used in touch handling */
117 __isSingleTouch: false,
118
119 /** {Boolean} Whether a touch event sequence is in progress */
120 __isTracking: false,
121
122 /** {Boolean} Whether a deceleration animation went to completion. */
123 __didDecelerationComplete: false,
124
125 /**
126 * {Boolean} Whether a gesture zoom/rotate event is in progress. Activates when
127 * a gesturestart event happens. This has higher priority than dragging.
128 */
129 __isGesturing: false,
130
131 /**
132 * {Boolean} Whether the user has moved by such a distance that we have enabled
133 * dragging mode. Hint: It's only enabled after some pixels of movement to
134 * not interrupt with clicks etc.
135 */
136 __isDragging: false,
137
138 /**
139 * {Boolean} Not touching and dragging anymore, and smoothly animating the
140 * touch sequence using deceleration.
141 */
142 __isDecelerating: false,
143
144 /**
145 * {Boolean} Smoothly animating the currently configured change
146 */
147 __isAnimating: false,
148
149 /*
150 ---------------------------------------------------------------------------
151 INTERNAL FIELDS :: DIMENSIONS
152 ---------------------------------------------------------------------------
153 */
154
155 /** {Integer} Available outer left position (from document perspective) */
156 __clientLeft: 0,
157
158 /** {Integer} Available outer top position (from document perspective) */
159 __clientTop: 0,
160
161 /** {Integer} Available outer width */
162 __clientWidth: 0,
163
164 /** {Integer} Available outer height */
165 __clientHeight: 0,
166
167 /** {Integer} Outer width of content */
168 __contentWidth: 0,
169
170 /** {Integer} Outer height of content */
171 __contentHeight: 0,
172
173 /** {Integer} Snapping width for content */
174 __snapWidth: 100,
175
176 /** {Integer} Snapping height for content */
177 __snapHeight: 100,
178
179 /** {Integer} Height to assign to refresh area */
180 __refreshHeight: null,
181
182 /** {Boolean} Whether the refresh process is enabled when the event is released now */
183 __refreshActive: false,
184
185 /** {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */
186 __refreshActivate: null,
187
188 /** {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */
189 __refreshDeactivate: null,
190
191 /** {Function} Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */
192 __refreshStart: null,
193
194 /** {Number} Zoom level */
195 __zoomLevel: 1,
196
197 /** {Number} Scroll position on x-axis */
198 __scrollLeft: 0,
199
200 /** {Number} Scroll position on y-axis */
201 __scrollTop: 0,
202
203 /** {Integer} Maximum allowed scroll position on x-axis */
204 __maxScrollLeft: 0,
205
206 /** {Integer} Maximum allowed scroll position on y-axis */
207 __maxScrollTop: 0,
208
209 /* {Number} Scheduled left position (final position when animating) */
210 __scheduledLeft: 0,
211
212 /* {Number} Scheduled top position (final position when animating) */
213 __scheduledTop: 0,
214
215 /* {Number} Scheduled zoom level (final scale when animating) */
216 __scheduledZoom: 0,
217
218 /*
219 ---------------------------------------------------------------------------
220 INTERNAL FIELDS :: LAST POSITIONS
221 ---------------------------------------------------------------------------
222 */
223
224 /** {Number} Left position of finger at start */
225 __lastTouchLeft: null,
226
227 /** {Number} Top position of finger at start */
228 __lastTouchTop: null,
229
230 /** {Date} Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */
231 __lastTouchMove: null,
232
233 /** {Array} List of positions, uses three indexes for each state: left, top, timestamp */
234 __positions: null,
235
236 /*
237 ---------------------------------------------------------------------------
238 INTERNAL FIELDS :: DECELERATION SUPPORT
239 ---------------------------------------------------------------------------
240 */
241
242 /** {Integer} Minimum left scroll position during deceleration */
243 __minDecelerationScrollLeft: null,
244
245 /** {Integer} Minimum top scroll position during deceleration */
246 __minDecelerationScrollTop: null,
247
248 /** {Integer} Maximum left scroll position during deceleration */
249 __maxDecelerationScrollLeft: null,
250
251 /** {Integer} Maximum top scroll position during deceleration */
252 __maxDecelerationScrollTop: null,
253
254 /** {Number} Current factor to modify horizontal scroll position with on every step */
255 __decelerationVelocityX: null,
256
257 /** {Number} Current factor to modify vertical scroll position with on every step */
258 __decelerationVelocityY: null,
259
260 /*
261 ---------------------------------------------------------------------------
262 PUBLIC API
263 ---------------------------------------------------------------------------
264 */
265
266 /**
267 * Configures the dimensions of the client (outer) and content (inner) elements.
268 * Requires the available space for the outer element and the outer size of the inner element.
269 * All values which are falsy (null or zero etc.) are ignored and the old value is kept.
270 *
271 * @param clientWidth {Integer ? null} Inner width of outer element
272 * @param clientHeight {Integer ? null} Inner height of outer element
273 * @param contentWidth {Integer ? null} Outer width of inner element
274 * @param contentHeight {Integer ? null} Outer height of inner element
275 */
276 setDimensions: function setDimensions(clientWidth, clientHeight, contentWidth, contentHeight) {
277
278 var self = this;
279
280 // Only update values which are defined
281 if (clientWidth === +clientWidth) {
282 self.__clientWidth = clientWidth;
283 }
284
285 if (clientHeight === +clientHeight) {
286 self.__clientHeight = clientHeight;
287 }
288
289 if (contentWidth === +contentWidth) {
290 self.__contentWidth = contentWidth;
291 }
292
293 if (contentHeight === +contentHeight) {
294 self.__contentHeight = contentHeight;
295 }
296
297 // Refresh maximums
298 self.__computeScrollMax();
299
300 // Refresh scroll position
301 self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
302 },
303
304 /**
305 * Sets the client coordinates in relation to the document.
306 *
307 * @param left {Integer ? 0} Left position of outer element
308 * @param top {Integer ? 0} Top position of outer element
309 */
310 setPosition: function setPosition(left, top) {
311
312 var self = this;
313
314 self.__clientLeft = left || 0;
315 self.__clientTop = top || 0;
316 },
317
318 /**
319 * Configures the snapping (when snapping is active)
320 *
321 * @param width {Integer} Snapping width
322 * @param height {Integer} Snapping height
323 */
324 setSnapSize: function setSnapSize(width, height) {
325
326 var self = this;
327
328 self.__snapWidth = width;
329 self.__snapHeight = height;
330 },
331
332 /**
333 * Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever
334 * the user event is released during visibility of this zone. This was introduced by some apps on iOS like
335 * the official Twitter client.
336 *
337 * @param height {Integer} Height of pull-to-refresh zone on top of rendered list
338 * @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release.
339 * @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled.
340 * @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh.
341 */
342 activatePullToRefresh: function activatePullToRefresh(height, activateCallback, deactivateCallback, startCallback) {
343
344 var self = this;
345
346 self.__refreshHeight = height;
347 self.__refreshActivate = activateCallback;
348 self.__refreshDeactivate = deactivateCallback;
349 self.__refreshStart = startCallback;
350 },
351
352 /**
353 * Starts pull-to-refresh manually.
354 */
355 triggerPullToRefresh: function triggerPullToRefresh() {
356 // Use publish instead of scrollTo to allow scrolling to out of boundary position
357 // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
358 this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true);
359
360 if (this.__refreshStart) {
361 this.__refreshStart();
362 }
363 },
364
365 /**
366 * Signalizes that pull-to-refresh is finished.
367 */
368 finishPullToRefresh: function finishPullToRefresh() {
369
370 var self = this;
371
372 self.__refreshActive = false;
373 if (self.__refreshDeactivate) {
374 self.__refreshDeactivate();
375 }
376
377 self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
378 },
379
380 /**
381 * Returns the scroll position and zooming values
382 *
383 * @return {Map} `left` and `top` scroll position and `zoom` level
384 */
385 getValues: function getValues() {
386
387 var self = this;
388
389 return {
390 left: self.__scrollLeft,
391 top: self.__scrollTop,
392 zoom: self.__zoomLevel
393 };
394 },
395
396 /**
397 * Returns the maximum scroll values
398 *
399 * @return {Map} `left` and `top` maximum scroll values
400 */
401 getScrollMax: function getScrollMax() {
402
403 var self = this;
404
405 return {
406 left: self.__maxScrollLeft,
407 top: self.__maxScrollTop
408 };
409 },
410
411 /**
412 * Zooms to the given level. Supports optional animation. Zooms
413 * the center when no coordinates are given.
414 *
415 * @param level {Number} Level to zoom to
416 * @param animate {Boolean ? false} Whether to use animation
417 * @param originLeft {Number ? null} Zoom in at given left coordinate
418 * @param originTop {Number ? null} Zoom in at given top coordinate
419 * @param callback {Function ? null} A callback that gets fired when the zoom is complete.
420 */
421 zoomTo: function zoomTo(level, animate, originLeft, originTop, callback) {
422
423 var self = this;
424
425 if (!self.options.zooming) {
426 throw new Error("Zooming is not enabled!");
427 }
428
429 // Add callback if exists
430 if (callback) {
431 self.__zoomComplete = callback;
432 }
433
434 // Stop deceleration
435 if (self.__isDecelerating) {
436 core.effect.Animate.stop(self.__isDecelerating);
437 self.__isDecelerating = false;
438 }
439
440 var oldLevel = self.__zoomLevel;
441
442 // Normalize input origin to center of viewport if not defined
443 if (originLeft == null) {
444 originLeft = self.__clientWidth / 2;
445 }
446
447 if (originTop == null) {
448 originTop = self.__clientHeight / 2;
449 }
450
451 // Limit level according to configuration
452 level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
453
454 // Recompute maximum values while temporary tweaking maximum scroll ranges
455 self.__computeScrollMax(level);
456
457 // Recompute left and top coordinates based on new zoom level
458 var left = (originLeft + self.__scrollLeft) * level / oldLevel - originLeft;
459 var top = (originTop + self.__scrollTop) * level / oldLevel - originTop;
460
461 // Limit x-axis
462 if (left > self.__maxScrollLeft) {
463 left = self.__maxScrollLeft;
464 } else if (left < 0) {
465 left = 0;
466 }
467
468 // Limit y-axis
469 if (top > self.__maxScrollTop) {
470 top = self.__maxScrollTop;
471 } else if (top < 0) {
472 top = 0;
473 }
474
475 // Push values out
476 self.__publish(left, top, level, animate);
477 },
478
479 /**
480 * Zooms the content by the given factor.
481 *
482 * @param factor {Number} Zoom by given factor
483 * @param animate {Boolean ? false} Whether to use animation
484 * @param originLeft {Number ? 0} Zoom in at given left coordinate
485 * @param originTop {Number ? 0} Zoom in at given top coordinate
486 * @param callback {Function ? null} A callback that gets fired when the zoom is complete.
487 */
488 zoomBy: function zoomBy(factor, animate, originLeft, originTop, callback) {
489
490 var self = this;
491
492 self.zoomTo(self.__zoomLevel * factor, animate, originLeft, originTop, callback);
493 },
494
495 /**
496 * Scrolls to the given position. Respect limitations and snapping automatically.
497 *
498 * @param left {Number?null} Horizontal scroll position, keeps current if value is <code>null</code>
499 * @param top {Number?null} Vertical scroll position, keeps current if value is <code>null</code>
500 * @param animate {Boolean?false} Whether the scrolling should happen using an animation
501 * @param zoom {Number?null} Zoom level to go to
502 */
503 scrollTo: function scrollTo(left, top, animate, zoom) {
504
505 var self = this;
506
507 // Stop deceleration
508 if (self.__isDecelerating) {
509 core.effect.Animate.stop(self.__isDecelerating);
510 self.__isDecelerating = false;
511 }
512
513 // Correct coordinates based on new zoom level
514 if (zoom != null && zoom !== self.__zoomLevel) {
515
516 if (!self.options.zooming) {
517 throw new Error("Zooming is not enabled!");
518 }
519
520 left *= zoom;
521 top *= zoom;
522
523 // Recompute maximum values while temporary tweaking maximum scroll ranges
524 self.__computeScrollMax(zoom);
525 } else {
526
527 // Keep zoom when not defined
528 zoom = self.__zoomLevel;
529 }
530
531 if (!self.options.scrollingX) {
532
533 left = self.__scrollLeft;
534 } else {
535
536 if (self.options.paging) {
537 left = Math.round(left / self.__clientWidth) * self.__clientWidth;
538 } else if (self.options.snapping) {
539 left = Math.round(left / self.__snapWidth) * self.__snapWidth;
540 }
541 }
542
543 if (!self.options.scrollingY) {
544
545 top = self.__scrollTop;
546 } else {
547
548 if (self.options.paging) {
549 top = Math.round(top / self.__clientHeight) * self.__clientHeight;
550 } else if (self.options.snapping) {
551 top = Math.round(top / self.__snapHeight) * self.__snapHeight;
552 }
553 }
554
555 // Limit for allowed ranges
556 left = Math.max(Math.min(self.__maxScrollLeft, left), 0);
557 top = Math.max(Math.min(self.__maxScrollTop, top), 0);
558
559 // Don't animate when no change detected, still call publish to make sure
560 // that rendered position is really in-sync with internal data
561 if (left === self.__scrollLeft && top === self.__scrollTop) {
562 animate = false;
563 }
564
565 // Publish new values
566 self.__publish(left, top, zoom, animate);
567 },
568
569 /**
570 * Scroll by the given offset
571 *
572 * @param left {Number ? 0} Scroll x-axis by given offset
573 * @param top {Number ? 0} Scroll x-axis by given offset
574 * @param animate {Boolean ? false} Whether to animate the given change
575 */
576 scrollBy: function scrollBy(left, top, animate) {
577
578 var self = this;
579
580 var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
581 var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;
582
583 self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
584 },
585
586 /*
587 ---------------------------------------------------------------------------
588 EVENT CALLBACKS
589 ---------------------------------------------------------------------------
590 */
591
592 /**
593 * Mouse wheel handler for zooming support
594 */
595 doMouseZoom: function doMouseZoom(wheelDelta, timeStamp, pageX, pageY) {
596
597 var self = this;
598 var change = wheelDelta > 0 ? 0.97 : 1.03;
599
600 return self.zoomTo(self.__zoomLevel * change, false, pageX - self.__clientLeft, pageY - self.__clientTop);
601 },
602
603 /**
604 * Touch start handler for scrolling support
605 */
606 doTouchStart: function doTouchStart(touches, timeStamp) {
607
608 // Array-like check is enough here
609 if (touches.length == null) {
610 throw new Error("Invalid touch list: " + touches);
611 }
612
613 if (timeStamp instanceof Date) {
614 timeStamp = timeStamp.valueOf();
615 }
616 if (typeof timeStamp !== "number") {
617 throw new Error("Invalid timestamp value: " + timeStamp);
618 }
619
620 var self = this;
621
622 // Reset interruptedAnimation flag
623 self.__interruptedAnimation = true;
624
625 // Stop deceleration
626 if (self.__isDecelerating) {
627 core.effect.Animate.stop(self.__isDecelerating);
628 self.__isDecelerating = false;
629 self.__interruptedAnimation = true;
630 }
631
632 // Stop animation
633 if (self.__isAnimating) {
634 core.effect.Animate.stop(self.__isAnimating);
635 self.__isAnimating = false;
636 self.__interruptedAnimation = true;
637 }
638
639 // Use center point when dealing with two fingers
640 var currentTouchLeft, currentTouchTop;
641 var isSingleTouch = touches.length === 1;
642 if (isSingleTouch) {
643 currentTouchLeft = touches[0].pageX;
644 currentTouchTop = touches[0].pageY;
645 } else {
646 currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
647 currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
648 }
649
650 // Store initial positions
651 self.__initialTouchLeft = currentTouchLeft;
652 self.__initialTouchTop = currentTouchTop;
653
654 // Store current zoom level
655 self.__zoomLevelStart = self.__zoomLevel;
656
657 // Store initial touch positions
658 self.__lastTouchLeft = currentTouchLeft;
659 self.__lastTouchTop = currentTouchTop;
660
661 // Store initial move time stamp
662 self.__lastTouchMove = timeStamp;
663
664 // Reset initial scale
665 self.__lastScale = 1;
666
667 // Reset locking flags
668 self.__enableScrollX = !isSingleTouch && self.options.scrollingX;
669 self.__enableScrollY = !isSingleTouch && self.options.scrollingY;
670
671 // Reset tracking flag
672 self.__isTracking = true;
673
674 // Reset deceleration complete flag
675 self.__didDecelerationComplete = false;
676
677 // Dragging starts directly with two fingers, otherwise lazy with an offset
678 self.__isDragging = !isSingleTouch;
679
680 // Some features are disabled in multi touch scenarios
681 self.__isSingleTouch = isSingleTouch;
682
683 // Clearing data structure
684 self.__positions = [];
685 },
686
687 /**
688 * Touch move handler for scrolling support
689 */
690 doTouchMove: function doTouchMove(touches, timeStamp, scale) {
691 // Array-like check is enough here
692 if (touches.length == null) {
693 throw new Error("Invalid touch list: " + touches);
694 }
695
696 if (timeStamp instanceof Date) {
697 timeStamp = timeStamp.valueOf();
698 }
699 if (typeof timeStamp !== "number") {
700 throw new Error("Invalid timestamp value: " + timeStamp);
701 }
702
703 var self = this;
704
705 // Ignore event when tracking is not enabled (event might be outside of element)
706 if (!self.__isTracking) {
707 return;
708 }
709
710 var currentTouchLeft, currentTouchTop;
711
712 // Compute move based around of center of fingers
713 if (touches.length === 2) {
714 currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
715 currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
716 } else {
717 currentTouchLeft = touches[0].pageX;
718 currentTouchTop = touches[0].pageY;
719 }
720
721 var positions = self.__positions;
722
723 // Are we already is dragging mode?
724 if (self.__isDragging) {
725
726 // Compute move distance
727 var moveX = currentTouchLeft - self.__lastTouchLeft;
728 var moveY = currentTouchTop - self.__lastTouchTop;
729
730 // Read previous scroll position and zooming
731 var scrollLeft = self.__scrollLeft;
732 var scrollTop = self.__scrollTop;
733 var level = self.__zoomLevel;
734
735 // Work with scaling
736 if (scale != null && self.options.zooming) {
737
738 var oldLevel = level;
739
740 // Recompute level based on previous scale and new scale
741 level = level / self.__lastScale * scale;
742
743 // Limit level according to configuration
744 level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
745
746 // Only do further compution when change happened
747 if (oldLevel !== level) {
748
749 // Compute relative event position to container
750 var currentTouchLeftRel = currentTouchLeft - self.__clientLeft;
751 var currentTouchTopRel = currentTouchTop - self.__clientTop;
752
753 // Recompute left and top coordinates based on new zoom level
754 scrollLeft = (currentTouchLeftRel + scrollLeft) * level / oldLevel - currentTouchLeftRel;
755 scrollTop = (currentTouchTopRel + scrollTop) * level / oldLevel - currentTouchTopRel;
756
757 // Recompute max scroll values
758 self.__computeScrollMax(level);
759 }
760 }
761
762 if (self.__enableScrollX) {
763
764 scrollLeft -= moveX * this.options.speedMultiplier;
765 var maxScrollLeft = self.__maxScrollLeft;
766
767 if (scrollLeft > maxScrollLeft || scrollLeft < 0) {
768
769 // Slow down on the edges
770 if (self.options.bouncing) {
771
772 scrollLeft += moveX / 2 * this.options.speedMultiplier;
773 } else if (scrollLeft > maxScrollLeft) {
774
775 scrollLeft = maxScrollLeft;
776 } else {
777
778 scrollLeft = 0;
779 }
780 }
781 }
782
783 // Compute new vertical scroll position
784 if (self.__enableScrollY) {
785
786 scrollTop -= moveY * this.options.speedMultiplier;
787 var maxScrollTop = self.__maxScrollTop;
788
789 if (scrollTop > maxScrollTop || scrollTop < 0) {
790
791 // Slow down on the edges
792 if (self.options.bouncing) {
793
794 scrollTop += moveY / 2 * this.options.speedMultiplier;
795
796 // Support pull-to-refresh (only when only y is scrollable)
797 if (!self.__enableScrollX && self.__refreshHeight != null) {
798
799 if (!self.__refreshActive && scrollTop <= -self.__refreshHeight) {
800
801 self.__refreshActive = true;
802 if (self.__refreshActivate) {
803 self.__refreshActivate();
804 }
805 } else if (self.__refreshActive && scrollTop > -self.__refreshHeight) {
806
807 self.__refreshActive = false;
808 if (self.__refreshDeactivate) {
809 self.__refreshDeactivate();
810 }
811 }
812 }
813 } else if (scrollTop > maxScrollTop) {
814
815 scrollTop = maxScrollTop;
816 } else {
817
818 scrollTop = 0;
819 }
820 }
821 }
822
823 // Keep list from growing infinitely (holding min 10, max 20 measure points)
824 if (positions.length > 60) {
825 positions.splice(0, 30);
826 }
827
828 // Track scroll movement for decleration
829 positions.push(scrollLeft, scrollTop, timeStamp);
830
831 // Sync scroll position
832 self.__publish(scrollLeft, scrollTop, level);
833
834 // Otherwise figure out whether we are switching into dragging mode now.
835 } else {
836
837 var minimumTrackingForScroll = self.options.locking ? 3 : 0;
838 var minimumTrackingForDrag = 5;
839
840 var distanceX = Math.abs(currentTouchLeft - self.__initialTouchLeft);
841 var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop);
842
843 self.__enableScrollX = self.options.scrollingX && distanceX >= minimumTrackingForScroll;
844 self.__enableScrollY = self.options.scrollingY && distanceY >= minimumTrackingForScroll;
845
846 positions.push(self.__scrollLeft, self.__scrollTop, timeStamp);
847
848 self.__isDragging = (self.__enableScrollX || self.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag);
849 if (self.__isDragging) {
850 self.__interruptedAnimation = false;
851 }
852 }
853
854 // Update last touch positions and time stamp for next event
855 self.__lastTouchLeft = currentTouchLeft;
856 self.__lastTouchTop = currentTouchTop;
857 self.__lastTouchMove = timeStamp;
858 self.__lastScale = scale;
859 },
860
861 /**
862 * Touch end handler for scrolling support
863 */
864 doTouchEnd: function doTouchEnd(timeStamp) {
865
866 if (timeStamp instanceof Date) {
867 timeStamp = timeStamp.valueOf();
868 }
869 if (typeof timeStamp !== "number") {
870 throw new Error("Invalid timestamp value: " + timeStamp);
871 }
872
873 var self = this;
874
875 // Ignore event when tracking is not enabled (no touchstart event on element)
876 // This is required as this listener ('touchmove') sits on the document and not on the element itself.
877 if (!self.__isTracking) {
878 return;
879 }
880
881 // Not touching anymore (when two finger hit the screen there are two touch end events)
882 self.__isTracking = false;
883
884 // Be sure to reset the dragging flag now. Here we also detect whether
885 // the finger has moved fast enough to switch into a deceleration animation.
886 if (self.__isDragging) {
887
888 // Reset dragging flag
889 self.__isDragging = false;
890
891 // Start deceleration
892 // Verify that the last move detected was in some relevant time frame
893 if (self.__isSingleTouch && self.options.animating && timeStamp - self.__lastTouchMove <= 100) {
894
895 // Then figure out what the scroll position was about 100ms ago
896 var positions = self.__positions;
897 var endPos = positions.length - 1;
898 var startPos = endPos;
899
900 // Move pointer to position measured 100ms ago
901 for (var i = endPos; i > 0 && positions[i] > self.__lastTouchMove - 100; i -= 3) {
902 startPos = i;
903 }
904
905 // If start and stop position is identical in a 100ms timeframe,
906 // we cannot compute any useful deceleration.
907 if (startPos !== endPos) {
908
909 // Compute relative movement between these two points
910 var timeOffset = positions[endPos] - positions[startPos];
911 var movedLeft = self.__scrollLeft - positions[startPos - 2];
912 var movedTop = self.__scrollTop - positions[startPos - 1];
913
914 // Based on 50ms compute the movement to apply for each render step
915 self.__decelerationVelocityX = movedLeft / timeOffset * (1000 / 60);
916 self.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60);
917
918 // How much velocity is required to start the deceleration
919 var minVelocityToStartDeceleration = self.options.paging || self.options.snapping ? 4 : 1;
920
921 // Verify that we have enough velocity to start deceleration
922 if (Math.abs(self.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration) {
923
924 // Deactivate pull-to-refresh when decelerating
925 if (!self.__refreshActive) {
926 self.__startDeceleration(timeStamp);
927 }
928 }
929 } else {
930 self.options.scrollingComplete();
931 }
932 } else if (timeStamp - self.__lastTouchMove > 100) {
933 self.options.scrollingComplete();
934 }
935 }
936
937 // If this was a slower move it is per default non decelerated, but this
938 // still means that we want snap back to the bounds which is done here.
939 // This is placed outside the condition above to improve edge case stability
940 // e.g. touchend fired without enabled dragging. This should normally do not
941 // have modified the scroll positions or even showed the scrollbars though.
942 if (!self.__isDecelerating) {
943
944 if (self.__refreshActive && self.__refreshStart) {
945
946 // Use publish instead of scrollTo to allow scrolling to out of boundary position
947 // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
948 self.__publish(self.__scrollLeft, -self.__refreshHeight, self.__zoomLevel, true);
949
950 if (self.__refreshStart) {
951 self.__refreshStart();
952 }
953 } else {
954
955 if (self.__interruptedAnimation || self.__isDragging) {
956 self.options.scrollingComplete();
957 }
958 self.scrollTo(self.__scrollLeft, self.__scrollTop, true, self.__zoomLevel);
959
960 // Directly signalize deactivation (nothing todo on refresh?)
961 if (self.__refreshActive) {
962
963 self.__refreshActive = false;
964 if (self.__refreshDeactivate) {
965 self.__refreshDeactivate();
966 }
967 }
968 }
969 }
970
971 // Fully cleanup list
972 self.__positions.length = 0;
973 },
974
975 /*
976 ---------------------------------------------------------------------------
977 PRIVATE API
978 ---------------------------------------------------------------------------
979 */
980
981 /**
982 * Applies the scroll position to the content element
983 *
984 * @param left {Number} Left scroll position
985 * @param top {Number} Top scroll position
986 * @param animate {Boolean?false} Whether animation should be used to move to the new coordinates
987 */
988 __publish: function __publish(left, top, zoom, animate) {
989
990 var self = this;
991
992 // Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
993 var wasAnimating = self.__isAnimating;
994 if (wasAnimating) {
995 core.effect.Animate.stop(wasAnimating);
996 self.__isAnimating = false;
997 }
998
999 if (animate && self.options.animating) {
1000
1001 // Keep scheduled positions for scrollBy/zoomBy functionality
1002 self.__scheduledLeft = left;
1003 self.__scheduledTop = top;
1004 self.__scheduledZoom = zoom;
1005
1006 var oldLeft = self.__scrollLeft;
1007 var oldTop = self.__scrollTop;
1008 var oldZoom = self.__zoomLevel;
1009
1010 var diffLeft = left - oldLeft;
1011 var diffTop = top - oldTop;
1012 var diffZoom = zoom - oldZoom;
1013
1014 var step = function step(percent, now, render) {
1015
1016 if (render) {
1017
1018 self.__scrollLeft = oldLeft + diffLeft * percent;
1019 self.__scrollTop = oldTop + diffTop * percent;
1020 self.__zoomLevel = oldZoom + diffZoom * percent;
1021
1022 // Push values out
1023 if (self.__callback) {
1024 self.__callback(self.__scrollLeft, self.__scrollTop, self.__zoomLevel);
1025 }
1026 }
1027 };
1028
1029 var verify = function verify(id) {
1030 return self.__isAnimating === id;
1031 };
1032
1033 var completed = function completed(renderedFramesPerSecond, animationId, wasFinished) {
1034 if (animationId === self.__isAnimating) {
1035 self.__isAnimating = false;
1036 }
1037 if (self.__didDecelerationComplete || wasFinished) {
1038 self.options.scrollingComplete();
1039 }
1040
1041 if (self.options.zooming) {
1042 self.__computeScrollMax();
1043 if (self.__zoomComplete) {
1044 self.__zoomComplete();
1045 self.__zoomComplete = null;
1046 }
1047 }
1048 };
1049
1050 // When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
1051 self.__isAnimating = core.effect.Animate.start(step, verify, completed, self.options.animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic);
1052 } else {
1053
1054 self.__scheduledLeft = self.__scrollLeft = left;
1055 self.__scheduledTop = self.__scrollTop = top;
1056 self.__scheduledZoom = self.__zoomLevel = zoom;
1057
1058 // Push values out
1059 if (self.__callback) {
1060 self.__callback(left, top, zoom);
1061 }
1062
1063 // Fix max scroll ranges
1064 if (self.options.zooming) {
1065 self.__computeScrollMax();
1066 if (self.__zoomComplete) {
1067 self.__zoomComplete();
1068 self.__zoomComplete = null;
1069 }
1070 }
1071 }
1072 },
1073
1074 /**
1075 * Recomputes scroll minimum values based on client dimensions and content dimensions.
1076 */
1077 __computeScrollMax: function __computeScrollMax(zoomLevel) {
1078
1079 var self = this;
1080
1081 if (zoomLevel == null) {
1082 zoomLevel = self.__zoomLevel;
1083 }
1084
1085 self.__maxScrollLeft = Math.max(self.__contentWidth * zoomLevel - self.__clientWidth, 0);
1086 self.__maxScrollTop = Math.max(self.__contentHeight * zoomLevel - self.__clientHeight, 0);
1087 },
1088
1089 /*
1090 ---------------------------------------------------------------------------
1091 ANIMATION (DECELERATION) SUPPORT
1092 ---------------------------------------------------------------------------
1093 */
1094
1095 /**
1096 * Called when a touch sequence end and the speed of the finger was high enough
1097 * to switch into deceleration mode.
1098 */
1099 __startDeceleration: function __startDeceleration(timeStamp) {
1100
1101 var self = this;
1102
1103 if (self.options.paging) {
1104
1105 var scrollLeft = Math.max(Math.min(self.__scrollLeft, self.__maxScrollLeft), 0);
1106 var scrollTop = Math.max(Math.min(self.__scrollTop, self.__maxScrollTop), 0);
1107 var clientWidth = self.__clientWidth;
1108 var clientHeight = self.__clientHeight;
1109
1110 // We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area.
1111 // Each page should have exactly the size of the client area.
1112 self.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth;
1113 self.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight;
1114 self.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth;
1115 self.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight;
1116 } else {
1117
1118 self.__minDecelerationScrollLeft = 0;
1119 self.__minDecelerationScrollTop = 0;
1120 self.__maxDecelerationScrollLeft = self.__maxScrollLeft;
1121 self.__maxDecelerationScrollTop = self.__maxScrollTop;
1122 }
1123
1124 // Wrap class method
1125 var step = function step(percent, now, render) {
1126 self.__stepThroughDeceleration(render);
1127 };
1128
1129 // How much velocity is required to keep the deceleration running
1130 var minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.1;
1131
1132 // Detect whether it's still worth to continue animating steps
1133 // If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
1134 var verify = function verify() {
1135 var shouldContinue = Math.abs(self.__decelerationVelocityX) >= minVelocityToKeepDecelerating || Math.abs(self.__decelerationVelocityY) >= minVelocityToKeepDecelerating;
1136 if (!shouldContinue) {
1137 self.__didDecelerationComplete = true;
1138 }
1139 return shouldContinue;
1140 };
1141
1142 var completed = function completed(renderedFramesPerSecond, animationId, wasFinished) {
1143 self.__isDecelerating = false;
1144 if (self.__didDecelerationComplete) {
1145 self.options.scrollingComplete();
1146 }
1147
1148 // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
1149 self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping);
1150 };
1151
1152 // Start animation and switch on flag
1153 self.__isDecelerating = core.effect.Animate.start(step, verify, completed);
1154 },
1155
1156 /**
1157 * Called on every step of the animation
1158 *
1159 * @param inMemory {Boolean?false} Whether to not render the current step, but keep it in memory only. Used internally only!
1160 */
1161 __stepThroughDeceleration: function __stepThroughDeceleration(render) {
1162
1163 var self = this;
1164
1165 //
1166 // COMPUTE NEXT SCROLL POSITION
1167 //
1168
1169 // Add deceleration to scroll position
1170 var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX;
1171 var scrollTop = self.__scrollTop + self.__decelerationVelocityY;
1172
1173 //
1174 // HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE
1175 //
1176
1177 if (!self.options.bouncing) {
1178
1179 var scrollLeftFixed = Math.max(Math.min(self.__maxDecelerationScrollLeft, scrollLeft), self.__minDecelerationScrollLeft);
1180 if (scrollLeftFixed !== scrollLeft) {
1181 scrollLeft = scrollLeftFixed;
1182 self.__decelerationVelocityX = 0;
1183 }
1184
1185 var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop);
1186 if (scrollTopFixed !== scrollTop) {
1187 scrollTop = scrollTopFixed;
1188 self.__decelerationVelocityY = 0;
1189 }
1190 }
1191
1192 //
1193 // UPDATE SCROLL POSITION
1194 //
1195
1196 if (render) {
1197
1198 self.__publish(scrollLeft, scrollTop, self.__zoomLevel);
1199 } else {
1200
1201 self.__scrollLeft = scrollLeft;
1202 self.__scrollTop = scrollTop;
1203 }
1204
1205 //
1206 // SLOW DOWN
1207 //
1208
1209 // Slow down velocity on every iteration
1210 if (!self.options.paging) {
1211
1212 // This is the factor applied to every iteration of the animation
1213 // to slow down the process. This should emulate natural behavior where
1214 // objects slow down when the initiator of the movement is removed
1215 var frictionFactor = 0.95;
1216
1217 self.__decelerationVelocityX *= frictionFactor;
1218 self.__decelerationVelocityY *= frictionFactor;
1219 }
1220
1221 //
1222 // BOUNCING SUPPORT
1223 //
1224
1225 if (self.options.bouncing) {
1226
1227 var scrollOutsideX = 0;
1228 var scrollOutsideY = 0;
1229
1230 // This configures the amount of change applied to deceleration/acceleration when reaching boundaries
1231 var penetrationDeceleration = self.options.penetrationDeceleration;
1232 var penetrationAcceleration = self.options.penetrationAcceleration;
1233
1234 // Check limits
1235 if (scrollLeft < self.__minDecelerationScrollLeft) {
1236 scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft;
1237 } else if (scrollLeft > self.__maxDecelerationScrollLeft) {
1238 scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft;
1239 }
1240
1241 if (scrollTop < self.__minDecelerationScrollTop) {
1242 scrollOutsideY = self.__minDecelerationScrollTop - scrollTop;
1243 } else if (scrollTop > self.__maxDecelerationScrollTop) {
1244 scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop;
1245 }
1246
1247 // Slow down until slow enough, then flip back to snap position
1248 if (scrollOutsideX !== 0) {
1249 if (scrollOutsideX * self.__decelerationVelocityX <= 0) {
1250 self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration;
1251 } else {
1252 self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration;
1253 }
1254 }
1255
1256 if (scrollOutsideY !== 0) {
1257 if (scrollOutsideY * self.__decelerationVelocityY <= 0) {
1258 self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration;
1259 } else {
1260 self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration;
1261 }
1262 }
1263 }
1264 }
1265 };
1266
1267 // Copy over members to prototype
1268 for (var key in members) {
1269 Scroller.prototype[key] = members[key];
1270 }
1271})();
1272
1273module.exports = Scroller;
\No newline at end of file