UNPKG

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