1 /****************************************************************************
  2  Copyright (c) 2010-2013 cocos2d-x.org
  3  Copyright (c) 2010 Sangwoo Im
  4 
  5  http://www.cocos2d-x.org
  6 
  7  Permission is hereby granted, free of charge, to any person obtaining a copy
  8  of this software and associated documentation files (the "Software"), to deal
  9  in the Software without restriction, including without limitation the rights
 10  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 11  copies of the Software, and to permit persons to whom the Software is
 12  furnished to do so, subject to the following conditions:
 13 
 14  The above copyright notice and this permission notice shall be included in
 15  all copies or substantial portions of the Software.
 16 
 17  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 18  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 19  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 20  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 21  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 22  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 23  THE SOFTWARE.
 24  ****************************************************************************/
 25 
 26 /**
 27  * @ignore
 28  */
 29 cc.SCROLLVIEW_DIRECTION_NONE = -1;
 30 
 31 cc.SCROLLVIEW_DIRECTION_HORIZONTAL = 0;
 32 
 33 cc.SCROLLVIEW_DIRECTION_VERTICAL = 1;
 34 
 35 cc.SCROLLVIEW_DIRECTION_BOTH = 2;
 36 
 37 var SCROLL_DEACCEL_RATE = 0.95;
 38 var SCROLL_DEACCEL_DIST = 1.0;
 39 var BOUNCE_DURATION = 0.15;
 40 var INSET_RATIO = 0.2;
 41 var MOVE_INCH = 7.0/160.0;
 42 
 43 cc.convertDistanceFromPointToInch = function(pointDis){
 44     var eglViewer = cc.view;
 45     var factor = (eglViewer.getScaleX() + eglViewer.getScaleY())/2;
 46     return (pointDis * factor) / 160;               // CCDevice::getDPI() default value
 47 };
 48 
 49 cc.ScrollViewDelegate = cc.Class.extend({
 50     scrollViewDidScroll:function (view) {
 51     },
 52     scrollViewDidZoom:function (view) {
 53     }
 54 });
 55 
 56 /**
 57  * ScrollView support for cocos2d -x.
 58  * It provides scroll view functionalities to cocos2d projects natively.
 59  * @class
 60  * @extend cc.Layer
 61  *
 62  * @property {cc.Point}                 minOffset   - <@readonly> The current container's minimum offset
 63  * @property {cc.Point}                 maxOffset   - <@readonly> The current container's maximum offset
 64  * @property {Boolean}                  bounceable  - Indicate whether the scroll view is bounceable
 65  * @property {cc.Size}                  viewSize    - The size of the scroll view
 66  * @property {cc.Layer}                 container   - The inside container of the scroll view
 67  * @property {Number}                   direction   - The direction allowed to scroll: cc.SCROLLVIEW_DIRECTION_BOTH by default, or cc.SCROLLVIEW_DIRECTION_NONE | cc.SCROLLVIEW_DIRECTION_HORIZONTAL | cc.SCROLLVIEW_DIRECTION_VERTICAL
 68  * @property {cc.ScrollViewDelegate}    delegate    - The inside container of the scroll view
 69  * @property {Boolean}           clippingToBounds   - Indicate whether the scroll view clips its children
 70  */
 71 cc.ScrollView = cc.Layer.extend(/** @lends cc.ScrollView# */{
 72     _zoomScale:0,
 73     _minZoomScale:0,
 74     _maxZoomScale:0,
 75     _delegate:null,
 76     _direction:cc.SCROLLVIEW_DIRECTION_BOTH,
 77     _dragging:false,
 78     _contentOffset:null,
 79     _container:null,
 80     _touchMoved:false,
 81     _maxInset:null,
 82     _minInset:null,
 83     _bounceable:false,
 84     _clippingToBounds:false,
 85     _scrollDistance:null,
 86     _touchPoint:null,
 87     _touchLength:0,
 88     _touches:null,
 89     _viewSize:null,
 90     _minScale:0,
 91     _maxScale:0,
 92 
 93     //scissor rect for parent, just for restoring GL_SCISSOR_BOX
 94     _parentScissorRect:null,
 95     _scissorRestored:false,
 96 
 97     // cache object
 98     _tmpViewRect:null,
 99     _touchListener: null,
100     _className:"ScrollView",
101 
102     ctor:function () {
103         cc.Layer.prototype.ctor.call(this);
104         this._contentOffset = cc.p(0,0);
105         this._maxInset = cc.p(0, 0);
106         this._minInset = cc.p(0, 0);
107         this._scrollDistance = cc.p(0, 0);
108         this._touchPoint = cc.p(0, 0);
109         this._touches = [];
110         this._viewSize = cc.size(0, 0);
111         this._parentScissorRect = new cc.Rect(0,0,0,0);
112         this._tmpViewRect = new cc.Rect(0,0,0,0);
113     },
114 
115     init:function () {
116         return this.initWithViewSize(cc.size(200, 200), null);
117     },
118 
119     /**
120      * initialized whether success or fail
121      * @param {cc.Size} size
122      * @param {cc.Node} container
123      * @return {Boolean}
124      */
125     initWithViewSize:function (size, container) {
126         var pZero = cc.p(0,0);
127         if (cc.Layer.prototype.init.call(this)) {
128             this._container = container;
129 
130             if (!this._container) {
131                 this._container = cc.Layer.create();
132                 this._container.ignoreAnchorPointForPosition(false);
133                 this._container.setAnchorPoint(pZero);
134             }
135 
136             this.setViewSize(size);
137 
138             this.setTouchEnabled(true);
139             this._touches.length = 0;
140             this._delegate = null;
141             this._bounceable = true;
142             this._clippingToBounds = true;
143 
144             //this._container.setContentSize(CCSizeZero);
145             this._direction = cc.SCROLLVIEW_DIRECTION_BOTH;
146             this._container.setPosition(pZero);
147             this._touchLength = 0.0;
148 
149             this.addChild(this._container);
150             this._minScale = this._maxScale = 1.0;
151             return true;
152         }
153         return false;
154     },
155 
156     /**
157      * Sets a new content offset. It ignores max/min offset. It just sets what's given. (just like UIKit's UIScrollView)
158      *
159      * @param {cc.Point} offset new offset
160      * @param {Number} [animated=] If true, the view will scroll to the new offset
161      */
162     setContentOffset: function (offset, animated) {
163         if (animated) { //animate scrolling
164             this.setContentOffsetInDuration(offset, BOUNCE_DURATION);
165             return;
166         }
167         if (!this._bounceable) {
168             var minOffset = this._getMinContainerOffset();
169             var maxOffset = this._getMaxContainerOffset();
170 
171             offset.x = Math.max(minOffset.x, Math.min(maxOffset.x, offset.x));
172             offset.y = Math.max(minOffset.y, Math.min(maxOffset.y, offset.y));
173         }
174 
175         this._container.setPosition(offset);
176         var locDelegate = this._delegate;
177         if (locDelegate != null && locDelegate.scrollViewDidScroll) {
178             locDelegate.scrollViewDidScroll(this);
179         }
180 
181     },
182 
183     getContentOffset:function () {
184         var locPos = this._container.getPosition();
185         return cc.p(locPos.x, locPos.y);
186     },
187 
188     /**
189      * <p>Sets a new content offset. It ignores max/min offset. It just sets what's given. (just like UIKit's UIScrollView) <br/>
190      * You can override the animation duration with this method.
191      * </p>
192      * @param {cc.Point} offset new offset
193      * @param {Number} dt animation duration
194      */
195     setContentOffsetInDuration:function (offset, dt) {
196         var scroll = cc.MoveTo.create(dt, offset);
197         var expire = cc.CallFunc.create(this._stoppedAnimatedScroll, this);
198         this._container.runAction(cc.Sequence.create(scroll, expire));
199         this.schedule(this._performedAnimatedScroll);
200     },
201 
202     /**
203      * Sets a new scale and does that for a predefined duration.
204      *
205      * @param {Number} scale a new scale vale
206      * @param {Boolean} [animated=null] if YES, scaling is animated
207      */
208     setZoomScale: function (scale, animated) {
209         if (animated) {
210             this.setZoomScaleInDuration(scale, BOUNCE_DURATION);
211             return;
212         }
213 
214         var locContainer = this._container;
215         if (locContainer.getScale() != scale) {
216             var oldCenter, newCenter;
217             var center;
218 
219             if (this._touchLength == 0.0) {
220                 var locViewSize = this._viewSize;
221                 center = cc.p(locViewSize.width * 0.5, locViewSize.height * 0.5);
222                 center = this.convertToWorldSpace(center);
223             } else
224                 center = this._touchPoint;
225 
226             oldCenter = locContainer.convertToNodeSpace(center);
227             locContainer.setScale(Math.max(this._minScale, Math.min(this._maxScale, scale)));
228             newCenter = locContainer.convertToWorldSpace(oldCenter);
229 
230             var offset = cc.pSub(center, newCenter);
231             if (this._delegate && this._delegate.scrollViewDidZoom)
232                 this._delegate.scrollViewDidZoom(this);
233             this.setContentOffset(cc.pAdd(locContainer.getPosition(), offset));
234         }
235     },
236 
237     getZoomScale:function () {
238         return this._container.getScale();
239     },
240 
241     /**
242      * Sets a new scale for container in a given duration.
243      *
244      * @param {Number} s a new scale value
245      * @param {Number} dt animation duration
246      */
247     setZoomScaleInDuration:function (s, dt) {
248         if (dt > 0) {
249             var locScale = this._container.getScale();
250             if (locScale != s) {
251                 var scaleAction = cc.ActionTween.create(dt, "zoomScale", locScale, s);
252                 this.runAction(scaleAction);
253             }
254         } else {
255             this.setZoomScale(s);
256         }
257     },
258 
259     /**
260      * Returns the current container's minimum offset. You may want this while you animate scrolling by yourself
261      * @return {cc.Point} Returns the current container's minimum offset.
262      */
263     _getMinContainerOffset:function () {
264         var locContainer = this._container;
265         var locContentSize = locContainer.getContentSize(), locViewSize = this._viewSize;
266         return cc.p(locViewSize.width - locContentSize.width * locContainer.getScaleX(),
267             locViewSize.height - locContentSize.height * locContainer.getScaleY());
268     },
269 
270     /**
271      * Returns the current container's maximum offset. You may want this while you animate scrolling by yourself
272      * @return {cc.Point} Returns the current container's maximum offset.
273      */
274     _getMaxContainerOffset:function () {
275         return cc.p(0.0, 0.0);
276     },
277 
278     /**
279      * Determines if a given node's bounding box is in visible bounds
280      * @param {cc.Node} node
281      * @return {Boolean} YES if it is in visible bounds
282      */
283     isNodeVisible:function (node) {
284         var offset = this.getContentOffset();
285         var size = this.getViewSize();
286         var scale = this.getZoomScale();
287 
288         var viewRect = cc.rect(-offset.x / scale, -offset.y / scale, size.width / scale, size.height / scale);
289 
290         return cc.rectIntersectsRect(viewRect, node.getBoundingBox());
291     },
292 
293     /**
294      * Provided to make scroll view compatible with SWLayer's pause method
295      */
296     pause:function (sender) {
297         this._container.pauseSchedulerAndActions();
298         var selChildren = this._container.getChildren();
299         for (var i = 0; i < selChildren.length; i++) {
300             selChildren[i].pauseSchedulerAndActions();
301         }
302     },
303 
304     /**
305      * Provided to make scroll view compatible with SWLayer's resume method
306      */
307     resume:function (sender) {
308         var selChildren = this._container.getChildren();
309         for (var i = 0, len = selChildren.length; i < len; i++) {
310             selChildren[i].resumeSchedulerAndActions();
311         }
312 
313         this._container.resumeSchedulerAndActions();
314     },
315 
316     isDragging:function () {
317         return this._dragging;
318     },
319     isTouchMoved:function () {
320         return this._touchMoved;
321     },
322     isBounceable:function () {
323         return this._bounceable;
324     },
325     setBounceable:function (bounceable) {
326         this._bounceable = bounceable;
327     },
328 
329     /**
330      * <p>
331      * size to clip. CCNode boundingBox uses contentSize directly.                   <br/>
332      * It's semantically different what it actually means to common scroll views.    <br/>
333      * Hence, this scroll view will use a separate size property.
334      * </p>
335      */
336     getViewSize:function () {
337         return this._viewSize;
338     },
339 
340     setViewSize:function (size) {
341         this._viewSize = size;
342         cc.Node.prototype.setContentSize.call(this,size);
343     },
344 
345     getContainer:function () {
346         return this._container;
347     },
348 
349     setContainer:function (container) {
350         // Make sure that 'm_pContainer' has a non-NULL value since there are
351         // lots of logic that use 'm_pContainer'.
352         if (!container)
353             return;
354 
355         this.removeAllChildren(true);
356 
357         this._container = container;
358         container.ignoreAnchorPointForPosition(false);
359         container.setAnchorPoint(0, 0);
360 
361         this.addChild(container);
362         this.setViewSize(this._viewSize);
363     },
364 
365     /**
366      * direction allowed to scroll. CCScrollViewDirectionBoth by default.
367      */
368     getDirection:function () {
369         return this._direction;
370     },
371     setDirection:function (direction) {
372         this._direction = direction;
373     },
374 
375     getDelegate:function () {
376         return this._delegate;
377     },
378     setDelegate:function (delegate) {
379         this._delegate = delegate;
380     },
381 
382     /** override functions */
383     // optional
384     onTouchBegan:function (touch, event) {
385         if (!this.isVisible())
386             return false;
387         //var frameOriginal = this.getParent().convertToWorldSpace(this.getPosition());
388         //var frame = cc.rect(frameOriginal.x, frameOriginal.y, this._viewSize.width, this._viewSize.height);
389         var frame = this._getViewRect();
390 
391         //dispatcher does not know about clipping. reject touches outside visible bounds.
392         var locContainer = this._container;
393         var locPoint = locContainer.convertToWorldSpace(locContainer.convertTouchToNodeSpace(touch));
394         var locTouches = this._touches;
395         if (locTouches.length > 2 || this._touchMoved || !cc.rectContainsPoint(frame, locPoint))
396             return false;
397 
398         locTouches.push(touch);
399         //}
400 
401         if (locTouches.length === 1) { // scrolling
402             this._touchPoint = this.convertTouchToNodeSpace(touch);
403             this._touchMoved = false;
404             this._dragging = true; //dragging started
405             this._scrollDistance.x = 0;
406             this._scrollDistance.y = 0;
407             this._touchLength = 0.0;
408         } else if (locTouches.length == 2) {
409             this._touchPoint = cc.pMidpoint(this.convertTouchToNodeSpace(locTouches[0]),
410                 this.convertTouchToNodeSpace(locTouches[1]));
411             this._touchLength = cc.pDistance(locContainer.convertTouchToNodeSpace(locTouches[0]),
412                 locContainer.convertTouchToNodeSpace(locTouches[1]));
413             this._dragging = false;
414         }
415         return true;
416     },
417 
418     onTouchMoved:function (touch, event) {
419         if (!this.isVisible())
420             return;
421 
422         if (this._touches.length === 1 && this._dragging) { // scrolling
423             this._touchMoved = true;
424             //var frameOriginal = this.getParent().convertToWorldSpace(this.getPosition());
425             //var frame = cc.rect(frameOriginal.x, frameOriginal.y, this._viewSize.width, this._viewSize.height);
426             var frame = this._getViewRect();
427 
428             //var newPoint = this.convertTouchToNodeSpace(this._touches[0]);
429             var newPoint = this.convertTouchToNodeSpace(touch);
430             var moveDistance = cc.pSub(newPoint, this._touchPoint);
431 
432             var dis = 0.0, locDirection = this._direction;
433             if (locDirection === cc.SCROLLVIEW_DIRECTION_VERTICAL)
434                 dis = moveDistance.y;
435             else if (locDirection === cc.SCROLLVIEW_DIRECTION_HORIZONTAL)
436                 dis = moveDistance.x;
437             else
438                 dis = Math.sqrt(moveDistance.x * moveDistance.x + moveDistance.y * moveDistance.y);
439 
440             if (!this._touchMoved && Math.abs(cc.convertDistanceFromPointToInch(dis)) < MOVE_INCH ){
441                 //CCLOG("Invalid movement, distance = [%f, %f], disInch = %f", moveDistance.x, moveDistance.y);
442                 return;
443             }
444 
445             if (!this._touchMoved){
446                 moveDistance.x = 0;
447                 moveDistance.y = 0;
448             }
449 
450             this._touchPoint = newPoint;
451             this._touchMoved = true;
452 
453             if (cc.rectContainsPoint(frame, this.convertToWorldSpace(newPoint))) {
454                 switch (locDirection) {
455                     case cc.SCROLLVIEW_DIRECTION_VERTICAL:
456                         moveDistance.x = 0.0;
457                         break;
458                     case cc.SCROLLVIEW_DIRECTION_HORIZONTAL:
459                         moveDistance.y = 0.0;
460                         break;
461                     default:
462                         break;
463                 }
464 
465                 var locPosition = this._container.getPosition();
466                 var newX = locPosition.x + moveDistance.x;
467                 var newY = locPosition.y + moveDistance.y;
468 
469                 this._scrollDistance = moveDistance;
470                 this.setContentOffset(cc.p(newX, newY));
471             }
472         } else if (this._touches.length === 2 && !this._dragging) {
473             var len = cc.pDistance(this._container.convertTouchToNodeSpace(this._touches[0]),
474                 this._container.convertTouchToNodeSpace(this._touches[1]));
475             this.setZoomScale(this.getZoomScale() * len / this._touchLength);
476         }
477     },
478 
479     onTouchEnded:function (touch, event) {
480         if (!this.isVisible())
481             return;
482 
483         if (this._touches.length == 1 && this._touchMoved)
484             this.schedule(this._deaccelerateScrolling);
485 
486         this._touches.length = 0;
487         this._dragging = false;
488         this._touchMoved = false;
489     },
490 
491     onTouchCancelled:function (touch, event) {
492         if (!this.isVisible())
493             return;
494 
495         this._touches.length = 0;
496         this._dragging = false;
497         this._touchMoved = false;
498     },
499 
500     setContentSize: function (size, height) {
501         if (this.getContainer() != null) {
502             if(height === undefined)
503                 this.getContainer().setContentSize(size);
504             else
505                 this.getContainer().setContentSize(size, height);
506             this.updateInset();
507         }
508     },
509 	_setWidth: function (value) {
510 		var container = this.getContainer();
511 		if (container != null) {
512 			container._setWidth(value);
513 			this.updateInset();
514 		}
515 	},
516 	_setHeight: function (value) {
517 		var container = this.getContainer();
518 		if (container != null) {
519 			container._setHeight(value);
520 			this.updateInset();
521 		}
522 	},
523 
524     getContentSize:function () {
525         return this._container.getContentSize();
526     },
527 
528     updateInset:function () {
529         if (this.getContainer() != null) {
530             var locViewSize = this._viewSize;
531             var tempOffset = this._getMaxContainerOffset();
532             this._maxInset.x = tempOffset.x + locViewSize.width * INSET_RATIO;
533             this._maxInset.y = tempOffset.y + locViewSize.height * INSET_RATIO;
534             tempOffset = this._getMinContainerOffset();
535             this._minInset.x = tempOffset.x - locViewSize.width * INSET_RATIO;
536             this._minInset.y = tempOffset.y - locViewSize.height * INSET_RATIO;
537         }
538     },
539 
540     /**
541      * Determines whether it clips its children or not.
542      */
543     isClippingToBounds:function () {
544         return this._clippingToBounds;
545     },
546 
547     setClippingToBounds:function (clippingToBounds) {
548         this._clippingToBounds = clippingToBounds;
549     },
550 
551     visit:function (ctx) {
552         // quick return if not visible
553         if (!this.isVisible())
554             return;
555 
556         var context = ctx || cc._renderContext;
557         var i, locChildren = this._children, selChild, childrenLen;
558         if (cc._renderType === cc._RENDER_TYPE_CANVAS) {
559             context.save();
560             this.transform(context);
561             this._beforeDraw(context);
562 
563             if (locChildren && locChildren.length > 0) {
564                 childrenLen = locChildren.length;
565                 this.sortAllChildren();
566                 // draw children zOrder < 0
567                 for (i = 0; i < childrenLen; i++) {
568                     selChild = locChildren[i];
569                     if (selChild && selChild._localZOrder < 0)
570                         selChild.visit(context);
571                     else
572                         break;
573                 }
574 
575                 this.draw(context);             // self draw
576 
577                 // draw children zOrder >= 0
578                 for (; i < childrenLen; i++)
579                     locChildren[i].visit(context);
580             } else{
581                 this.draw(context);             // self draw
582             }
583 
584             this._afterDraw();
585 
586             context.restore();
587         } else {
588             cc.kmGLPushMatrix();
589             var locGrid = this.grid;
590             if (locGrid && locGrid.isActive()) {
591                 locGrid.beforeDraw();
592                 this.transformAncestors();
593             }
594 
595             this.transform(context);
596             this._beforeDraw(context);
597             if (locChildren && locChildren.length > 0) {
598                 childrenLen = locChildren.length;
599                 // draw children zOrder < 0
600                 for (i = 0; i < childrenLen; i++) {
601                     selChild = locChildren[i];
602                     if (selChild && selChild._localZOrder < 0)
603                         selChild.visit();
604                     else
605                         break;
606                 }
607 
608                 // this draw
609                 this.draw(context);
610 
611                 // draw children zOrder >= 0
612                 for (; i < childrenLen; i++)
613                     locChildren[i].visit();
614             } else{
615                 this.draw(context);
616             }
617 
618             this._afterDraw(context);
619             if (locGrid && locGrid.isActive())
620                 locGrid.afterDraw(this);
621 
622             cc.kmGLPopMatrix();
623         }
624     },
625 
626     addChild:function (child, zOrder, tag) {
627         if (!child)
628             throw new Error("child must not nil!");
629 
630         zOrder = zOrder || child.getLocalZOrder();
631         tag = tag || child.getTag();
632 
633         child.ignoreAnchorPointForPosition(false);
634         child.setAnchorPoint(0, 0);
635         if (this._container != child) {
636             this._container.addChild(child, zOrder, tag);
637         } else {
638             cc.Layer.prototype.addChild.call(this, child, zOrder, tag);
639         }
640     },
641 
642     isTouchEnabled: function(){
643         return this._touchListener != null;
644     },
645 
646     setTouchEnabled:function (e) {
647         if(this._touchListener)
648             cc.eventManager.removeListener(this._touchListener);
649         this._touchListener = null;
650         if (!e) {
651             this._dragging = false;
652             this._touchMoved = false;
653             this._touches.length = 0;
654         } else {
655             var listener = cc.EventListener.create({
656                 event: cc.EventListener.TOUCH_ONE_BY_ONE
657             });
658             if(this.onTouchBegan)
659                 listener.onTouchBegan = this.onTouchBegan.bind(this);
660             if(this.onTouchMoved)
661                 listener.onTouchMoved = this.onTouchMoved.bind(this);
662             if(this.onTouchEnded)
663                 listener.onTouchEnded = this.onTouchEnded.bind(this);
664             if(this.onTouchCancelled)
665                 listener.onTouchCancelled = this.onTouchCancelled.bind(this);
666             this._touchListener = listener;
667             cc.eventManager.addListener(listener, this);
668         }
669     },
670 
671     /**
672      * Init this object with a given size to clip its content.
673      *
674      * @param size view size
675      * @return initialized scroll view object
676      */
677     _initWithViewSize:function (size) {
678         return null;
679     },
680 
681     /**
682      * Relocates the container at the proper offset, in bounds of max/min offsets.
683      *
684      * @param animated If YES, relocation is animated
685      */
686     _relocateContainer:function (animated) {
687         var min = this._getMinContainerOffset();
688         var max = this._getMaxContainerOffset();
689         var locDirection = this._direction;
690 
691         var oldPoint = this._container.getPosition();
692         var newX = oldPoint.x;
693         var newY = oldPoint.y;
694         if (locDirection === cc.SCROLLVIEW_DIRECTION_BOTH || locDirection === cc.SCROLLVIEW_DIRECTION_HORIZONTAL) {
695             newX = Math.max(newX, min.x);
696             newX = Math.min(newX, max.x);
697         }
698 
699         if (locDirection == cc.SCROLLVIEW_DIRECTION_BOTH || locDirection == cc.SCROLLVIEW_DIRECTION_VERTICAL) {
700             newY = Math.min(newY, max.y);
701             newY = Math.max(newY, min.y);
702         }
703 
704         if (newY != oldPoint.y || newX != oldPoint.x) {
705             this.setContentOffset(cc.p(newX, newY), animated);
706         }
707     },
708     /**
709      * implements auto-scrolling behavior. change SCROLL_DEACCEL_RATE as needed to choose    <br/>
710      * deacceleration speed. it must be less than 1.0.
711      *
712      * @param {Number} dt delta
713      */
714     _deaccelerateScrolling:function (dt) {
715         if (this._dragging) {
716             this.unschedule(this._deaccelerateScrolling);
717             return;
718         }
719 
720         var maxInset, minInset;
721         var oldPosition = this._container.getPosition();
722         var locScrollDistance = this._scrollDistance;
723         this._container.setPosition(oldPosition.x + locScrollDistance.x , oldPosition.y + locScrollDistance.y);
724         if (this._bounceable) {
725             maxInset = this._maxInset;
726             minInset = this._minInset;
727         } else {
728             maxInset = this._getMaxContainerOffset();
729             minInset = this._getMinContainerOffset();
730         }
731 
732         //check to see if offset lies within the inset bounds
733         var newX = this._container.getPositionX();
734         var newY = this._container.getPositionY();
735         
736         locScrollDistance.x = locScrollDistance.x * SCROLL_DEACCEL_RATE;
737         locScrollDistance.y = locScrollDistance.y * SCROLL_DEACCEL_RATE;
738 
739         this.setContentOffset(cc.p(newX, newY));
740 
741         if ((Math.abs(locScrollDistance.x) <= SCROLL_DEACCEL_DIST &&
742             Math.abs(locScrollDistance.y) <= SCROLL_DEACCEL_DIST) ||
743             newY > maxInset.y || newY < minInset.y ||
744             newX > maxInset.x || newX < minInset.x ||
745             newX == maxInset.x || newX == minInset.x ||
746             newY == maxInset.y || newY == minInset.y) {
747             this.unschedule(this._deaccelerateScrolling);
748             this._relocateContainer(true);
749         }
750     },
751     /**
752      * This method makes sure auto scrolling causes delegate to invoke its method
753      */
754     _performedAnimatedScroll:function (dt) {
755         if (this._dragging) {
756             this.unschedule(this._performedAnimatedScroll);
757             return;
758         }
759 
760         if (this._delegate && this._delegate.scrollViewDidScroll)
761             this._delegate.scrollViewDidScroll(this);
762     },
763     /**
764      * Expire animated scroll delegate calls
765      */
766     _stoppedAnimatedScroll:function (node) {
767         this.unschedule(this._performedAnimatedScroll);
768         // After the animation stopped, "scrollViewDidScroll" should be invoked, this could fix the bug of lack of tableview cells.
769         if (this._delegate && this._delegate.scrollViewDidScroll) {
770             this._delegate.scrollViewDidScroll(this);
771         }
772     },
773 
774     /**
775      * clip this view so that outside of the visible bounds can be hidden.
776      */
777     _beforeDraw:function (context) {
778         if (this._clippingToBounds) {
779             this._scissorRestored = false;
780             var frame = this._getViewRect(), locEGLViewer = cc.view;
781 
782             var scaleX = this.getScaleX();
783             var scaleY = this.getScaleY();
784 
785             var ctx = context || cc._renderContext;
786             if (cc._renderType === cc._RENDER_TYPE_CANVAS) {
787                 var getWidth = (this._viewSize.width * scaleX) * locEGLViewer.getScaleX();
788                 var getHeight = (this._viewSize.height * scaleY) * locEGLViewer.getScaleY();
789                 var startX = 0;
790                 var startY = 0;
791 
792                 ctx.beginPath();
793                 ctx.rect(startX, startY, getWidth, -getHeight);
794                 ctx.clip();
795                 ctx.closePath();
796             } else {
797                 var EGLViewer = cc.view;
798                 if(EGLViewer.isScissorEnabled()){
799                     this._scissorRestored = true;
800                     this._parentScissorRect = EGLViewer.getScissorRect();
801                     //set the intersection of m_tParentScissorRect and frame as the new scissor rect
802                     if (cc.rectIntersection(frame, this._parentScissorRect)) {
803                         var locPSRect = this._parentScissorRect;
804                         var x = Math.max(frame.x, locPSRect.x);
805                         var y = Math.max(frame.y, locPSRect.y);
806                         var xx = Math.min(frame.x + frame.width, locPSRect.x + locPSRect.width);
807                         var yy = Math.min(frame.y + frame.height, locPSRect.y + locPSRect.height);
808                         EGLViewer.setScissorInPoints(x, y, xx - x, yy - y);
809                     }
810                 }else{
811                     ctx.enable(ctx.SCISSOR_TEST);
812                     //clip
813                     EGLViewer.setScissorInPoints(frame.x, frame.y, frame.width, frame.height);
814                 }
815             }
816         }
817     },
818     /**
819      * retract what's done in beforeDraw so that there's no side effect to
820      * other nodes.
821      */
822     _afterDraw:function (context) {
823         if (this._clippingToBounds && cc._renderType === cc._RENDER_TYPE_WEBGL) {
824             if (this._scissorRestored) {  //restore the parent's scissor rect
825                 var rect = this._parentScissorRect;
826                 cc.view.setScissorInPoints(rect.x, rect.y, rect.width, rect.height)
827             }else{
828                 var ctx = context || cc._renderContext;
829                 ctx.disable(ctx.SCISSOR_TEST);
830             }
831         }
832     },
833     /**
834      * Zoom handling
835      */
836     _handleZoom:function () {
837     },
838 
839     _getViewRect:function(){
840         var screenPos = this.convertToWorldSpace(cc.p(0,0));
841         var locViewSize = this._viewSize;
842 
843         var scaleX = this.getScaleX();
844         var scaleY = this.getScaleY();
845 
846         for (var p = this._parent; p != null; p = p.getParent()) {
847             scaleX *= p.getScaleX();
848             scaleY *= p.getScaleY();
849         }
850 
851         // Support negative scaling. Not doing so causes intersectsRect calls
852         // (eg: to check if the touch was within the bounds) to return false.
853         // Note, CCNode::getScale will assert if X and Y scales are different.
854         if (scaleX < 0) {
855             screenPos.x += locViewSize.width * scaleX;
856             scaleX = -scaleX;
857         }
858         if (scaleY < 0) {
859             screenPos.y += locViewSize.height * scaleY;
860             scaleY = -scaleY;
861         }
862 
863         var locViewRect = this._tmpViewRect;
864         locViewRect.x = screenPos.x;
865         locViewRect.y = screenPos.y;
866         locViewRect.width = locViewSize.width * scaleX;
867         locViewRect.height = locViewSize.height * scaleY;
868         return locViewRect;
869     }
870 });
871 
872 window._p = cc.ScrollView.prototype;
873 
874 // Extended properties
875 /** @expose */
876 _p.minOffset;
877 cc.defineGetterSetter(_p, "minOffset", _p._getMinContainerOffset);
878 /** @expose */
879 _p.maxOffset;
880 cc.defineGetterSetter(_p, "maxOffset", _p._getMaxContainerOffset);
881 /** @expose */
882 _p.bounceable;
883 cc.defineGetterSetter(_p, "bounceable", _p.isBounceable, _p.setBounceable);
884 /** @expose */
885 _p.viewSize;
886 cc.defineGetterSetter(_p, "viewSize", _p.getViewSize, _p.setViewSize);
887 /** @expose */
888 _p.container;
889 cc.defineGetterSetter(_p, "container", _p.getContainer, _p.setContainer);
890 /** @expose */
891 _p.direction;
892 cc.defineGetterSetter(_p, "direction", _p.getDirection, _p.setDirection);
893 /** @expose */
894 _p.delegate;
895 cc.defineGetterSetter(_p, "delegate", _p.getDelegate, _p.setDelegate);
896 /** @expose */
897 _p.clippingToBounds;
898 cc.defineGetterSetter(_p, "clippingToBounds", _p.isClippingToBounds, _p.setClippingToBounds);
899 
900 delete window._p;
901 
902 /**
903  * Returns an autoreleased scroll view object.
904  *
905  * @param {cc.Size} size view size
906  * @param {cc.Node} container parent object
907  * @return {cc.ScrollView} scroll view object
908  */
909 cc.ScrollView.create = function (size, container) {
910     var pRet = new cc.ScrollView();
911     if (arguments.length == 2) {
912         if (pRet && pRet.initWithViewSize(size, container))
913             return pRet;
914     } else {
915         if (pRet && pRet.init())
916             return pRet;
917     }
918     return null;
919 };