1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2014 7x7 Software Inc. All rights reserved.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 sc_require('views/scroller');
  8 
  9 
 10 SC.SCROLL = {
 11 
 12   /**
 13     The rate of deceleration in pixels per square millisecond after scrolling from a drag gesture.
 14 
 15     @static
 16     @type Number
 17     @default 3.0
 18   */
 19   DRAG_SCROLL_DECELERATION: 3.0,
 20 
 21   /**
 22     The number of pixels a gesture needs to move before it should be considered a scroll gesture.
 23 
 24     @static
 25     @type Number
 26     @default 5
 27   */
 28   SCROLL_GESTURE_THRESHOLD: 5,
 29 
 30   /**
 31     The number of pixels a gesture needs to move in only a single direction, before it should be
 32     considered as a locked scrolling direction (i.e. no gestures in the other direction will scroll
 33     in that direction).
 34 
 35     @static
 36     @type Number
 37     @default 50
 38   */
 39   SCROLL_LOCK_GESTURE_THRESHOLD: 50,
 40 
 41   /**
 42     The number of pixels a gesture needs to expand or contract before it should be considered a scale gesture.
 43 
 44     @static
 45     @type Number
 46     @default 3
 47   */
 48   SCALE_GESTURE_THRESHOLD: 3
 49 
 50 };
 51 
 52 
 53 /** @class
 54   Implements a complete scroll view. SproutCore implements its own JS-based scrolling in order
 55   to unify scrolling behavior across platforms, and to enable progressive rendering (via the
 56   clipping frame) during scroll on all devices.
 57 
 58   Important Properties
 59   -----
 60 
 61   ScrollView positions its contentView according to three properties: `verticalScrollOffset`,
 62   `horizontalScrollOffset`, and `scale`. These properties are bindable and observable, but you
 63   should not override them.
 64 
 65   Gutter vs. Overlaid Scrollers
 66   -----
 67 
 68   Scroll views use swappable scroll-bar views with various behavior (see `verticalScrollerView`
 69   and `horizontalScrollerView`). `SC.ScrollerView` is a gutter-based scroller which persists and
 70   takes up fourteen pixels. (By default, no scroller is shown for content that is too small to
 71   scroll; see `autohidesHorizontalScroller` and `autohidesVerticalScroller`.) `SC.OverlayScrollerView`
 72   is a gutterless view which fades when not scrolling. If you would like your view to always have
 73   OS X-style fading overlaid scrollers, you can use the following:
 74 
 75         SC.ScrollView.extend({
 76           horizontalOverlay: true,
 77           verticalOverlay: true
 78         });
 79 
 80   @extends SC.View
 81   @since SproutCore 1.0
 82 */
 83 SC.ScrollView = SC.View.extend({
 84 /** @scope SC.ScrollView.prototype */
 85 
 86   // ---------------------------------------------------------------------------------------------
 87   // Properties
 88   //
 89 
 90   /** @private Flag used to determine whether to animate the adjustment. */
 91   _sc_animationDuration: null,
 92 
 93   /** @private The animation timing to use. */
 94   _sc_animationTiming: null,
 95 
 96   /** @private The cached height of the container. */
 97   _sc_containerHeight: 0,
 98 
 99   /** @private The cached offset of the container. */
100   _sc_containerOffset: null,
101 
102   /** @private The cached width of the container. */
103   _sc_containerWidth: 0,
104 
105   /** @private The cached height of the content. */
106   _sc_contentHeight: 0,
107 
108   /** @private Flag used to react accordingly when the content's height changes. */
109   _sc_contentHeightDidChange: false,
110 
111   /** @private The cached scale of the content. */
112   _sc_contentScale: undefined,
113 
114   /** @private Flag used to react accordingly when the content's scale changes. */
115   _sc_contentScaleDidChange: false,
116 
117   /** @private The cached width of the content. */
118   _sc_contentWidth: 0,
119 
120   /** @private Flag used to react accordingly when the content's width changes. */
121   _sc_contentWidthDidChange: false,
122 
123   /** @private The anchor horizontal offset of the touch gesture. */
124   _sc_gestureAnchorHOffset: null,
125 
126   /** @private The anchor position of the initial touch gesture. */
127   _sc_gestureAnchorTotalD: null,
128 
129   /** @private The anchor position of the initial touch gesture. */
130   _sc_gestureAnchorTotalX: null,
131 
132   /** @private The anchor position of the initial touch gesture. */
133   _sc_gestureAnchorTotalY: null,
134 
135   /** @private The anchor vertical offset of the touch gesture. */
136   _sc_gestureAnchorVOffset: null,
137 
138   /** @private The anchor position of the last touch gesture. */
139   _sc_gestureAnchorX: null,
140 
141   /** @private The anchor position of the last touch gesture. */
142   _sc_gestureAnchorY: null,
143 
144   /** @private The anchor distance from center of the last touch gesture. */
145   _sc_gestureAnchorD: null,
146 
147   /** @private The original scale before a touch gesture. */
148   _sc_gestureAnchorScale: null,
149 
150   /** @private The timer used to fade out this scroller. */
151   _sc_horizontalFadeOutTimer: null,
152 
153   /** @private The actual horizontal scroll offset. */
154   _sc_horizontalScrollOffset: null,
155 
156   /** @private The percentage offset scrolled horizontally. Used to maintain the horizontal position when the content size changes. */
157   _sc_horizontalPct: null,
158 
159   /** @private Flag is true when scaling. Used in capturing touches. */
160   _sc_isTouchScaling: false,
161 
162   /** @private Flag is true when scrolling horizontally. Used in capturing touches. */
163   _sc_isTouchScrollingH: false,
164 
165   /** @private Flag is true when scrolling is locked to horizontal. */
166   _sc_isTouchScrollingHOnly: false,
167 
168   /** @private Flag is true when scrolling vertically. Used in capturing touches. */
169   _sc_isTouchScrollingV: false,
170 
171   /** @private Flag is true when scrolling is locked to vertical. */
172   _sc_isTouchScrollingVOnly: false,
173 
174   /** @private The minimum delay before applying a fade transition. */
175   _sc_minimumFadeOutDelay: function () {
176     // The fade out delay is never less than 100ms (so that the current run loop can complete) and is never less than the fade in duration (so that it can fade fully in).
177     return Math.max(Math.max(this.get('fadeOutDelay') || 0, 0.1), this.get('fadeInDuration') || 0) * 1000;
178   }.property('fadeOutDelay').cacheable(),
179 
180   /** @private The amount of slip while over dragging (drag past bounds). 1.0 or 100% would slip completely, and 0.0 or 0% would not slip at all.  */
181   _sc_overDragSlip: 0.5,
182 
183   /** @private Timer used to pass a touch through to its content if we don't start scrolling in that time. */
184   _sc_passTouchToContentTimer: null,
185 
186   /** @private The actual scale. */
187   _sc_scale: 1,
188 
189   /** @private Flag used to indicate when we should resize the content width manually. */
190   // _sc_shouldResizeContentWidth: false,
191 
192   /** @private Flag used to indicate when we should resize the content height manually. */
193   // _sc_shouldResizeContentHeight: false,
194 
195   /** @private The offset center x of a multi-touch gesture. */
196   _sc_touchCenterX: null,
197 
198   /** @private The offset center y of a multi-touch gesture. */
199   _sc_touchCenterY: null,
200 
201   /** @private The timer used to fade out this scroller. */
202   _sc_verticalFadeOutTimer: null,
203 
204   /** @private The actual vertical scroll offset. */
205   _sc_verticalScrollOffset: null,
206 
207   /** @private The percentage offset scrolled vertically. Used to maintain the vertical position when the content size changes. */
208   _sc_verticalPct: null,
209 
210   /** @see SC.View.prototype.acceptsMultitouch
211 
212     @type Boolean
213     @default true
214   */
215   acceptsMultitouch: true,
216 
217   /** @private Animation curves. Kept private b/c it will likely become a computed property. */
218   animationCurveDecelerate: SC.easingCurve(0.35,0.34,0.84,1), // 'cubic-bezier(.35,.34,.84,1)',  // http://cubic-bezier.com
219 
220   /** @private Animation curves. Kept private b/c it will likely become a computed property. */
221   animationCurveReverse: SC.easingCurve(0.45,-0.47,0.73,1.3), // 'cubic-bezier(0.45,-0.47,0.73,1.3)',
222 
223   /** @private Animation curves. Kept private b/c it will likely become a computed property. */
224   animationCurveSnap: SC.easingCurve(0.28,0.36,0.52,1), // 'cubic-bezier(.28,.36,.52,1)',
225 
226   /**
227     If true, the horizontal scroller will automatically hide if the contentView is smaller than the
228     visible area.  The `hasHorizontalScroller` property must be set to true in order for this property
229     to have any effect.
230 
231     @type Boolean
232     @default true
233   */
234   autohidesHorizontalScroller: true,
235 
236   /**
237     If true, the vertical scroller will automatically hide if the contentView is smaller than the
238     visible area.  The `hasVerticalScroller` property must be set to true in order for this property
239     to have any effect.
240 
241     @type Boolean
242     @default true
243   */
244   autohidesVerticalScroller: true,
245 
246   /**
247     Determines whether scaling is allowed.
248 
249     @type Boolean
250     @default false
251   */
252   canScale: false,
253 
254   /**
255     Returns true if the view has both a horizontal scroller and the scroller is visible.
256 
257     @field
258     @type Boolean
259     @readonly
260   */
261   canScrollHorizontal: function () {
262     return !!(this.get('hasHorizontalScroller') && // This property isn't bindable.
263       this.get('horizontalScrollerView') && // This property isn't bindable.
264       this.get('isHorizontalScrollerVisible'));
265   }.property('isHorizontalScrollerVisible').cacheable(),
266 
267   /**
268     Returns true if the view has both a vertical scroller and the scroller is visible.
269 
270     @field
271     @type Boolean
272     @readonly
273   */
274   canScrollVertical: function () {
275     return !!(this.get('hasVerticalScroller') && // This property isn't bindable.
276       this.get('verticalScrollerView') && // This property isn't bindable.
277       this.get('isVerticalScrollerVisible'));
278   }.property('isVerticalScrollerVisible').cacheable(),
279 
280   /**
281     @type Array
282     @default ['sc-scroll-view']
283     @see SC.View#classNames
284   */
285   classNames: ['sc-scroll-view'],
286 
287   /**
288     The container view that will wrap your content view.  You can replace this property with your own
289     custom class if you prefer.
290 
291     @type SC.ContainerView
292     @default SC.ContainerView
293   */
294   containerView: SC.ContainerView,
295 
296   /**
297     The content view you want the scroll view to manage.
298 
299     @type SC.View
300     @default null
301   */
302   contentView: null,
303 
304   /**
305     The scroll deceleration rate.
306 
307     @type Number
308     @default SC.SCROLL.DRAG_SCROLL_DECELERATION
309   */
310   decelerationRate: SC.SCROLL.DRAG_SCROLL_DECELERATION,
311 
312   /** @private
313     Whether to delay touches from passing through to the content. By default, if the touch moves enough to
314     trigger a scroll within 150ms, this view will retain control of the touch, and content views will not
315     have a chance to handle it. This is generally the behavior you want.
316 
317     If you set this to NO, the touch will not trigger a scroll until you pass control back to this view via
318     `touch.restoreLastTouchResponder`, for example when the touch has dragged by a certain amount. You should
319     use this option only if you know what you're doing.
320 
321     @type Boolean
322     @default true
323   */
324   delaysContentTouches: true,
325 
326   /**
327     Determines how long (in seconds) scrollbars wait before fading out.
328 
329     @property Number
330     @default 0.4
331    */
332   fadeOutDelay: 0.4,
333 
334   /**
335     True if the view should maintain a horizontal scroller.   This property must be set when the
336     view is created.
337 
338     @type Boolean
339     @default true
340   */
341   hasHorizontalScroller: true,
342 
343   /**
344     True if the view should maintain a vertical scroller.   This property must be set when the
345     view is created.
346 
347     @type Boolean
348     @default true
349   */
350   hasVerticalScroller: true,
351 
352   /**
353     The horizontal alignment for non-filling content inside of the ScrollView. Possible values:
354 
355       - SC.ALIGN_LEFT
356       - SC.ALIGN_RIGHT
357       - SC.ALIGN_CENTER
358 
359     @type String
360     @default SC.ALIGN_CENTER
361   */
362   horizontalAlign: SC.ALIGN_CENTER,
363 
364   /**
365     Determines whether the horizontal scroller should fade out while in overlay mode. Has no effect
366     if `horizontalOverlay` is set to false.
367 
368     @property Boolean
369     @default true
370    */
371   horizontalFade: true,
372 
373   /**
374     Amount to scroll one horizontal line.
375 
376     Used by the default implementation of scrollLeftLine() and
377     scrollRightLine().
378 
379     @type Number
380     @default 20
381   */
382   horizontalLineScroll: 20,
383 
384   /**
385     Use this to overlay the horizontal scroller.
386 
387     This ensures that the content container will not resize to accommodate the horizontal scroller,
388     hence overlaying the scroller on top of the container.
389 
390     @field
391     @type Boolean
392     @default true
393   */
394   horizontalOverlay: false,
395 
396   /**
397     Amount to scroll one horizontal page.
398 
399     Used by the default implementation of scrollLeftPage() and scrollRightPage().
400 
401     @field
402     @type Number
403     @default value of frame.width
404     @observes frame
405   */
406   horizontalPageScroll: function () {
407     return this.get('frame').width;
408   }.property('frame'),
409 
410   /**
411     The current horizontal scroll offset. Changing this value will update both the position of the
412     contentView and the horizontal scroller, if there is one.
413 
414     @field
415     @type Number
416     @default 0
417   */
418   horizontalScrollOffset: function (key, value) {
419     var containerWidth = this._sc_containerWidth,
420         contentWidth = this._sc_contentWidth,
421         min = this.get('minimumHorizontalScrollOffset'),
422         max = this.get('maximumHorizontalScrollOffset');
423 
424     /* jshint eqnull:true */
425     if (value != null) {
426       // When touch scrolling, we allow scroll to pass the limits by a small amount.
427       if (!this._sc_isTouchScrollingH) {
428         // Constrain to the set limits.
429         value = Math.max(min, Math.min(max, value));
430       }
431 
432       // Record the relative percentage offset for maintaining position while scaling.
433       if (contentWidth > 0) {
434         this._sc_horizontalPct = (value + (containerWidth / 2)) / contentWidth;
435       }
436 
437     // Use the cached value.
438     } else {
439       value = this._sc_horizontalScrollOffset;
440 
441       // Default value.
442       if (value == null) {
443         var horizontalAlign = this.get('initialHorizontalAlign');
444 
445         value = this._sc_alignedHorizontalOffset(horizontalAlign, containerWidth, contentWidth);
446       }
447     }
448 
449     // Update the actual value.
450     this._sc_horizontalScrollOffset = value;
451 
452     return value;
453   }.property().cacheable(),
454 
455   /**
456     Use to control the positioning of the horizontal scroller. If you do not set 'horizontalOverlay' to
457     true, then the content view will be automatically sized to meet the left edge of the vertical
458     scroller, wherever it may be.
459 
460     This allows you to easily, for example, have “one pixel higher and one pixel lower” scroll bars
461     that blend into their parent views.
462 
463     If you do set 'horizontalOverlay' to true, then the scroller view will “float on top” of the content view.
464 
465     Example: { left: -1, bottom: 0, right: -1 }
466 
467     @type Object
468     @default null
469   */
470   horizontalScrollerLayout: null,
471 
472   /**
473     The horizontal scroller view class. This will be replaced with a view instance when the
474     ScrollView is created unless `hasHorizontalScroller` is false.
475 
476     If `horizontalOverlay` is `true`, the default view used will be an SC.OverlayScrollerView,
477     otherwise SC.ScrollerView will be used.
478 
479     @type SC.View
480     @default SC.ScrollerView | SC.OverlayScrollerView
481   */
482   horizontalScrollerView: null,
483 
484   /**
485     Your content view's initial horizontal alignment, if wider than the container. This allows you to e.g.
486     center the content view when zoomed out, but begin with it zoomed in and left-aligned. If not specified,
487     defaults to value of `horizontalAlign`. May be:
488 
489       - SC.ALIGN_LEFT
490       - SC.ALIGN_RIGHT
491       - SC.ALIGN_CENTER
492 
493     @type String
494     @default SC.ALIGN_LEFT
495   */
496   initialHorizontalAlign: SC.outlet('horizontalAlign'),
497 
498   /**
499     Your content view's initial vertical alignment, if taller than the container. This allows you to e.g.
500     center the content view when zoomed out, but begin with it zoomed in and top-aligned. If not specified,
501     defaults to the value of `verticalAlign`. May be:
502 
503       - SC.ALIGN_TOP
504       - SC.ALIGN_BOTTOM
505       - SC.ALIGN_MIDDLE
506 
507     @type String
508     @default SC.ALIGN_TOP
509   */
510   initialVerticalAlign: SC.outlet('verticalAlign'),
511 
512   /**
513     True if the horizontal scroller should be visible. You can change this property value anytime to
514     show or hide the horizontal scroller.  If you do not want to use a horizontal scroller at all, you
515     should instead set `hasHorizontalScroller` to false to avoid creating a scroller view in the first
516     place.
517 
518     @type Boolean
519     @default true
520   */
521   isHorizontalScrollerVisible: true,
522 
523   /**
524     Walk like a duck.
525 
526     @type Boolean
527     @default true
528     @readOnly
529   */
530   isScrollable: true,
531 
532   /**
533     True if the vertical scroller should be visible. You can change this property value anytime to
534     show or hide the vertical scroller.  If you do not want to use a vertical scroller at all, you
535     should instead set `hasVerticalScroller` to false to avoid creating a scroller view in the first
536     place.
537 
538     @type Boolean
539     @default true
540   */
541   isVerticalScrollerVisible: true,
542 
543   /**
544     The maximum horizontal scroll offset allowed given the current contentView size and the size of
545     the scroll view.  If horizontal scrolling is disabled, this will always return 0.
546 
547     @field
548     @type Number
549     @default 0
550   */
551   maximumHorizontalScrollOffset: function () {
552     return Math.max(this._sc_contentWidth - this._sc_containerWidth, 0);
553   }.property('_sc_containerWidth', '_sc_contentWidth').cacheable(),
554 
555   /**
556     The maximum scale.
557 
558     @type Number
559     @default 2.0
560   */
561   maximumScale: 2.0,
562 
563   /**
564     The maximum vertical scroll offset allowed given the current contentView size and the size of
565     the scroll view.  If vertical scrolling is disabled, this will always return 0 (or whatever
566     alignment dictates).
567 
568     @field
569     @type Number
570     @default 0
571   */
572   maximumVerticalScrollOffset: function () {
573     return Math.max(this._sc_contentHeight - this._sc_containerHeight, 0);
574   }.property('_sc_containerHeight', '_sc_contentHeight').cacheable(),
575 
576   /**
577     The minimum horizontal scroll offset allowed given the current contentView size and the size of
578     the scroll view.  If horizontal scrolling is disabled, this will always return 0 (or whatever alignment dictates).
579 
580     @field
581     @type Number
582     @default 0
583   */
584   minimumHorizontalScrollOffset: function () {
585     return Math.min(this._sc_contentWidth - this._sc_containerWidth, 0);
586   }.property('_sc_containerWidth', '_sc_contentWidth').cacheable(),
587 
588   /**
589     The minimum scale.
590 
591     @type Number
592     @default 0.25
593   */
594   minimumScale: 0.25,
595 
596   /**
597     The minimum vertical scroll offset allowed given the current contentView size and the size of
598     the scroll view.  If vertical scrolling is disabled, this will always return 0 (or whatever alignment dictates).
599 
600     @field
601     @type Number
602     @default 0
603   */
604   minimumVerticalScrollOffset: function () {
605     return Math.min(this._sc_contentHeight - this._sc_containerHeight, 0);
606   }.property('_sc_containerHeight', '_sc_contentHeight').cacheable(),
607 
608   /**
609     The current scale. Setting this will adjust the scale of the contentView.
610 
611     If the contentView implements the SC.Scalable protocol, it will instead pass the scale to the contentView's
612     `applyScale` method instead.
613 
614     Note that on platforms that allow bounce, setting scale outside of the minimum/maximumScale bounds will
615     result in a bounce. It is up to the developer to alert this view when the action is over and it should
616     bounce back.
617 
618     @field
619     @type Number
620     @default 1.0
621   */
622   scale: function (key, value) {
623     /* jshint eqnull:true */
624     if (value != null) {
625       if (!this.get('canScale')) {
626         value = 1;
627       } else {
628         var min = this.get('minimumScale'),
629           max = this.get('maximumScale');
630 
631         // When touch scaling, we allow scaling to pass the limits by a small amount.
632         if (this._sc_isTouchScaling) {
633           min = min - (min * 0.1);
634           max = max + (max * 0.1);
635           if ((value < min || value > max)) {
636             value = Math.min(Math.max(min, value), max);
637           }
638 
639         // Constrain to the set limits.
640         } else {
641           if ((value < min || value > max)) {
642             value = Math.min(Math.max(min, value), max);
643           }
644         }
645       }
646     } else {
647       value = this._sc_scale;
648     }
649 
650     // Update the actual value.
651     this._sc_scale = value;
652 
653     return value;
654   }.property('canScale', 'minimumScale', 'maximumScale').cacheable(),
655 
656   /**
657     This determines how much a gesture must pinch or spread apart (in pixels) before it is registered as a scale action.
658 
659     You can change this value for all instances of SC.ScrollView in your application by overriding
660     `SC.SCROLL.SCALE_GESTURE_THRESHOLD` at launch time.
661 
662     @type Number
663     @default SC.SCROLL.SCALE_GESTURE_THRESHOLD
664   */
665   scaleGestureThreshold: SC.SCROLL.SCALE_GESTURE_THRESHOLD,
666 
667   /**
668     This determines how far (in pixels) a gesture must move before it is registered as a scroll.
669 
670     You can change this value for all instances of SC.ScrollView in your application by overriding
671     `SC.SCROLL.SCROLL_THRESHOLD` at launch time.
672 
673     @type Number
674     @default SC.SCROLL.SCROLL_GESTURE_THRESHOLD
675   */
676   scrollGestureThreshold: SC.SCROLL.SCROLL_GESTURE_THRESHOLD,
677 
678   /**
679     Once a vertical or horizontal scroll has been triggered, this determines how far (in pixels) the gesture
680     must move on the other axis to trigger a two-axis scroll. If your scroll view's content is omnidirectional
681     (e.g. a map) you should set this value to 0.
682 
683     You can change this value for all instances of SC.ScrollView in your application by overriding
684     `SC.SCROLL.SCROLL_LOCK_GESTURE_THRESHOLD` at launch time.
685 
686     @type Number
687     @default SC.SCROLL.SCROLL_LOCK_GESTURE_THRESHOLD
688   */
689   scrollLockGestureThreshold: SC.SCROLL.SCROLL_LOCK_GESTURE_THRESHOLD,
690 
691   /** @private
692     Once a vertical or horizontal scroll has been triggered, this determines how far (in pixels) the gesture
693     must move on the other axis to trigger a two-axis scroll. If your scroll view's content is omnidirectional
694     (e.g. a map) you should set this value to 0.
695 
696     You can change this value for all instances of SC.ScrollView in your application by overriding
697     `SC.SCROLL.TOUCH.DEFAULT_SECONDARY_SCROLL_THRESHOLD` at launch time.
698 
699     @type Number
700     @default SC.SCROLL.TOUCH.DEFAULT_SECONDARY_SCROLL_THRESHOLD
701   */
702   // scrollSecondaryGestureThreshold: SC.SCROLL.TOUCH.DEFAULT_SECONDARY_SCROLL_THRESHOLD,
703 
704   /**
705     The vertical alignment for non-filling content inside of the ScrollView. Possible values:
706 
707       - SC.ALIGN_TOP
708       - SC.ALIGN_BOTTOM
709       - SC.ALIGN_MIDDLE
710 
711     @type String
712     @default SC.ALIGN_TOP
713   */
714   verticalAlign: SC.ALIGN_TOP,
715 
716   /**
717     Determines whether the vertical scroller should fade out while in overlay mode. Has no effect if
718     `verticalOverlay` is set to false.
719 
720     @property Boolean
721     @default true
722    */
723   verticalFade: true,
724 
725   /**
726     Amount to scroll one vertical line.
727 
728     Used by the default implementation of scrollDownLine() and scrollUpLine().
729 
730     @type Number
731     @default 20
732   */
733   verticalLineScroll: 20,
734 
735   /**
736     Use this to overlay the vertical scroller.
737 
738     This ensures that the content container will not resize to accommodate the vertical scroller,
739     hence overlaying the scroller on top of the container.
740 
741     @field
742     @type Boolean
743     @default true
744   */
745   verticalOverlay: false,
746 
747   /**
748     Amount to scroll one vertical page.
749 
750     Used by the default implementation of scrollUpPage() and scrollDownPage().
751 
752     @field
753     @type Number
754     @default value of frame.height
755     @observes frame
756   */
757   verticalPageScroll: function () {
758     return this.get('frame').height;
759   }.property('frame'),
760 
761   /**
762     The current vertical scroll offset. Changing this value will update both the position of the
763     contentView and the vertical scroller, if there is one.
764 
765     @field
766     @type Number
767     @default 0
768   */
769   verticalScrollOffset: function (key, value) {
770     var containerHeight = this._sc_containerHeight,
771       contentHeight = this._sc_contentHeight,
772       min = this.get('minimumVerticalScrollOffset'),
773       max = this.get('maximumVerticalScrollOffset');
774 
775     /* jshint eqnull:true */
776     if (value != null) {
777 
778       // When touch scrolling, we allow scroll to pass the limits by a small amount.
779       if (!this._sc_isTouchScrollingV) {
780         // Constrain to the set limits.
781         value = Math.max(min, Math.min(max, value));
782       }
783 
784       // Record the relative percentage offset for maintaining position while scaling.
785       if (contentHeight > 0) {
786         this._sc_verticalPct = (value + (containerHeight / 2)) / contentHeight;
787       }
788 
789     // Use the cached value.
790     } else {
791       value = this._sc_verticalScrollOffset;
792 
793       // Default value.
794       if (value == null) {
795         var verticalAlign = this.get('initialVerticalAlign');
796 
797         value = this._sc_alignedVerticalOffset(verticalAlign, containerHeight, contentHeight);
798       }
799     }
800 
801     // Update the actual value.
802     this._sc_verticalScrollOffset = value;
803 
804     return value;
805   }.property().cacheable(),
806 
807   /**
808     Use to control the positioning of the vertical scroller. If you do not set 'verticalOverlay' to
809     true, then the content view will be automatically sized to meet the left edge of the vertical
810     scroller, wherever it may be.
811 
812     This allows you to easily, for example, have “one pixel higher and one pixel lower” scroll bars
813     that blend into their parent views.
814 
815     If you do set 'verticalOverlay' to true, then the scroller view will “float on top” of the content view.
816 
817     Example: { top: -1, bottom: -1, right: 0 }
818 
819     @type Object
820     @default null
821   */
822   verticalScrollerLayout: null,
823 
824   /**
825     The vertical scroller view class. This will be replaced with a view instance when the
826     ScrollView is created unless `hasVerticalScroller` is false.
827 
828     If `verticalOverlay` is `true`, the default view used will be an SC.OverlayScrollerView,
829     otherwise SC.ScrollerView will be used.
830 
831     @type SC.View
832     @default SC.ScrollerView | SC.OverlayScrollerView
833   */
834   verticalScrollerView: null,
835 
836   // ---------------------------------------------------------------------------------------------
837   // Methods
838   //
839 
840   /** @private Aligns the content horizontally. */
841   _sc_alignedHorizontalOffset: function (horizontalAlign, containerWidth, contentWidth) {
842     switch (horizontalAlign) {
843     case SC.ALIGN_RIGHT:
844       return 0 - (containerWidth - contentWidth);
845     case SC.ALIGN_CENTER:
846       return 0 - ((containerWidth - contentWidth) / 2);
847     default: // SC.ALIGN_LEFT
848       return 0;
849     }
850   },
851 
852   /** @private Aligns the content vertically. */
853   _sc_alignedVerticalOffset: function (verticalAlign, containerHeight, contentHeight) {
854     switch (verticalAlign) {
855     case SC.ALIGN_BOTTOM:
856       return 0 - (containerHeight - contentHeight);
857     case SC.ALIGN_MIDDLE:
858       return 0 - ((containerHeight - contentHeight) / 2);
859     default: // SC.ALIGN_TOP
860       return 0;
861     }
862   },
863 
864   /** @private Manually animates the content view. */
865   _sc_animateContentView: function (contentAdjustMap) {
866     var easingCurve = this._sc_animationTiming,
867       totalDuration = this._sc_animationDuration * 1000,
868       start = new Date(),
869       contentView = this.get('contentView'),
870       contentViewLayout = contentView.get('layout'),
871       leftStart = contentViewLayout.left,
872       leftDelta = contentAdjustMap.left - leftStart,
873       scaleStart = contentViewLayout.scale == null ? 1 : contentViewLayout.scale,
874       scaleDelta = contentAdjustMap.scale - scaleStart,
875       topStart = contentViewLayout.top,
876       topDelta = contentAdjustMap.top - topStart,
877       self = this;
878 
879     function animationFrame() {
880       if (self._sc_isAnimating) {
881         var duration = new Date() - start,
882             percent = Math.min(duration / totalDuration, 1); // Capped at 100%
883 
884         SC.run(function () {
885           var currentLeft = leftStart + leftDelta * easingCurve.value(percent),
886               currentScale = scaleStart + scaleDelta * easingCurve.value(percent),
887               currentTop = topStart + topDelta * easingCurve.value(percent);
888 
889           contentAdjustMap.left = currentLeft;
890           contentAdjustMap.top = currentTop;
891           contentAdjustMap.scale = currentScale;
892 
893           contentView.adjust(contentAdjustMap);
894         });
895 
896         // Keep animating as long as we haven't hit 100%.
897         if (percent < 1) {
898           window.requestAnimationFrame(animationFrame);
899         } else {
900           // Clear out the animation flags.
901           self._sc_isAnimating = false;
902           self._sc_animationDuration = null;
903           self._sc_animationTiming = null;
904         }
905       }
906     }
907 
908     // Start the animation.
909     self._sc_isAnimating = true;
910     animationFrame();
911   },
912 
913   /* @private Cancels any content view animation if it exists. */
914   _sc_cancelAnimation: function () {
915     if (this._sc_isAnimating) {
916       var contentView = this.get('contentView');
917 
918     // UNUSED. Animate using SC.View.prototype.animate. Cancelling the animation in place proved problematic.
919     // if (contentView.get('viewState') === SC.CoreView.ATTACHED_SHOWN_ANIMATING) {
920     //   // Stop the animation in place.
921     //   contentView.cancelAnimation(SC.LayoutState.CURRENT);
922 
923       var curLayout = contentView.get('layout');
924 
925       // Update offsets to match actual placement.
926       this.set('horizontalScrollOffset', -curLayout.left);
927       this.set('verticalScrollOffset', -curLayout.top);
928       this.set('scale', curLayout.scale);
929 
930       // Clear out the animation flags.
931       this._sc_isAnimating = false;
932       this._sc_animationDuration = null;
933       this._sc_animationTiming = null;
934     }
935 
936   },
937 
938   /** @private Reposition our content view if necessary according to aligment. */
939   _sc_containerViewFrameDidChange: function () {
940     // Run the content view size change code (updates min & max offsets, sets content alignment if necessary, shows scrollers if necessary)
941     var containerFrame = this.getPath('containerView.frame'),
942       contentView = this.get('contentView'),
943       lastMaximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'),
944       lastMaximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'),
945       lastMinimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset'),
946       lastMinimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset');
947 
948     // Cache the current height and width of the container view, so we can only watch for size changes.
949     // This will update the maximum scroll offsets when they are requested.
950     this.set('_sc_containerHeight', containerFrame.height);
951     this.set('_sc_containerWidth', containerFrame.width);
952 
953     if (contentView) {
954       // var didAdjust = false;
955 
956       // if (this._sc_shouldResizeContentHeight) {
957       //   contentView.adjust('height', containerFrame.height);
958       //   didAdjust = true;
959       // }
960 
961       // if (this._sc_shouldResizeContentWidth) {
962       //   contentView.adjust('width', containerFrame.width);
963       //   didAdjust = true;
964       // }
965 
966       // Update the scrollers regardless.
967       // if (!didAdjust) {
968       this._sc_contentViewSizeDidChange(lastMinimumHorizontalScrollOffset, lastMaximumHorizontalScrollOffset, lastMinimumVerticalScrollOffset, lastMaximumVerticalScrollOffset);
969       // }
970     }
971 
972   },
973 
974   /** @private Whenever the contentView of the container changes, set up new observers and clean up old observers. */
975   _sc_contentViewDidChange: function () {
976     var newView = this.get('contentView'), // Our content view.
977       containerView = this.get('containerView'),
978       frameChangeFunc = this._sc_contentViewFrameDidChange;
979 
980     // Clean up observers on the previous content view.
981     this._sc_removeContentViewObservers();
982 
983     // Reset caches.
984     // this._sc_shouldResizeContentWidth = false;
985     // this._sc_shouldResizeContentHeight = false;
986     this._sc_contentHeight = 0;
987     this._sc_contentWidth = 0;
988     this._sc_contentScale = undefined;
989 
990     // Assign the content view to our container view. This ensures that it is instantiated.
991     containerView.set('contentView', newView);
992     newView = this.contentView = containerView.get('contentView'); // Actual content view.
993 
994     if (newView) {
995       /* jshint eqnull:true */
996 
997       // Be wary of content views that replace their layers.
998       // newView.addObserver('layer', this, layerChangeFunc);
999 
1000       if (!newView.useStaticLayout) {
1001         // When a view wants an accelerated layer and isn't a fixed size, we convert it to a fixed
1002         // size and resize it when our container resizes.
1003         // if (newView.get('wantsAcceleratedLayer') && !newView.get('isFixedSize')) {
1004         //   var contentViewLayout = newView.get('layout');
1005 
1006         //   // Fix the width.
1007         //   if (contentViewLayout.width == null) {
1008         //     this._sc_shouldResizeContentWidth = true; // Flag to indicate that when the container's width changes, we should update the content's width.
1009 
1010         //     newView.adjust({
1011         //       right: null,
1012         //       width: this._sc_containerWidth
1013         //     });
1014         //   }
1015 
1016         //   // Fix the height.
1017         //   if (contentViewLayout.height == null) {
1018         //     this._sc_shouldResizeContentHeight = true; // Flag to indicate that when the container's height changes, we should update the content's height.
1019 
1020         //     newView.adjust({
1021         //       bottom: null,
1022         //       height: this._sc_containerHeight
1023         //     });
1024         //   }
1025         // }
1026       }
1027 
1028       // TODO: Can we remove this if a calculated property exists?
1029       newView.addObserver('frame', this, frameChangeFunc);
1030 
1031       // Initialize once.
1032       this._sc_contentViewFrameDidChange();
1033     }
1034 
1035     // Cache the current content view so that we can properly clean up when it changes.
1036     this._sc_contentView = newView;
1037   },
1038 
1039   /** @private */
1040   // _sc_contentViewLayerDidChange: function () {
1041   //   ???
1042   // },
1043 
1044   /** @private Check frame changes for size changes. */
1045   _sc_contentViewFrameDidChange: function () {
1046     var lastHeight = this._sc_contentHeight,
1047         lastScale = this._sc_contentScale,
1048         lastWidth = this._sc_contentWidth,
1049         newFrame = this.getPath('contentView.borderFrame'),
1050         lastMaximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'),
1051         lastMaximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'),
1052         lastMinimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset'),
1053         lastMinimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset');
1054 
1055     if (newFrame) {
1056       // Determine whether the scale has changed.
1057       if (lastScale !== newFrame.scale) {
1058         this._sc_contentScaleDidChange = true;
1059         this.set('_sc_contentScale', newFrame.scale);
1060       }
1061 
1062       if (lastWidth !== newFrame.width) {
1063         this._sc_contentWidthDidChange = true;
1064         this.set('_sc_contentWidth', newFrame.width);
1065       }
1066 
1067       if (lastHeight !== newFrame.height) {
1068         this._sc_contentHeightDidChange = true;
1069         this.set('_sc_contentHeight', newFrame.height);
1070       }
1071 
1072       // If any of the size values changed, update.
1073       if (this._sc_contentScaleDidChange || this._sc_contentWidthDidChange || this._sc_contentHeightDidChange) {
1074         this._sc_contentViewSizeDidChange(lastMinimumHorizontalScrollOffset, lastMaximumHorizontalScrollOffset, lastMinimumVerticalScrollOffset, lastMaximumVerticalScrollOffset);
1075       }
1076     }
1077   },
1078 
1079   /** @private When the content view's size changes, we need to update our scroll offset properties. */
1080   _sc_contentViewSizeDidChange: function (lastMinimumHorizontalScrollOffset, lastMaximumHorizontalScrollOffset, lastMinimumVerticalScrollOffset, lastMaximumVerticalScrollOffset) {
1081     var maximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'),
1082       maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'),
1083       containerHeight, containerWidth,
1084       contentHeight, contentWidth;
1085 
1086     containerHeight = this._sc_containerHeight;
1087     containerWidth = this._sc_containerWidth;
1088     contentHeight = this._sc_contentHeight;
1089     contentWidth = this._sc_contentWidth;
1090 
1091     var value;
1092     if (contentWidth) {
1093       if (maximumHorizontalScrollOffset === 0) {
1094         // Align horizontally.
1095         value = this._sc_alignedHorizontalOffset(this.get('horizontalAlign'), containerWidth, contentWidth);
1096         this.set('horizontalScrollOffset', value); // Note: Trigger for _sc_scrollOffsetHorizontalDidChange
1097 
1098       } else {
1099         /* jshint eqnull:true */
1100         // If the horizontal position has never been set, use the initial alignment.
1101         if (this._sc_horizontalPct == null) {
1102           this._sc_horizontalScrollOffset = null;
1103           this.notifyPropertyChange('horizontalScrollOffset');
1104 
1105         // If the scale of the content view changes, we want to maintain relative position so that zooming remains centered.
1106         } else if (this._sc_contentScaleDidChange) {
1107           if (this._sc_touchCenterX != null) {
1108             value = (this._sc_horizontalPct * contentWidth) - this._sc_touchCenterX;
1109           } else {
1110             value = (this._sc_horizontalPct * contentWidth) - (containerWidth / 2);
1111           }
1112           this.set('horizontalScrollOffset', value); // Note: Trigger for _sc_scrollOffsetHorizontalDidChange
1113 
1114           // Live scale gesture. Update the anchor so that the scroll deltas are calculated correctly.
1115           if (this._sc_gestureAnchorHOffset != null) {
1116             this._sc_gestureAnchorHOffset = value;
1117           }
1118         } else if (this.get('canScrollHorizontal')) {
1119           // Take alignment into account.
1120           var horizontalAlign = this.get('horizontalAlign'),
1121               horizontalScrollOffset = this._sc_horizontalScrollOffset,
1122               minimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset');
1123 
1124           switch (horizontalAlign) {
1125           case SC.ALIGN_CENTER:
1126             // Switched to scrolling horizontally, stick to center OR was scrolled at center and size changed.
1127             if ((lastMinimumHorizontalScrollOffset < 0 && minimumHorizontalScrollOffset === 0) ||
1128               (horizontalScrollOffset === lastMaximumHorizontalScrollOffset / 2)) {
1129               this.set('horizontalScrollOffset', maximumHorizontalScrollOffset / 2);
1130             }
1131 
1132             break;
1133           case SC.ALIGN_RIGHT:
1134             // Switched to scrolling horizontally, stick to right side OR was scrolled to right and size changed.
1135             if ((lastMinimumHorizontalScrollOffset < 0 && minimumHorizontalScrollOffset === 0) ||
1136               (horizontalScrollOffset === lastMaximumHorizontalScrollOffset)) {
1137               this.set('horizontalScrollOffset', maximumHorizontalScrollOffset);
1138             }
1139 
1140             break;
1141           }
1142 
1143           // Was at right side and size shrunk.
1144           if (horizontalScrollOffset > maximumHorizontalScrollOffset) {
1145             this.set('horizontalScrollOffset', maximumHorizontalScrollOffset);
1146           }
1147         }
1148       }
1149     }
1150 
1151     if (contentHeight) {
1152       if (maximumVerticalScrollOffset === 0) {
1153         // Align vertically.
1154         value = this._sc_alignedVerticalOffset(this.get('verticalAlign'), containerHeight, contentHeight);
1155         this.set('verticalScrollOffset', value); // Note: Trigger for _sc_scrollOffsetHorizontalDidChange
1156 
1157       } else {
1158         /* jshint eqnull:true */
1159         // If the vertical position has never been set, use the initial alignment.
1160         if (this._sc_verticalPct == null) {
1161           this._sc_verticalScrollOffset = null;
1162           this.notifyPropertyChange('verticalScrollOffset');
1163 
1164         // If the scale of the content view changes, we want to maintain relative position so that zooming remains centered.
1165         } else if (this._sc_contentScaleDidChange) {
1166           if (this._sc_touchCenterY != null) {
1167             value = (this._sc_verticalPct * contentHeight) - this._sc_touchCenterY;
1168           } else {
1169             value = (this._sc_verticalPct * contentHeight) - (containerHeight / 2);
1170           }
1171           this.set('verticalScrollOffset', value); // Note: Trigger for _sc_scrollOffsetVerticalDidChange
1172 
1173           // Live scale gesture. Update the anchor so that the scroll deltas are calculated correctly.
1174           if (this._sc_gestureAnchorVOffset != null) {
1175             this._sc_gestureAnchorVOffset = value;
1176           }
1177         } else if (this.get('canScrollVertical')) {
1178           var verticalAlign = this.get('verticalAlign'),
1179               verticalScrollOffset = this._sc_verticalScrollOffset,
1180               minimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset');
1181 
1182           switch (verticalAlign) {
1183           case SC.ALIGN_MIDDLE:
1184             // Switched to scrolling vertically, stick to middle OR was scrolled at middle and size changed.
1185             if ((lastMinimumVerticalScrollOffset < 0 && minimumVerticalScrollOffset === 0) ||
1186               (verticalScrollOffset === lastMaximumVerticalScrollOffset / 2)) {
1187               this.set('verticalScrollOffset', maximumVerticalScrollOffset / 2);
1188             }
1189 
1190             break;
1191           case SC.ALIGN_BOTTOM:
1192             // Switched to scrolling vertically, stick to bottom side OR was scrolled to bottom and size changed.
1193             if ((lastMinimumVerticalScrollOffset < 0 && minimumVerticalScrollOffset === 0) ||
1194               (verticalScrollOffset === lastMaximumVerticalScrollOffset)) {
1195               this.set('verticalScrollOffset', maximumVerticalScrollOffset);
1196             }
1197 
1198             break;
1199           }
1200 
1201           // Was at bottom side and size shrunk.
1202           if (verticalScrollOffset > maximumVerticalScrollOffset) {
1203             this.set('verticalScrollOffset', maximumVerticalScrollOffset);
1204           }
1205         }
1206       }
1207     }
1208 
1209     // Reset our flags.
1210     this._sc_contentScaleDidChange = false;
1211     this._sc_contentHeightDidChange = false;
1212     this._sc_contentWidthDidChange = false;
1213 
1214     // TODO: Updating scrollers may change the container size, which will cause this to run again. Can we bring
1215     // this into a single call?
1216     this._sc_updateScrollers();
1217   },
1218 
1219   /** @private Fade in the horizontal scroller. Each scroller fades in/out independently. */
1220   _sc_fadeInHorizontalScroller: function () {
1221     var canScrollHorizontal = this.get('canScrollHorizontal'),
1222       horizontalScroller = this.get('horizontalScrollerView'),
1223       delay;
1224 
1225     if (canScrollHorizontal && horizontalScroller && horizontalScroller.get('fadeIn')) {
1226       if (this._sc_horizontalFadeOutTimer) {
1227         // Reschedule the current timer (avoid creating a new instance).
1228         this._sc_horizontalFadeOutTimer.startTime = null;
1229         this._sc_horizontalFadeOutTimer.schedule();
1230       } else {
1231         // Fade in.
1232         horizontalScroller.fadeIn();
1233 
1234         // Wait the minimum time before fading out again.
1235         delay = this.get('_sc_minimumFadeOutDelay');
1236         this._sc_horizontalFadeOutTimer = this.invokeLater(this._sc_fadeOutHorizontalScroller, delay);
1237       }
1238     }
1239   },
1240 
1241   /** @private Fade in the vertical scroller. Each scroller fades in/out independently. */
1242   _sc_fadeInVerticalScroller: function () {
1243     var canScrollVertical = this.get('canScrollVertical'),
1244       verticalScroller = this.get('verticalScrollerView'),
1245       delay;
1246 
1247     if (canScrollVertical && verticalScroller && verticalScroller.get('fadeIn')) {
1248       if (this._sc_verticalFadeOutTimer) {
1249         // Reschedule the current timer (avoid creating a new instance).
1250         this._sc_verticalFadeOutTimer.startTime = null;
1251         this._sc_verticalFadeOutTimer.schedule();
1252 
1253       } else {
1254         // Fade in.
1255         verticalScroller.fadeIn();
1256 
1257         // Wait the minimum time before fading out again.
1258         delay = this.get('_sc_minimumFadeOutDelay');
1259         this._sc_verticalFadeOutTimer = this.invokeLater(this._sc_fadeOutVerticalScroller, delay);
1260       }
1261     }
1262   },
1263 
1264   /** @private Fade out the horizontal scroller. */
1265   _sc_fadeOutHorizontalScroller: function () {
1266     var horizontalScroller = this.get('horizontalScrollerView');
1267 
1268     if (horizontalScroller && horizontalScroller.get('fadeOut')) {
1269       // Fade out.
1270       horizontalScroller.fadeOut();
1271     }
1272 
1273     this._sc_horizontalFadeOutTimer = null;
1274   },
1275 
1276   /** @private Fade out the vertical scroller. */
1277   _sc_fadeOutVerticalScroller: function () {
1278     var verticalScroller = this.get('verticalScrollerView');
1279 
1280     if (verticalScroller && verticalScroller.get('fadeOut')) {
1281       // Fade out.
1282       verticalScroller.fadeOut();
1283     }
1284 
1285     this._sc_verticalFadeOutTimer = null;
1286   },
1287 
1288   /** @private Adjust the content alignment horizontally on change. */
1289   _sc_horizontalAlignDidChange: function () {
1290     var maximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset');
1291 
1292     // Align horizontally (Unless content width is zero).
1293     if (maximumHorizontalScrollOffset === 0 && this._sc_contentWidth) {
1294       var horizontalAlign = this.get('horizontalAlign'),
1295           value;
1296 
1297       value = this._sc_alignedHorizontalOffset(horizontalAlign, this._sc_containerWidth, this._sc_contentWidth);
1298       this.set('horizontalScrollOffset', value);
1299     }
1300   },
1301 
1302   /** @private
1303     Calculates the maximum offset given content and container sizes, and the
1304     alignment.
1305   */
1306   _sc_maximumScrollOffset: function (contentSize, containerSize, align, canScroll) {
1307     // If we can't scroll, we pretend our content size is no larger than the container.
1308     if (canScroll === NO) contentSize = Math.min(contentSize, containerSize);
1309 
1310     // if our content size is larger than or the same size as the container, it's quite
1311     // simple to calculate the answer. Otherwise, we need to do some fancy-pants
1312     // alignment logic (read: simple math)
1313     if (contentSize >= containerSize) return contentSize - containerSize;
1314 
1315     // alignment, yeah
1316     if (align === SC.ALIGN_LEFT || align === SC.ALIGN_TOP) {
1317       // if we left-align something, and it is smaller than the view, does that not mean
1318       // that it's maximum (and minimum) offset is 0, because it should be positioned at 0?
1319       return 0;
1320     } else if (align === SC.ALIGN_MIDDLE || align === SC.ALIGN_CENTER) {
1321       // middle align means the difference divided by two, because we want equal parts on each side.
1322       return 0 - Math.round((containerSize - contentSize) / 2);
1323     } else {
1324       // right align means the entire difference, because we want all that space on the left
1325       return 0 - (containerSize - contentSize);
1326     }
1327   },
1328 
1329   /** @private
1330     Calculates the minimum offset given content and container sizes, and the
1331     alignment.
1332   */
1333   _sc_minimumScrollOffset: function (contentSize, containerSize, align, canScroll) {
1334     // If we can't scroll, we pretend our content size is no larger than the container.
1335     if (canScroll === NO) contentSize = Math.min(contentSize, containerSize);
1336 
1337     // if the content is larger than the container, we have no need to change the minimum
1338     // away from the natural 0 position.
1339     if (contentSize > containerSize) return 0;
1340 
1341     // alignment, yeah
1342     if (align === SC.ALIGN_LEFT || align === SC.ALIGN_TOP) {
1343       // if we left-align something, and it is smaller than the view, does that not mean
1344       // that it's maximum (and minimum) offset is 0, because it should be positioned at 0?
1345       return 0;
1346     } else if (align === SC.ALIGN_MIDDLE || align === SC.ALIGN_CENTER) {
1347       // middle align means the difference divided by two, because we want equal parts on each side.
1348       return 0 - Math.round((containerSize - contentSize) / 2);
1349     } else {
1350       // right align means the entire difference, because we want all that space on the left
1351       return 0 - (containerSize - contentSize);
1352     }
1353   },
1354 
1355   /** @private Registers/deregisters view with SC.Drag for autoscrolling. */
1356   _sc_registerAutoscroll: function () {
1357     if (this.get('isVisibleInWindow') && this.get('isEnabledInPane')) {
1358       SC.Drag.addScrollableView(this);
1359     } else {
1360       SC.Drag.removeScrollableView(this);
1361     }
1362   },
1363 
1364   /** @private Reposition the content view to match the current scroll offsets and scale. */
1365   _sc_repositionContentView: function () {
1366     var contentView = this.get('contentView');
1367 
1368     if (contentView) {
1369       this.invokeOnce(this._sc_repositionContentViewUnfiltered);
1370     }
1371   },
1372 
1373   /** @private Reposition the content view to match the current scroll offsets and scale. */
1374   _sc_repositionContentViewUnfiltered: function () {
1375     var containerView = this.get('containerView'),
1376         contentView = this.get('contentView'),
1377         horizontalScrollOffset = this.get('horizontalScrollOffset'),
1378         verticalScrollOffset = this.get('verticalScrollOffset'),
1379         scale = this.get('scale');
1380 
1381     // If the content is statically laid out, use margins in the container layer to move it.
1382     // TODO: Remove static layout support.
1383     if (contentView.useStaticLayout) {
1384       var containerViewLayer = containerView.get('layer');
1385 
1386       // Set the margins on the layer.
1387       containerViewLayer.style.marginLeft = -horizontalScrollOffset + 'px';
1388       containerViewLayer.style.marginTop = -verticalScrollOffset + 'px';
1389 
1390     // Otherwise call adjust on the content.
1391     } else {
1392       // Constrain the offsets to full (actual) pixels to prevent blurry text et cetera. Note that this assumes
1393       // that the scroll view itself is living at a scale of 1, and may give blurry subpixel results if scaled.
1394       var horizontalAlign = this.get('horizontalAlign'),
1395         verticalAlign = this.get('verticalAlign'),
1396         left, top;
1397 
1398       left = -horizontalScrollOffset;
1399 
1400       // Round according to the alignment to avoid jitter at the edges. For example, we don't want 0.55 rounding up to 1 when left aligned. This also prevents implied percentage values (i.e. 0.0 > value > 1.0 == %)!
1401       switch (horizontalAlign) {
1402       case SC.ALIGN_CENTER:
1403         left = Math.round(left);
1404         break;
1405       case SC.ALIGN_RIGHT:
1406         left = Math.ceil(left);
1407         break;
1408       default: // SC.ALIGN_LEFT
1409         left = Math.floor(left);
1410       }
1411 
1412       top = -verticalScrollOffset;
1413 
1414       switch (verticalAlign) {
1415       case SC.ALIGN_MIDDLE:
1416         top = Math.round(top);
1417         break;
1418       case SC.ALIGN_BOTTOM:
1419         top = Math.ceil(top);
1420         break;
1421       default: // SC.ALIGN_TOP
1422         top = Math.floor(top);
1423       }
1424 
1425       // Cancel any active animation in place.
1426       // this._sc_cancelAnimation();
1427 
1428       var contentAdjustMap = SC.ScrollView._SC_CONTENT_ADJUST_MAP; // Shared object used to avoid continually initializing/destroying objects.
1429 
1430       // Create the content adjust map once. Note: This is a shared object, all properties must be overwritten each time.
1431       if (!contentAdjustMap) {
1432         contentAdjustMap = SC.ScrollView._SC_CONTENT_ADJUST_MAP = {
1433           // Ensure that scale transforms occur from the top-left corner (per our math).
1434           transformOriginX: 0,
1435           transformOriginY: 0
1436         };
1437       }
1438 
1439       contentAdjustMap.left = left;
1440       contentAdjustMap.top = top;
1441       contentAdjustMap.scale = scale;
1442 
1443       if (this._sc_animationDuration) {
1444         // UNUSED. Animate using SC.View.prototype.animate. Cancelling the animation in place proved problematic.
1445         // contentView.animate({ left: left, top: top, scale: scale }, {
1446         //   duration: this._sc_animationDuration,
1447         //   timing: this._sc_animationTiming
1448         // });
1449 
1450         // // Run the animation immediately (don't wait for next Run Loop).
1451         // // Note: The next run loop will be queued none-the-less, so we may want to avoid that entirely in the future.
1452         // contentView._animate();
1453         this._sc_animateContentView(contentAdjustMap);
1454 
1455       } else {
1456         contentView.adjust(contentAdjustMap);
1457       }
1458     }
1459   },
1460 
1461   /** @private Re-position the scrollers and content depending on the need to scroll or not. */
1462   _sc_repositionScrollers: function () {
1463     this.invokeOnce(this._sc_repositionScrollersUnfiltered);
1464   },
1465 
1466   /** @private Re-position the scrollers and content depending on the need to scroll or not. */
1467   _sc_repositionScrollersUnfiltered: function () {
1468     var hasHorizontalScroller = this.get('hasHorizontalScroller'),
1469       hasVerticalScroller = this.get('hasVerticalScroller'),
1470       canScrollHorizontal = this.get('canScrollHorizontal'),
1471       canScrollVertical = this.get('canScrollVertical'),
1472       containerLayoutMap = SC.ScrollView._SC_CONTAINER_LAYOUT_MAP, // Shared object used to avoid continually initializing/destroying objects.
1473       horizontalScrollerView = this.get('horizontalScrollerView'),
1474       horizontalScrollerHeight = horizontalScrollerView && canScrollHorizontal ? horizontalScrollerView.get('scrollbarThickness') : 0,
1475       verticalScrollerView = this.get('verticalScrollerView'),
1476       verticalScrollerWidth = verticalScrollerView && canScrollVertical ? verticalScrollerView.get('scrollbarThickness') : 0,
1477       layout; // The new layout to be applied to each scroller.
1478 
1479     // Create the container layout map once. Note: This is a shared object, all properties must be overwritten each time.
1480     if (!containerLayoutMap) { containerLayoutMap = SC.ScrollView._SC_CONTAINER_LAYOUT_MAP = {}; }
1481 
1482     // Set the standard.
1483     containerLayoutMap.bottom = 0;
1484     containerLayoutMap.right = 0;
1485 
1486     // Adjust the horizontal scroller.
1487     if (hasHorizontalScroller) {
1488       var horizontalOverlay = this.get('horizontalOverlay'),
1489         horizontalScrollerLayout = this.get('horizontalScrollerLayout');
1490 
1491       // Adjust the scroller view accordingly. Allow for a custom default scroller layout to be set.
1492       if (horizontalScrollerLayout) {
1493         layout = {
1494           left: horizontalScrollerLayout.left,
1495           bottom: horizontalScrollerLayout.bottom,
1496           right: horizontalScrollerLayout.right + verticalScrollerWidth,
1497           height: horizontalScrollerHeight
1498         };
1499       } else {
1500         layout = {
1501           left: 0,
1502           bottom: 0,
1503           right: verticalScrollerWidth,
1504           height: horizontalScrollerHeight
1505         };
1506       }
1507       horizontalScrollerView.set('layout', layout);
1508 
1509       // Adjust the content view accordingly.
1510       if (canScrollHorizontal && !horizontalOverlay) {
1511         containerLayoutMap.bottom = horizontalScrollerHeight;
1512       }
1513 
1514       // Set the visibility of the scroller immediately.
1515       horizontalScrollerView.set('isVisible', canScrollHorizontal);
1516       this._sc_fadeInHorizontalScroller();
1517     }
1518 
1519     // Adjust the vertical scroller.
1520     if (hasVerticalScroller) {
1521       var verticalOverlay = this.get('verticalOverlay'),
1522         verticalScrollerLayout = this.get('verticalScrollerLayout');
1523 
1524       // Adjust the scroller view accordingly. Allow for a custom default scroller layout to be set.
1525       if (verticalScrollerLayout) {
1526         layout = {
1527           top: verticalScrollerLayout.top,
1528           right: verticalScrollerLayout.right,
1529           bottom: verticalScrollerLayout.bottom + horizontalScrollerHeight,
1530           width: verticalScrollerWidth
1531         };
1532       } else {
1533         layout = {
1534           top: 0,
1535           right: 0,
1536           bottom: horizontalScrollerHeight,
1537           width: verticalScrollerWidth
1538         };
1539       }
1540       verticalScrollerView.set('layout', layout);
1541 
1542       // Prepare to adjust the content view accordingly.
1543       if (canScrollVertical && !verticalOverlay) {
1544         containerLayoutMap.right = verticalScrollerWidth;
1545       }
1546 
1547       // Set the visibility of the scroller immediately.
1548       verticalScrollerView.set('isVisible', canScrollVertical);
1549       this._sc_fadeInVerticalScroller();
1550     }
1551 
1552     // Adjust the container view accordingly (i.e. to make space for scrollers or take space back).
1553     var containerView = this.get('containerView');
1554     containerView.adjust(containerLayoutMap);
1555   },
1556 
1557   /** @private Clean up observers on the content view. */
1558   _sc_removeContentViewObservers: function () {
1559     var oldView = this._sc_contentView,
1560       frameChangeFunc = this._sc_contentViewFrameDidChange;
1561       // layerChangeFunc = this._sc_contentViewLayerDidChange;
1562 
1563     if (oldView) {
1564       oldView.removeObserver('frame', this, frameChangeFunc);
1565       // oldView.removeObserver('layer', this, layerChangeFunc);
1566 
1567       // this._sc_shouldResizeContentWidth = false;
1568       // this._sc_shouldResizeContentHeight = false;
1569     }
1570   },
1571 
1572   /** @private Whenever the scale changes, update the scrollers and adjust the location of the content view. */
1573   _sc_scaleDidChange: function () {
1574     var contentView = this.get('contentView'),
1575       scale = this.get('scale');
1576 
1577     // If the content is statically laid out, use margins in the container layer to move it.
1578     // TODO: Remove static layout support.
1579     if (contentView) {
1580       if (contentView.useStaticLayout) {
1581         //@if(debug)
1582         // If the scale is not 1 then assume the developer is trying to scale static content.
1583         if (scale !== 1) {
1584           SC.warn("Developer Warning: SC.ScrollView's `scale` feature does not support statically laid out content views.");
1585         }
1586         //@endif
1587 
1588       // Reposition the content view to apply the scale.
1589       } else {
1590         // Apply this change.
1591         this._sc_repositionContentView();
1592       }
1593     }
1594   },
1595 
1596   /** @private Whenever the scroll offsets change, update the scrollers and adjust the location of the content view. */
1597   _sc_scrollOffsetHorizontalDidChange: function () {
1598     this._sc_repositionContentView();
1599     this.invokeLast(this._sc_fadeInHorizontalScroller);
1600   },
1601 
1602   /** @private Whenever the scroll offsets change, update the scrollers and adjust the location of the content view. */
1603   _sc_scrollOffsetVerticalDidChange: function () {
1604     this._sc_repositionContentView();
1605     this.invokeLast(this._sc_fadeInVerticalScroller);
1606   },
1607 
1608   /** @private Update the scrollers. */
1609   _sc_updateScrollers: function () {
1610     var horizontalScrollerView = this.get('horizontalScrollerView'),
1611       verticalScrollerView = this.get('verticalScrollerView'),
1612       minimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset'),
1613       minimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset'),
1614       maximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'),
1615       maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'),
1616       containerHeight, containerWidth,
1617       contentHeight, contentWidth;
1618 
1619     containerHeight = this._sc_containerHeight;
1620     containerWidth = this._sc_containerWidth;
1621     contentHeight = this._sc_contentHeight;
1622     contentWidth = this._sc_contentWidth;
1623 
1624     // Update the minimum and maximum scrollable distance on the scrollers as well as their visibility.
1625     var proportion;
1626     if (horizontalScrollerView) {
1627       horizontalScrollerView.set('minimum', minimumHorizontalScrollOffset);
1628       horizontalScrollerView.set('maximum', maximumHorizontalScrollOffset);
1629 
1630       if (this.get('autohidesHorizontalScroller')) {
1631         this.setIfChanged('isHorizontalScrollerVisible', contentWidth > containerWidth);
1632       }
1633 
1634       // Constrain the proportion to 100%.
1635       proportion = Math.min(containerWidth / contentWidth, 1.0);
1636       horizontalScrollerView.setIfChanged('proportion', proportion);
1637     }
1638 
1639     if (verticalScrollerView) {
1640       verticalScrollerView.set('minimum', minimumVerticalScrollOffset);
1641       verticalScrollerView.set('maximum', maximumVerticalScrollOffset);
1642 
1643       if (this.get('autohidesVerticalScroller')) {
1644         this.setIfChanged('isVerticalScrollerVisible', contentHeight > containerHeight);
1645       }
1646 
1647       // Constrain the proportion to 100%.
1648       proportion = Math.min(containerHeight / contentHeight, 1.0);
1649       verticalScrollerView.setIfChanged('proportion', proportion);
1650     }
1651   },
1652 
1653   /** @private Adjust the content alignment vertically on change. */
1654   _sc_verticalAlignDidChange: function () {
1655     var maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset');
1656 
1657     // Align vertically (Unless content height is zero).
1658     if (maximumVerticalScrollOffset === 0 && this._sc_contentHeight) {
1659       var verticalAlign = this.get('verticalAlign'),
1660         value;
1661 
1662       value = this._sc_alignedVerticalOffset(verticalAlign, this._sc_containerHeight, this._sc_contentHeight);
1663       this.set('verticalScrollOffset', value);
1664     }
1665   },
1666 
1667   /** @private Instantiate the container view and the scrollers as needed. */
1668   createChildViews: function () {
1669     var childViews = [];
1670 
1671     // Set up the container view.
1672     var containerView = this.get('containerView');
1673 
1674     //@if(debug)
1675     // Provide some debug-mode only developer support to prevent problems.
1676     if (!containerView) {
1677       throw new Error("Developer Error: SC.ScrollView must have a containerView class set before it is instantiated.");
1678     }
1679     //@endif
1680 
1681     containerView = this.containerView = this.createChildView(containerView, {
1682       contentView: this.contentView // Initialize the view with the currently set container view.
1683     });
1684     this.contentView = containerView.get('contentView'); // Replace our content view with the instantiated version.
1685     childViews.push(containerView);
1686 
1687     // Set up the scrollers.
1688     var scrollerView;
1689 
1690     // Create a horizontal scroller view if needed.
1691     if (!this.get('hasHorizontalScroller')) {
1692       // Remove the class entirely.
1693       this.horizontalScrollerView = null;
1694     } else {
1695       scrollerView = this.get('horizontalScrollerView');
1696 
1697       // Use a default scroller view.
1698       /* jshint eqnull:true */
1699       if (scrollerView == null) {
1700         scrollerView = this.get('horizontalOverlay') ? SC.OverlayScrollerView : SC.ScrollerView;
1701       }
1702 
1703       // Replace the class property with an instance.
1704       scrollerView = this.horizontalScrollerView = this.createChildView(scrollerView, {
1705         isVisible: !this.get('autohidesHorizontalScroller'),
1706         layoutDirection: SC.LAYOUT_HORIZONTAL,
1707         value: this.get('horizontalScrollOffset'),
1708         valueBinding: '.parentView.horizontalScrollOffset', // Bind the value of the scroller to our horizontal offset.
1709         minimum: this.get('minimumHorizontalScrollOffset'),
1710         maximum: this.get('maximumHorizontalScrollOffset')
1711       });
1712 
1713       // Add the scroller view to the child views array.
1714       childViews.push(scrollerView);
1715     }
1716 
1717     // Create a vertical scroller view if needed.
1718     if (!this.get('hasVerticalScroller')) {
1719       // Remove the class entirely.
1720       this.verticalScrollerView = null;
1721     } else {
1722       scrollerView = this.get('verticalScrollerView');
1723 
1724       // Use a default scroller view.
1725       /* jshint eqnull:true */
1726       if (scrollerView == null) {
1727         scrollerView = this.get('verticalOverlay') ? SC.OverlayScrollerView : SC.ScrollerView;
1728       }
1729 
1730       // Replace the class property with an instance.
1731       scrollerView = this.verticalScrollerView = this.createChildView(scrollerView, {
1732         isVisible: !this.get('autohidesVerticalScroller'),
1733         layoutDirection: SC.LAYOUT_VERTICAL,
1734         value: this.get('verticalScrollOffset'),
1735         valueBinding: '.parentView.verticalScrollOffset', // Bind the value of the scroller to our vertical offset.
1736         minimum: this.get('minimumVerticalScrollOffset'),
1737         maximum: this.get('maximumVerticalScrollOffset')
1738       });
1739 
1740       // Add the scroller view to the child views array.
1741       childViews.push(scrollerView);
1742     }
1743 
1744     // Set the childViews array.
1745     this.childViews = childViews;
1746   },
1747 
1748   /** @private */
1749   destroy: function() {
1750     // Clean up.
1751     this._sc_removeContentViewObservers();
1752     this.removeObserver('contentView', this, this._sc_contentViewDidChange);
1753 
1754     this.removeObserver('horizontalAlign', this, this._sc_horizontalAlignDidChange);
1755     this.removeObserver('verticalAlign', this, this._sc_verticalAlignDidChange);
1756 
1757     sc_super();
1758   },
1759 
1760   /** @private SC.View */
1761   didCreateLayer: function () {
1762     // Observe the scroll offsets for changes and initialize once.
1763     this.addObserver('horizontalScrollOffset', this, this._sc_scrollOffsetHorizontalDidChange);
1764     this.addObserver('verticalScrollOffset', this, this._sc_scrollOffsetVerticalDidChange);
1765     this._sc_scrollOffsetHorizontalDidChange();
1766     this._sc_scrollOffsetVerticalDidChange();
1767 
1768     // Observe the scroller visibility properties for changes and initialize once.
1769     this.addObserver('isHorizontalScrollerVisible', this, this._sc_repositionScrollers);
1770     this.addObserver('isVerticalScrollerVisible', this, this._sc_repositionScrollers);
1771     this._sc_repositionScrollers();
1772 
1773     // Observe the scale for changes and initialize once.
1774     this.addObserver('scale', this, this._sc_scaleDidChange);
1775     this._sc_scaleDidChange();
1776 
1777     // Observe our container view frame for changes and initialize once.
1778     var containerView = this.get('containerView');
1779     containerView.addObserver('frame', this, this._sc_containerViewFrameDidChange);
1780     this._sc_containerViewFrameDidChange();
1781 
1782     // Observe for changes in enablement and visibility for registering with SC.Drag auto-scrolling and initialize once.
1783     this.addObserver('isVisibleInWindow', this, this._sc_registerAutoscroll);
1784     this.addObserver('isEnabledInPane', this, this._sc_registerAutoscroll);
1785     this._sc_registerAutoscroll();
1786   },
1787 
1788   /** SC.Object.prototype */
1789   init: function () {
1790     sc_super();
1791 
1792     // Observe the content view for changes and initialize once.
1793     this.addObserver('contentView', this, this._sc_contentViewDidChange);
1794     this._sc_contentViewDidChange();
1795 
1796     // Observe the alignment properties for changes. No need to initialize, the default alignment property
1797     // will be used.
1798     this.addObserver('horizontalAlign', this, this._sc_horizontalAlignDidChange);
1799     this.addObserver('verticalAlign', this, this._sc_verticalAlignDidChange);
1800   },
1801 
1802   /**
1803     Scrolls the receiver in the horizontal and vertical directions by the amount specified, if
1804     allowed.  The actual scroll amount will be constrained by the current scroll minimums and
1805     maximums. (If you wish to scroll outside of those bounds, you should call `scrollTo` directly.)
1806 
1807     If you only want to scroll in one direction, pass null or 0 for the other direction.
1808 
1809     @param {Number} x change in the x direction (or hash)
1810     @param {Number} y change in the y direction
1811     @returns {SC.ScrollView} receiver
1812   */
1813   scrollBy: function (x, y) {
1814     // Normalize (deprecated).
1815     if (y === undefined && SC.typeOf(x) === SC.T_HASH) {
1816       //@if(debug)
1817       // Add some developer support. It's faster to pass the arguments individually so that we don't need to do this normalization and the
1818       // developer isn't creating an extra Object needlessly.
1819       SC.warn("Developer Warning: Passing an object to SC.ScrollView.scrollBy is deprecated. Please pass both the x and y arguments.");
1820       //@endif
1821 
1822       y = x.y;
1823       x = x.x;
1824     }
1825 
1826     // If null, undefined, or 0, pass null; otherwise just add current offset.
1827     x = (x) ? this.get('horizontalScrollOffset') + x : null;
1828     y = (y) ? this.get('verticalScrollOffset') + y : null;
1829 
1830     // Constrain within min and max. (Calls to scrollBy are generally convenience calls that should not have to
1831     // worry about exceeding bounds and making the followup call. Features that want to allow overscroll should call
1832     // scrollTo directly.)
1833     if (x !== null) {
1834       x = Math.min(Math.max(this.get('minimumHorizontalScrollOffset'), x), this.get('maximumHorizontalScrollOffset'));
1835     }
1836 
1837     if (y !== null) {
1838       y = Math.min(Math.max(this.get('minimumVerticalScrollOffset'), y), this.get('maximumVerticalScrollOffset'));
1839     }
1840 
1841     return this.scrollTo(x, y);
1842   },
1843 
1844   /**
1845     Scrolls the receiver down one or more lines if allowed.  If number of
1846     lines is not specified, scrolls one line.
1847 
1848     @param {Number} lines number of lines
1849     @returns {SC.ScrollView} receiver
1850   */
1851   scrollDownLine: function (lines) {
1852     if (lines === undefined) lines = 1;
1853     return this.scrollBy(null, this.get('verticalLineScroll') * lines);
1854   },
1855 
1856   /**
1857     Scrolls the receiver down one or more page if allowed.  If number of
1858     pages is not specified, scrolls one page.  The page size is determined by
1859     the verticalPageScroll value.  By default this is the size of the current
1860     scrollable area.
1861 
1862     @param {Number} pages number of lines
1863     @returns {SC.ScrollView} receiver
1864   */
1865   scrollDownPage: function (pages) {
1866     if (pages === undefined) pages = 1;
1867     return this.scrollBy(null, this.get('verticalPageScroll') * pages);
1868   },
1869 
1870   /**
1871     Scrolls the receiver left one or more lines if allowed.  If number of
1872     lines is not specified, scrolls one line.
1873 
1874     @param {Number} lines number of lines
1875     @returns {SC.ScrollView} receiver
1876   */
1877   scrollLeftLine: function (lines) {
1878     if (lines === undefined) lines = 1;
1879     return this.scrollTo(0 - this.get('horizontalLineScroll') * lines, null);
1880   },
1881 
1882   /**
1883     Scrolls the receiver left one or more page if allowed.  If number of
1884     pages is not specified, scrolls one page.  The page size is determined by
1885     the verticalPageScroll value.  By default this is the size of the current
1886     scrollable area.
1887 
1888     @param {Number} pages number of lines
1889     @returns {SC.ScrollView} receiver
1890   */
1891   scrollLeftPage: function (pages) {
1892     if (pages === undefined) pages = 1;
1893     return this.scrollBy(0 - (this.get('horizontalPageScroll') * pages), null);
1894   },
1895 
1896   /**
1897     Scrolls the receiver right one or more lines if allowed.  If number of
1898     lines is not specified, scrolls one line.
1899 
1900     @param {Number} lines number of lines
1901     @returns {SC.ScrollView} receiver
1902   */
1903   scrollRightLine: function (lines) {
1904     if (lines === undefined) lines = 1;
1905     return this.scrollTo(this.get('horizontalLineScroll') * lines, null);
1906   },
1907 
1908   /**
1909     Scrolls the receiver right one or more page if allowed.  If number of
1910     pages is not specified, scrolls one page.  The page size is determined by
1911     the verticalPageScroll value.  By default this is the size of the current
1912     scrollable area.
1913 
1914     @param {Number} pages number of lines
1915     @returns {SC.ScrollView} receiver
1916   */
1917   scrollRightPage: function (pages) {
1918     if (pages === undefined) pages = 1;
1919     return this.scrollBy(this.get('horizontalPageScroll') * pages, null);
1920   },
1921 
1922   /**
1923     Scrolls to the specified x,y coordinates.  This should be the offset into the contentView that
1924     you want to appear at the top-left corner of the scroll view.
1925 
1926     This method will contain the actual scroll based on whether the view can scroll in the named
1927     direction and the maximum distance it can scroll.
1928 
1929     If you only want to scroll in one direction, pass null for the other direction.
1930 
1931     @param {Number} x the x scroll location
1932     @param {Number} y the y scroll location
1933     @returns {SC.ScrollView} receiver
1934   */
1935   scrollTo: function (x, y) {
1936     // Normalize (deprecated).
1937     if (y === undefined && SC.typeOf(x) === SC.T_HASH) {
1938       //@if(debug)
1939       // Add some developer support. It's faster to pass the arguments individually so that we don't need to do this normalization and the
1940       // developer isn't creating an extra Object needlessly.
1941       SC.warn("Developer Warning: Passing an object to SC.ScrollView.scrollTo is deprecated. Please pass both the x and y arguments.");
1942       //@endif
1943 
1944       y = x.y;
1945       x = x.x;
1946     }
1947 
1948     if (!SC.none(x)) {
1949       this.set('horizontalScrollOffset', x);
1950     }
1951 
1952     if (!SC.none(y)) {
1953       this.set('verticalScrollOffset', y);
1954     }
1955 
1956     return this;
1957   },
1958 
1959   /**
1960     Scroll to the supplied rectangle.
1961 
1962     If the rectangle is bigger than the viewport, the top-left
1963     will be preferred.
1964 
1965     (Note that if your content is scaled, the rectangle must be
1966     relative to the contentView's scale, not the ScrollView's.)
1967 
1968     @param {Rect} rect Rectangle to which to scroll.
1969     @returns {Boolean} YES if scroll position was changed.
1970   */
1971   scrollToRect: function (rect) {
1972     // find current visible frame.
1973     var vo = SC.cloneRect(this.get('containerView').get('frame')),
1974         origX = this.get('horizontalScrollOffset'),
1975         origY = this.get('verticalScrollOffset'),
1976         scale = this.get('scale');
1977 
1978     vo.x = origX;
1979     vo.y = origY;
1980 
1981     // Scale rect.
1982     if (scale !== 1) {
1983       rect = SC.cloneRect(rect);
1984       rect.x *= scale;
1985       rect.y *= scale;
1986       rect.height *= scale;
1987       rect.width *= scale;
1988     }
1989 
1990     // if bottom edge is not visible, shift origin
1991     vo.y += Math.max(0, SC.maxY(rect) - SC.maxY(vo));
1992     vo.x += Math.max(0, SC.maxX(rect) - SC.maxX(vo));
1993 
1994     // if top edge is not visible, shift origin
1995     vo.y -= Math.max(0, SC.minY(vo) - SC.minY(rect));
1996     vo.x -= Math.max(0, SC.minX(vo) - SC.minX(rect));
1997 
1998     // scroll to that origin.
1999     if ((origX !== vo.x) || (origY !== vo.y)) {
2000       this.scrollTo(vo.x, vo.y);
2001       return YES;
2002     } else {
2003       return NO;
2004     }
2005   },
2006 
2007   /**
2008     Scrolls the receiver up one or more lines if allowed.  If number of
2009     lines is not specified, scrolls one line.
2010 
2011     @param {Number} lines number of lines
2012     @returns {SC.ScrollView} receiver
2013   */
2014   scrollUpLine: function (lines) {
2015     if (lines === undefined) lines = 1;
2016     return this.scrollBy(null, 0 - this.get('verticalLineScroll') * lines);
2017   },
2018 
2019   /**
2020     Scrolls the receiver up one or more page if allowed.  If number of
2021     pages is not specified, scrolls one page.  The page size is determined by
2022     the verticalPageScroll value.  By default this is the size of the current
2023     scrollable area.
2024 
2025     @param {Number} pages number of lines
2026     @returns {SC.ScrollView} receiver
2027   */
2028   scrollUpPage: function (pages) {
2029     if (pages === undefined) pages = 1;
2030     return this.scrollBy(null, 0 - (this.get('verticalPageScroll') * pages));
2031   },
2032 
2033   /**
2034     Scroll the view to make the view's frame visible.  For this to make sense,
2035     the view should be a subview of the contentView.  Otherwise the results
2036     will be undefined.
2037 
2038     @param {SC.View} view view to scroll or null to scroll receiver visible
2039     @returns {Boolean} YES if scroll position was changed
2040   */
2041   scrollToVisible: function (view) {
2042 
2043     // if no view is passed, do default
2044     if (arguments.length === 0) return sc_super();
2045 
2046     var contentView = this.get('contentView');
2047     if (!contentView) return NO; // nothing to do if no contentView.
2048 
2049     // get the frame for the view - should work even for views with static
2050     // layout, assuming it has been added to the screen.
2051     var viewFrame = view.get('borderFrame');
2052     if (!viewFrame) return NO; // nothing to do
2053 
2054     // convert view's frame to an offset from the contentView origin.  This
2055     // will become the new scroll offset after some adjustment.
2056     viewFrame = contentView.convertFrameFromView(viewFrame, view.get('parentView'));
2057 
2058     return this.scrollToRect(viewFrame);
2059   },
2060 
2061   /** @private SC.View */
2062   willDestroyLayer: function () {
2063     // Clean up.
2064     this.removeObserver('horizontalScrollOffset', this, this._sc_scrollOffsetHorizontalDidChange);
2065     this.removeObserver('verticalScrollOffset', this, this._sc_scrollOffsetVerticalDidChange);
2066     this.removeObserver('isHorizontalScrollerVisible', this, this._sc_repositionScrollers);
2067     this.removeObserver('isVerticalScrollerVisible', this, this._sc_repositionScrollers);
2068 
2069     this.removeObserver('scale', this, this._sc_scaleDidChange);
2070 
2071     var containerView = this.get('containerView');
2072     containerView.removeObserver('frame', this, this._sc_containerViewFrameDidChange);
2073 
2074     // Be sure to remove this view as a scrollable view for SC.Drag.
2075     this.removeObserver('isVisibleInWindow', this, this._sc_registerAutoscroll);
2076     this.removeObserver('isEnabledInPane', this, this._sc_registerAutoscroll);
2077     SC.Drag.removeScrollableView(this);
2078   },
2079 
2080   // ---------------------------------------------------------------------------------------------
2081   // Interaction
2082   //
2083 
2084   /** @private
2085     This method gives our descendent views a chance to capture the touch via captureTouch, and subsequently to handle the
2086     touch, via touchStart. If no view elects to do so, control is returned to the scroll view for standard scrolling.
2087   */
2088   _sc_beginTouchesInContent: function (touch) {
2089     // Clean up.
2090     this._sc_passTouchToContentTimer = null;
2091 
2092     // If the touch is not a scroll or scale, see if any of our descendent views want to handle the touch. If not,
2093     // we keep our existing respondership and all is well.
2094     if (!touch.captureTouch(this, true)) {
2095       touch.makeTouchResponder(touch.targetView, true, this);
2096     }
2097   },
2098 
2099   /** @private */
2100   _sc_touchEnded: function (touch, wasCancelled) {
2101     // When the last touch ends, we stop touch scrolling.
2102     var hasTouch = this.get('hasTouch');
2103     if (hasTouch) {
2104       // Update the average distance to center of the touch to include the new touch. This is used to recognize pinch/zoom movement of the touch.
2105       var avgTouch = touch.averagedTouchesForView(this);
2106 
2107       this._sc_gestureAnchorD = this._sc_gestureAnchorTotalD = avgTouch.d;
2108       this._sc_gestureAnchorX = this._sc_gestureAnchorTotalX = avgTouch.x;
2109       this._sc_gestureAnchorY = this._sc_gestureAnchorTotalY = avgTouch.y;
2110 
2111       if (this._sc_containerOffset) {
2112         this._sc_touchCenterX = avgTouch.x - this._sc_containerOffset.x;
2113         this._sc_touchCenterY = avgTouch.y - this._sc_containerOffset.y;
2114       }
2115 
2116     } else {
2117 
2118       // If we were scrolling, continue scrolling at present velocity with deceleration.
2119       if (this._sc_isTouchScrollingV || this._sc_isTouchScrollingH || this._sc_isTouchScaling) {
2120         var decelerationRate = this.get('decelerationRate'),
2121           containerHeight = this._sc_containerHeight,
2122           containerWidth = this._sc_containerWidth,
2123           durationH = 0,
2124           durationV = 0,
2125           c2x, c2y;
2126 
2127         if (this._sc_isTouchScrollingH) {
2128           var horizontalScrollOffset = this.get('horizontalScrollOffset'),
2129             maximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'),
2130             minimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset'),
2131             horizontalVelocity = this._sc_touchVelocityH;
2132 
2133           // Past the maximum.
2134           if (horizontalScrollOffset > maximumHorizontalScrollOffset) {
2135             this.set('horizontalScrollOffset', maximumHorizontalScrollOffset);
2136 
2137             // Moving away from maximum. Change direction.
2138             if (horizontalVelocity < 0.2) {
2139               this._sc_animationTiming = this.get('animationCurveReverse');
2140 
2141             // Stopped or moving back towards maximum. Maintain direction, snap at the end.
2142             } else {
2143               this._sc_animationTiming = this.get('animationCurveSnap');
2144             }
2145 
2146             // 0.8 seconds for a full screen animation (most will be 50% or less of screen)
2147             durationH = 0.8 * (horizontalScrollOffset - maximumHorizontalScrollOffset) / containerWidth;
2148 
2149           // Bounce back from min.
2150           } else if (horizontalScrollOffset < minimumHorizontalScrollOffset) {
2151             this.set('horizontalScrollOffset', minimumHorizontalScrollOffset);
2152 
2153             // Moving away from minimum. Change direction.
2154             if (horizontalVelocity > 0.2) {
2155               this._sc_animationTiming = this.get('animationCurveReverse');
2156 
2157             // Stopped or moving back towards minimum. Maintain direction, snap at the end.
2158             } else {
2159               this._sc_animationTiming = this.get('animationCurveSnap');
2160             }
2161 
2162             // 0.8 seconds for a full screen animation (most will be 50% or less of screen)
2163             durationH = 0.8 * (minimumHorizontalScrollOffset - horizontalScrollOffset) / containerWidth;
2164 
2165           // Slide.
2166           } else {
2167             // Set the final position we should slide to as we decelerate based on last velocity.
2168             horizontalScrollOffset -= (Math.abs(horizontalVelocity) * horizontalVelocity * 1000) / (2 * decelerationRate);
2169 
2170             // Constrain within bounds.
2171             if (horizontalScrollOffset > maximumHorizontalScrollOffset) {
2172               // Generate an animation curve that bounces past the end point.
2173               c2x = (horizontalScrollOffset - maximumHorizontalScrollOffset) / containerWidth;
2174               c2y = 2 * c2x;
2175               this._sc_animationTiming = SC.easingCurve(0.0,0.5,c2x.toFixed(1),c2y.toFixed(1)); // 'cubic-bezier(0.0,0.5,%@,%@)'.fmt(c2x.toFixed(1), c2y.toFixed(1));
2176 
2177               horizontalScrollOffset = maximumHorizontalScrollOffset;
2178 
2179             } else if (horizontalScrollOffset < minimumHorizontalScrollOffset) {
2180               // Generate an animation curve that bounces past the end point.
2181               c2x = (minimumHorizontalScrollOffset - horizontalScrollOffset) / containerWidth;
2182               c2y = 2 * c2x;
2183               this._sc_animationTiming = SC.easingCurve(0.0,0.5,c2x.toFixed(1),c2y.toFixed(1)); // 'cubic-bezier(0.0,0.5,%@,%@)'.fmt(c2x.toFixed(1), c2y.toFixed(1));
2184 
2185               horizontalScrollOffset = minimumHorizontalScrollOffset;
2186 
2187             } else {
2188               this._sc_animationTiming = this.get('animationCurveDecelerate');
2189             }
2190 
2191             this.set('horizontalScrollOffset', horizontalScrollOffset);
2192 
2193             durationH = Math.abs(horizontalVelocity / decelerationRate);
2194           }
2195         }
2196 
2197         if (this._sc_isTouchScrollingV) {
2198           var verticalScrollOffset = this.get('verticalScrollOffset'),
2199             maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'),
2200             minimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset'),
2201             verticalVelocity = this._sc_touchVelocityV;
2202 
2203           // Past the maximum.
2204           if (verticalScrollOffset > maximumVerticalScrollOffset) {
2205             this.set('verticalScrollOffset', maximumVerticalScrollOffset);
2206 
2207             // Moving away from maximum. Change direction.
2208             if (verticalVelocity < 0.2) {
2209               this._sc_animationTiming = this.get('animationCurveReverse');
2210 
2211             // Stopped or moving back towards maximum. Maintain direction, snap at the end.
2212             } else {
2213               this._sc_animationTiming = this.get('animationCurveSnap');
2214             }
2215 
2216             // 0.8 seconds for a full screen animation (most will be 50% or less of screen)
2217             durationV = 0.8 * (verticalScrollOffset - maximumVerticalScrollOffset) / containerHeight;
2218 
2219           // Bounce back from min.
2220           } else if (verticalScrollOffset < minimumVerticalScrollOffset) {
2221             this.set('verticalScrollOffset', minimumVerticalScrollOffset);
2222 
2223             // Moving away from minimum. Change direction.
2224             if (verticalVelocity > 0.2) {
2225               this._sc_animationTiming = this.get('animationCurveReverse');
2226 
2227             // Stopped or moving back towards minimum. Maintain direction, snap at the end.
2228             } else {
2229               this._sc_animationTiming = this.get('animationCurveSnap');
2230             }
2231 
2232             // 0.8 seconds for a full screen animation (most will be 50% or less of screen)
2233             durationV = 0.8 * (minimumVerticalScrollOffset - verticalScrollOffset) / containerHeight;
2234 
2235           // Slide.
2236           } else {
2237             // Set the final position we should slide to as we decelerate based on last velocity.
2238             verticalScrollOffset -= (Math.abs(verticalVelocity) * verticalVelocity * 1000) / (2 * decelerationRate);
2239 
2240             // Constrain within bounds.
2241             if (verticalScrollOffset > maximumVerticalScrollOffset) {
2242               // Generate an animation curve that bounces past the end point.
2243               c2x = (verticalScrollOffset - maximumVerticalScrollOffset) / containerHeight;
2244               c2y = 2 * c2x;
2245               this._sc_animationTiming = SC.easingCurve(0.0, 0.5,c2x.toFixed(1), c2y.toFixed(1)); 'cubic-bezier(0.0,0.5,%@,%@)'.fmt(c2x.toFixed(1), c2y.toFixed(1));
2246 
2247               verticalScrollOffset = maximumVerticalScrollOffset;
2248 
2249             } else if (verticalScrollOffset < minimumVerticalScrollOffset) {
2250               // Generate an animation curve that bounces past the end point.
2251               c2x = (minimumVerticalScrollOffset - verticalScrollOffset) / containerHeight;
2252               c2y = 2 * c2x;
2253               this._sc_animationTiming = SC.easingCurve(0.0, 0.5,c2x.toFixed(1), c2y.toFixed(1)); 'cubic-bezier(0.0,0.5,%@,%@)'.fmt(c2x.toFixed(1), c2y.toFixed(1));
2254 
2255               verticalScrollOffset = minimumVerticalScrollOffset;
2256 
2257             } else {
2258               this._sc_animationTiming = this.get('animationCurveDecelerate');
2259             }
2260 
2261             this.set('verticalScrollOffset', verticalScrollOffset);
2262 
2263             durationV = Math.abs(verticalVelocity / decelerationRate);
2264           }
2265         }
2266 
2267         var scale = this.get('scale'),
2268           maximumScale = this.get('maximumScale'),
2269           minimumScale = this.get('minimumScale'),
2270           durationS = 0;
2271 
2272         // Bounce back from max.
2273         if (scale > maximumScale) {
2274           this.set('scale', maximumScale);
2275           durationS = 0.25;
2276 
2277         // Bounce back from min.
2278         } else if (scale < minimumScale) {
2279           this.set('scale', minimumScale);
2280           durationS = 0.25;
2281 
2282         // Slide.
2283         } else {
2284 
2285         }
2286 
2287         // Determine how long the deceleration should take (we can't animate left/top separately, so use the largest duration for both).
2288         // This variable also acts as a flag so that when the content view is repositioned, it will be animated.
2289         this._sc_animationDuration = Math.max(Math.max(durationH, durationV), durationS);
2290 
2291         // Clear up all caches from touchesDragged.
2292         this._sc_touchVelocityH = null;
2293         this._sc_touchVelocityV = null;
2294 
2295       // Pass the initial touch on to the content view if it hasn't tried yet (i.e. a tap) and the touch wasn't cancelled.
2296       } else if (this._sc_passTouchToContentTimer) {
2297         // Clean up.
2298         this._sc_passTouchToContentTimer.invalidate();
2299         this._sc_passTouchToContentTimer = null;
2300 
2301         if (!wasCancelled) {
2302           // If the content has handled the touch, then immediately end it.
2303           if (touch.makeTouchResponder(touch.targetView, true, this)) {
2304             touch.end();
2305           }
2306         }
2307       }
2308 
2309       // Clean up all caches from touchStart & touchesDragged
2310       this._sc_gestureAnchorX = this._sc_gestureAnchorY = this._sc_gestureAnchorD = null;
2311       this._sc_gestureAnchorTotalX = this._sc_gestureAnchorTotalY = this._sc_gestureAnchorTotalD = null;
2312       this._sc_gestureAnchorScale = null;
2313       this._sc_gestureAnchorHOffset = null;
2314       this._sc_gestureAnchorVOffset = null;
2315       this._sc_containerOffset = null;
2316       this._sc_touchCenterX = null;
2317       this._sc_touchCenterY = null;
2318     }
2319 
2320     // Force recalculation of scrolling and scaling.
2321     this._sc_isTouchScrollingH = false;
2322     this._sc_isTouchScrollingHOnly = false;
2323     this._sc_isTouchScrollingV = false;
2324     this._sc_isTouchScrollingVOnly = false;
2325     this._sc_isTouchScaling = false;
2326 
2327     // TODO: What happens when isEnabledInPane goes false while interacting? Statechart would help solve this.
2328     return true;
2329   },
2330 
2331   /** @private @see SC.RootResponder.prototype.captureTouch */
2332   captureTouch: function (touch) {
2333     // Capture the touch and begin determination of actual scroll or not.
2334     if (this.get('delaysContentTouches')) {
2335       return true;
2336 
2337     // Otherwise, suggest ourselves as a reasonable fallback responder. If none of our children capture
2338     // the touch or handle touchStart, we'll get another crack at it in touchStart.
2339     } else {
2340       touch.stackCandidateTouchResponder(this);
2341 
2342       return false;
2343     }
2344   },
2345 
2346   /** @private */
2347   mouseWheel: function (evt) {
2348     var handled = false,
2349       contentView = this.get('contentView');
2350 
2351     // Ignore it if not enabled.
2352     if (contentView && this.get('isEnabledInPane')) {
2353 
2354       var horizontalScrollOffset = this.get('horizontalScrollOffset'),
2355         minimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset'),
2356         minimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset'),
2357         maximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset'),
2358         maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'),
2359         verticalScrollOffset = this.get('verticalScrollOffset'),
2360         wheelDeltaX = evt.wheelDeltaX,
2361         wheelDeltaY = evt.wheelDeltaY;
2362 
2363       // If we can't scroll in one direction, limit that direction.
2364       if (!this.get('canScrollHorizontal')) { // Don't allow inverted scrolling for now.
2365         wheelDeltaX = 0;
2366       }
2367 
2368       if (!this.get('canScrollVertical')) { // Don't allow inverted scrolling for now.
2369         wheelDeltaY = 0;
2370       }
2371 
2372       // Only attempt to scroll if we are allowed to scroll in the direction and have room to scroll
2373       // in the direction. Otherwise, ignore the event so that an outer ScrollView may capture it.
2374       handled = ((wheelDeltaX < 0 && horizontalScrollOffset > minimumHorizontalScrollOffset) ||
2375                  (wheelDeltaX > 0 && horizontalScrollOffset < maximumHorizontalScrollOffset)) ||
2376                 ((wheelDeltaY < 0 && verticalScrollOffset > minimumVerticalScrollOffset) ||
2377                   (wheelDeltaY > 0 && verticalScrollOffset < maximumVerticalScrollOffset));
2378 
2379       if (handled) {
2380         this.scrollBy(wheelDeltaX, wheelDeltaY);
2381       }
2382     }
2383 
2384     return handled;
2385   },
2386 
2387   /** @private */
2388   touchesDragged: function (evt, touchesForView) {
2389     var avgTouch = evt.averagedTouchesForView(this),
2390         canScale = this.get('canScale'),
2391         canScrollHorizontal = this.get('canScrollHorizontal'),
2392         canScrollVertical = this.get('canScrollVertical'),
2393         scrollThreshold = this.get('scrollGestureThreshold'),
2394         scaleThreshold = this.get('scaleGestureThreshold'),
2395         scrollLockThreshold = this.get('scrollLockGestureThreshold'),
2396         horizontalScrollOffset,
2397         verticalScrollOffset;
2398 
2399 
2400     // Determine if we've moved enough to claim horizontal or vertical scrolling.
2401     if (!(this._sc_isTouchScrollingH && this._sc_isTouchScrollingV) &&
2402       !this._sc_isTouchScrollingHOnly && !this._sc_isTouchScrollingVOnly) {
2403 
2404       if (canScrollHorizontal) {
2405         var totalAbsDeltaX = Math.abs(this._sc_gestureAnchorTotalX - avgTouch.x);
2406 
2407         if (!this._sc_isTouchScrollingH) {
2408           this._sc_isTouchScrollingH = totalAbsDeltaX >= scrollThreshold;
2409 
2410         // Determine if we've moved enough to lock scrolling to only this direction.
2411         } else {
2412           this._sc_isTouchScrollingHOnly = totalAbsDeltaX >= scrollLockThreshold;
2413         }
2414       }
2415 
2416       if (canScrollVertical) {
2417         var totalAbsDeltaY = Math.abs(this._sc_gestureAnchorTotalY - avgTouch.y);
2418 
2419         if (!this._sc_isTouchScrollingV) {
2420           this._sc_isTouchScrollingV = totalAbsDeltaY >= scrollThreshold;
2421 
2422         // Determine if we've moved enough to lock scrolling to only this direction.
2423         } else {
2424           this._sc_isTouchScrollingVOnly = totalAbsDeltaY >= scrollLockThreshold;
2425         }
2426       }
2427     }
2428 
2429     var touchDeltaX = this._sc_gestureAnchorX - avgTouch.x,
2430         absDeltaX = Math.abs(touchDeltaX);
2431 
2432     // Adjust scroll.
2433     if (canScrollHorizontal && absDeltaX >= 1 && !this._sc_isTouchScrollingVOnly) {
2434       // Record the last velocity.
2435       this._sc_touchVelocityH = avgTouch.velocityX;
2436 
2437       var minimumHorizontalScrollOffset = this.get('minimumHorizontalScrollOffset'),
2438         maximumHorizontalScrollOffset = this.get('maximumHorizontalScrollOffset');
2439 
2440       horizontalScrollOffset = this._sc_gestureAnchorHOffset + touchDeltaX;
2441 
2442       // Reset the anchor. Note: Do this before degrading the offset.
2443       this._sc_gestureAnchorX = avgTouch.x;
2444       this._sc_gestureAnchorHOffset = horizontalScrollOffset;
2445 
2446       // Degrade the offset as we pass maximum.
2447       if (horizontalScrollOffset > maximumHorizontalScrollOffset) {
2448         horizontalScrollOffset = horizontalScrollOffset - this._sc_overDragSlip * (horizontalScrollOffset - maximumHorizontalScrollOffset);
2449 
2450       // Degrade the offset as we pass minimum.
2451       } else if (horizontalScrollOffset < minimumHorizontalScrollOffset) {
2452         horizontalScrollOffset = horizontalScrollOffset + this._sc_overDragSlip * (minimumHorizontalScrollOffset - horizontalScrollOffset);
2453       }
2454 
2455       // Update the scroll offset.
2456       this.set('horizontalScrollOffset', horizontalScrollOffset);
2457     }
2458 
2459     var touchDeltaY = this._sc_gestureAnchorY - avgTouch.y,
2460         absDeltaY = Math.abs(touchDeltaY);
2461 
2462     if (canScrollVertical && absDeltaY > 0 && !this._sc_isTouchScrollingHOnly) {
2463       // Record the last velocity.
2464       this._sc_touchVelocityV = avgTouch.velocityY;
2465 
2466       var minimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset'),
2467           maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset');
2468 
2469       verticalScrollOffset = this._sc_gestureAnchorVOffset + touchDeltaY;
2470 
2471       // Reset the anchor. Note: Do this before degrading the offset.
2472       this._sc_gestureAnchorY = avgTouch.y;
2473       this._sc_gestureAnchorVOffset = verticalScrollOffset;
2474 
2475       // Degrade the offset as we pass maximum.
2476       if (verticalScrollOffset > maximumVerticalScrollOffset) {
2477         verticalScrollOffset = verticalScrollOffset - this._sc_overDragSlip * (verticalScrollOffset - maximumVerticalScrollOffset);
2478 
2479       // Degrade the offset as we pass minimum.
2480       } else if (verticalScrollOffset < minimumVerticalScrollOffset) {
2481         verticalScrollOffset = verticalScrollOffset + this._sc_overDragSlip * (minimumVerticalScrollOffset - verticalScrollOffset);
2482       }
2483 
2484       // Update the scroll offset.
2485       this.set('verticalScrollOffset', verticalScrollOffset);
2486     }
2487 
2488     // Adjust scale.
2489     if (canScale) {
2490 
2491       // Determine if we've moved enough to claim scaling.
2492       if (!this._sc_isTouchScaling) {
2493         var totalAbsDeltaD = Math.abs(this._sc_gestureAnchorTotalD - avgTouch.d);
2494         this._sc_isTouchScaling = !!avgTouch.d && totalAbsDeltaD > scaleThreshold;
2495       }
2496 
2497       var touchDeltaD = this._sc_gestureAnchorD - avgTouch.d,
2498           absDeltaD = Math.abs(touchDeltaD);
2499       if (absDeltaD > 0) {
2500         // The percentage difference in touch distance.
2501         var scalePercentChange = avgTouch.d / this._sc_gestureAnchorD,
2502             scale = this._sc_gestureAnchorScale * scalePercentChange;
2503 
2504         // Adjust the center of the zoom to the center of the gesture.
2505         horizontalScrollOffset = this._sc_horizontalScrollOffset;
2506         verticalScrollOffset = this._sc_verticalScrollOffset;
2507 
2508         // Cache the current offset of the container view in the document. Calculated each time touch scaling begins.
2509         if (!this._sc_containerOffset) {
2510           var el = this.getPath('containerView.layer');
2511 
2512           this._sc_containerOffset = SC.offset(el);
2513           this._sc_touchCenterX = avgTouch.x - this._sc_containerOffset.x;
2514           this._sc_touchCenterY = avgTouch.y - this._sc_containerOffset.y;
2515         }
2516 
2517         // Compute the relative center of the scale gesture.
2518         this._sc_horizontalPct = (horizontalScrollOffset + this._sc_touchCenterX) / this._sc_contentWidth;
2519         this._sc_verticalPct = (verticalScrollOffset + this._sc_touchCenterY) / this._sc_contentHeight;
2520 
2521         this.set('scale', scale);
2522 
2523         // Reset the anchor.
2524         this._sc_gestureAnchorD = avgTouch.d;
2525         this._sc_gestureAnchorScale = scale;
2526       }
2527     }
2528 
2529     // No longer pass the initial touch on to the content view if it was still about to.
2530     if (this._sc_passTouchToContentTimer && (this._sc_isTouchScrollingV || this._sc_isTouchScrollingH || this._sc_isTouchScaling)) {
2531       this._sc_passTouchToContentTimer.invalidate();
2532       this._sc_passTouchToContentTimer = null;
2533     }
2534 
2535     // Note: If the content view has already accepted the initial touch, it will be sent a touchCancelled event.
2536   },
2537 
2538   /** @private
2539     If we're in hand-holding mode and our content claims the touch, we will receive a touchCancelled
2540     event at its completion. We still need to do most of our touch-ending wrap up, for example to finish
2541     bouncing back from a previous gesture.
2542   */
2543   touchCancelled: function (touch) {
2544     return this._sc_touchEnded(touch, true);
2545   },
2546 
2547   /** @private
2548     If we are the touch's responder at its completion, we'll get a touchEnd event. If this is the
2549     gesture's last touch, we wrap up in spectacular fashion.
2550   */
2551   touchEnd: function (touch) {
2552     return this._sc_touchEnded(touch, false);
2553   },
2554 
2555   // /** @private */
2556   touchStart: function (touch) {
2557     var handled = false,
2558       contentView = this.get('contentView');
2559 
2560     if (contentView && this.get('isEnabledInPane')) {
2561       var hasTouch = this.get('hasTouch');
2562 
2563       // Additional touches can be used for pinching gestures.
2564       if (hasTouch) {
2565 
2566         // If a new touch has appeared, force scrolling to recalculate.
2567         this._sc_isTouchScrollingV = this._sc_isTouchScrollingH = false;
2568         this._sc_isTouchScrollingHOnly = this._sc_isTouchScrollingHOnly = false;
2569 
2570         // No longer pass the initial touch on to the content view if it was still about to.
2571         if (this._sc_passTouchToContentTimer) {
2572           this._sc_passTouchToContentTimer.invalidate();
2573           this._sc_passTouchToContentTimer = null;
2574         }
2575 
2576       // The first touch is used to set up initial state.
2577       } else {
2578         // Cancel any active animation in place.
2579         this._sc_cancelAnimation();
2580 
2581         // If we have captured the touch and are not yet scrolling, we may want to delay a moment to test for
2582         // scrolling and if not scrolling, we will pass the touch through to the content.
2583         // If configured to do so, delay 150ms to verify that the user is not scrolling before passing touches through to the content.
2584         if (this.get('delaysContentTouches')) {
2585           this._sc_passTouchToContentTimer = this.invokeLater(this._sc_beginTouchesInContent, 150, touch);
2586         } // Else do nothing.
2587       }
2588 
2589       // Update the average distance to center of the touch, which is used to recognize pinch/zoom movement of the touch.
2590       var avgTouch = touch.averagedTouchesForView(this, true);
2591 
2592       /* A note on these variables:
2593 
2594         _sc_gestureAnchorX: the last x position (so that we don't update horizontal scroll if the change since last is 0)
2595         _sc_gestureAnchorTotalX: the initial x position (so that we can determine whether to take total control of touches and possibly lock the position)
2596       */
2597       this._sc_gestureAnchorX = this._sc_gestureAnchorTotalX = avgTouch.x;
2598       this._sc_gestureAnchorY = this._sc_gestureAnchorTotalY = avgTouch.y;
2599       this._sc_gestureAnchorD = this._sc_gestureAnchorTotalD = avgTouch.d;
2600       this._sc_gestureAnchorScale = this.get('scale');
2601       this._sc_gestureAnchorHOffset = this.get('horizontalScrollOffset');
2602       this._sc_gestureAnchorVOffset = this.get('verticalScrollOffset');
2603 
2604       handled = true;
2605     }
2606 
2607     return handled;
2608   }
2609 
2610 });
2611 
2612 
2613 SC.ScrollView.mixin(
2614 /** @scope SC.ScrollView */ {
2615 
2616   /** @private Shared object used to avoid continually initializing/destroying objects. */
2617   _SC_CONTAINER_LAYOUT_MAP: null,
2618 
2619   /** @private Shared object used to avoid continually initializing/destroying objects. */
2620   _SC_CONTENT_ADJUST_MAP: null
2621 
2622 });
2623