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