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         this._super();
306     },
307 
308     /**
309      * Provided to make scroll view compatible with SWLayer's resume method
310      */
311     resume:function (sender) {
312         var selChildren = this._container.getChildren();
313         for (var i = 0, len = selChildren.length; i < len; i++) {
314             selChildren[i].resume();
315         }
316         this._container.resume();
317         this._super();
318     },
319 
320     isDragging:function () {
321         return this._dragging;
322     },
323     isTouchMoved:function () {
324         return this._touchMoved;
325     },
326     isBounceable:function () {
327         return this._bounceable;
328     },
329     setBounceable:function (bounceable) {
330         this._bounceable = bounceable;
331     },
332 
333     /**
334      * <p>
335      * size to clip. CCNode boundingBox uses contentSize directly.                   <br/>
336      * It's semantically different what it actually means to common scroll views.    <br/>
337      * Hence, this scroll view will use a separate size property.
338      * </p>
339      */
340     getViewSize:function () {
341         return this._viewSize;
342     },
343 
344     setViewSize:function (size) {
345         this._viewSize = size;
346         cc.Node.prototype.setContentSize.call(this,size);
347     },
348 
349     getContainer:function () {
350         return this._container;
351     },
352 
353     setContainer:function (container) {
354         // Make sure that 'm_pContainer' has a non-NULL value since there are
355         // lots of logic that use 'm_pContainer'.
356         if (!container)
357             return;
358 
359         this.removeAllChildren(true);
360 
361         this._container = container;
362         container.ignoreAnchorPointForPosition(false);
363         container.setAnchorPoint(0, 0);
364 
365         this.addChild(container);
366         this.setViewSize(this._viewSize);
367     },
368 
369     /**
370      * direction allowed to scroll. CCScrollViewDirectionBoth by default.
371      */
372     getDirection:function () {
373         return this._direction;
374     },
375     setDirection:function (direction) {
376         this._direction = direction;
377     },
378 
379     getDelegate:function () {
380         return this._delegate;
381     },
382     setDelegate:function (delegate) {
383         this._delegate = delegate;
384     },
385 
386     /** override functions */
387     // optional
388     onTouchBegan:function (touch, event) {
389         if (!this.isVisible())
390             return false;
391         //var frameOriginal = this.getParent().convertToWorldSpace(this.getPosition());
392         //var frame = cc.rect(frameOriginal.x, frameOriginal.y, this._viewSize.width, this._viewSize.height);
393         var frame = this._getViewRect();
394 
395         //dispatcher does not know about clipping. reject touches outside visible bounds.
396         var locContainer = this._container;
397         var locPoint = locContainer.convertToWorldSpace(locContainer.convertTouchToNodeSpace(touch));
398         var locTouches = this._touches;
399         if (locTouches.length > 2 || this._touchMoved || !cc.rectContainsPoint(frame, locPoint))
400             return false;
401 
402         locTouches.push(touch);
403         //}
404 
405         if (locTouches.length === 1) { // scrolling
406             this._touchPoint = this.convertTouchToNodeSpace(touch);
407             this._touchMoved = false;
408             this._dragging = true; //dragging started
409             this._scrollDistance.x = 0;
410             this._scrollDistance.y = 0;
411             this._touchLength = 0.0;
412         } else if (locTouches.length == 2) {
413             this._touchPoint = cc.pMidpoint(this.convertTouchToNodeSpace(locTouches[0]),
414                 this.convertTouchToNodeSpace(locTouches[1]));
415             this._touchLength = cc.pDistance(locContainer.convertTouchToNodeSpace(locTouches[0]),
416                 locContainer.convertTouchToNodeSpace(locTouches[1]));
417             this._dragging = false;
418         }
419         return true;
420     },
421 
422     onTouchMoved:function (touch, event) {
423         if (!this.isVisible())
424             return;
425 
426         if (this._touches.length === 1 && this._dragging) { // scrolling
427             this._touchMoved = true;
428             //var frameOriginal = this.getParent().convertToWorldSpace(this.getPosition());
429             //var frame = cc.rect(frameOriginal.x, frameOriginal.y, this._viewSize.width, this._viewSize.height);
430             var frame = this._getViewRect();
431 
432             //var newPoint = this.convertTouchToNodeSpace(this._touches[0]);
433             var newPoint = this.convertTouchToNodeSpace(touch);
434             var moveDistance = cc.pSub(newPoint, this._touchPoint);
435 
436             var dis = 0.0, locDirection = this._direction, pos;
437             if (locDirection === cc.SCROLLVIEW_DIRECTION_VERTICAL){
438                 dis = moveDistance.y;
439                 pos = this._container.getPositionY();
440                 if (!(this.minContainerOffset().y <= pos && pos <= this.maxContainerOffset().y))
441                     moveDistance.y *= BOUNCE_BACK_FACTOR;
442             } else if (locDirection === cc.SCROLLVIEW_DIRECTION_HORIZONTAL){
443                 dis = moveDistance.x;
444                 pos = this._container.getPositionX();
445                 if (!(this.minContainerOffset().x <= pos && pos <= this.maxContainerOffset().x))
446                     moveDistance.x *= BOUNCE_BACK_FACTOR;
447             }else {
448                 dis = Math.sqrt(moveDistance.x * moveDistance.x + moveDistance.y * moveDistance.y);
449 
450                 pos = this._container.getPositionY();
451                 var _minOffset = this.minContainerOffset(), _maxOffset = this.maxContainerOffset();
452                 if (!(_minOffset.y <= pos && pos <= _maxOffset.y))
453                     moveDistance.y *= BOUNCE_BACK_FACTOR;
454 
455                 pos = this._container.getPositionX();
456                 if (!(_minOffset.x <= pos && pos <= _maxOffset.x))
457                     moveDistance.x *= BOUNCE_BACK_FACTOR;
458             }
459 
460             if (!this._touchMoved && Math.abs(cc.convertDistanceFromPointToInch(dis)) < MOVE_INCH ){
461                 //CCLOG("Invalid movement, distance = [%f, %f], disInch = %f", moveDistance.x, moveDistance.y);
462                 return;
463             }
464 
465             if (!this._touchMoved){
466                 moveDistance.x = 0;
467                 moveDistance.y = 0;
468             }
469 
470             this._touchPoint = newPoint;
471             this._touchMoved = true;
472 
473             if (this._dragging) {
474                 switch (locDirection) {
475                     case cc.SCROLLVIEW_DIRECTION_VERTICAL:
476                         moveDistance.x = 0.0;
477                         break;
478                     case cc.SCROLLVIEW_DIRECTION_HORIZONTAL:
479                         moveDistance.y = 0.0;
480                         break;
481                     default:
482                         break;
483                 }
484 
485                 var locPosition = this._container.getPosition();
486                 var newX = locPosition.x + moveDistance.x;
487                 var newY = locPosition.y + moveDistance.y;
488 
489                 this._scrollDistance = moveDistance;
490                 this.setContentOffset(cc.p(newX, newY));
491             }
492         } else if (this._touches.length === 2 && !this._dragging) {
493             var len = cc.pDistance(this._container.convertTouchToNodeSpace(this._touches[0]),
494                 this._container.convertTouchToNodeSpace(this._touches[1]));
495             this.setZoomScale(this.getZoomScale() * len / this._touchLength);
496         }
497     },
498 
499     onTouchEnded:function (touch, event) {
500         if (!this.isVisible())
501             return;
502 
503         if (this._touches.length == 1 && this._touchMoved)
504             this.schedule(this._deaccelerateScrolling);
505 
506         this._touches.length = 0;
507         this._dragging = false;
508         this._touchMoved = false;
509     },
510 
511     onTouchCancelled:function (touch, event) {
512         if (!this.isVisible())
513             return;
514 
515         this._touches.length = 0;
516         this._dragging = false;
517         this._touchMoved = false;
518     },
519 
520     setContentSize: function (size, height) {
521         if (this.getContainer() != null) {
522             if(height === undefined)
523                 this.getContainer().setContentSize(size);
524             else
525                 this.getContainer().setContentSize(size, height);
526             this.updateInset();
527         }
528     },
529 	_setWidth: function (value) {
530 		var container = this.getContainer();
531 		if (container != null) {
532 			container._setWidth(value);
533 			this.updateInset();
534 		}
535 	},
536 	_setHeight: function (value) {
537 		var container = this.getContainer();
538 		if (container != null) {
539 			container._setHeight(value);
540 			this.updateInset();
541 		}
542 	},
543 
544     getContentSize:function () {
545         return this._container.getContentSize();
546     },
547 
548     updateInset:function () {
549         if (this.getContainer() != null) {
550             var locViewSize = this._viewSize;
551             var tempOffset = this.maxContainerOffset();
552             this._maxInset.x = tempOffset.x + locViewSize.width * INSET_RATIO;
553             this._maxInset.y = tempOffset.y + locViewSize.height * INSET_RATIO;
554             tempOffset = this.minContainerOffset();
555             this._minInset.x = tempOffset.x - locViewSize.width * INSET_RATIO;
556             this._minInset.y = tempOffset.y - locViewSize.height * INSET_RATIO;
557         }
558     },
559 
560     /**
561      * Determines whether it clips its children or not.
562      */
563     isClippingToBounds:function () {
564         return this._clippingToBounds;
565     },
566 
567     setClippingToBounds:function (clippingToBounds) {
568         this._clippingToBounds = clippingToBounds;
569     },
570 
571     visit:function (ctx) {
572         // quick return if not visible
573         if (!this.isVisible())
574             return;
575 
576         var context = ctx || cc._renderContext;
577         var i, locChildren = this._children, selChild, childrenLen;
578         if (cc._renderType === cc._RENDER_TYPE_CANVAS) {
579             context.save();
580             this.transform(context);
581             this._beforeDraw(context);
582 
583             if (locChildren && locChildren.length > 0) {
584                 childrenLen = locChildren.length;
585                 this.sortAllChildren();
586                 // draw children zOrder < 0
587                 for (i = 0; i < childrenLen; i++) {
588                     selChild = locChildren[i];
589                     if (selChild && selChild._localZOrder < 0)
590                         selChild.visit(context);
591                     else
592                         break;
593                 }
594 
595                 this.draw(context);             // self draw
596 
597                 // draw children zOrder >= 0
598                 for (; i < childrenLen; i++)
599                     locChildren[i].visit(context);
600             } else{
601                 this.draw(context);             // self draw
602             }
603 
604             this._afterDraw();
605 
606             context.restore();
607         } else {
608             cc.kmGLPushMatrix();
609             var locGrid = this.grid;
610             if (locGrid && locGrid.isActive()) {
611                 locGrid.beforeDraw();
612                 this.transformAncestors();
613             }
614 
615             this.transform(context);
616             this._beforeDraw(context);
617             if (locChildren && locChildren.length > 0) {
618                 childrenLen = locChildren.length;
619                 // draw children zOrder < 0
620                 for (i = 0; i < childrenLen; i++) {
621                     selChild = locChildren[i];
622                     if (selChild && selChild._localZOrder < 0)
623                         selChild.visit();
624                     else
625                         break;
626                 }
627 
628                 // this draw
629                 this.draw(context);
630 
631                 // draw children zOrder >= 0
632                 for (; i < childrenLen; i++)
633                     locChildren[i].visit();
634             } else{
635                 this.draw(context);
636             }
637 
638             this._afterDraw(context);
639             if (locGrid && locGrid.isActive())
640                 locGrid.afterDraw(this);
641 
642             cc.kmGLPopMatrix();
643         }
644     },
645 
646     addChild:function (child, zOrder, tag) {
647         if (!child)
648             throw new Error("child must not nil!");
649 
650         zOrder = zOrder || child.getLocalZOrder();
651         tag = tag || child.getTag();
652 
653         //child.ignoreAnchorPointForPosition(false);
654         //child.setAnchorPoint(0, 0);
655         if (this._container != child) {
656             this._container.addChild(child, zOrder, tag);
657         } else {
658             cc.Layer.prototype.addChild.call(this, child, zOrder, tag);
659         }
660     },
661 
662     isTouchEnabled: function(){
663         return this._touchListener != null;
664     },
665 
666     setTouchEnabled:function (e) {
667         if(this._touchListener)
668             cc.eventManager.removeListener(this._touchListener);
669         this._touchListener = null;
670         if (!e) {
671             this._dragging = false;
672             this._touchMoved = false;
673             this._touches.length = 0;
674         } else {
675             var listener = cc.EventListener.create({
676                 event: cc.EventListener.TOUCH_ONE_BY_ONE
677             });
678             if(this.onTouchBegan)
679                 listener.onTouchBegan = this.onTouchBegan.bind(this);
680             if(this.onTouchMoved)
681                 listener.onTouchMoved = this.onTouchMoved.bind(this);
682             if(this.onTouchEnded)
683                 listener.onTouchEnded = this.onTouchEnded.bind(this);
684             if(this.onTouchCancelled)
685                 listener.onTouchCancelled = this.onTouchCancelled.bind(this);
686             this._touchListener = listener;
687             cc.eventManager.addListener(listener, this);
688         }
689     },
690 
691     /**
692      * Init this object with a given size to clip its content.
693      *
694      * @param size view size
695      * @return initialized scroll view object
696      */
697     _initWithViewSize:function (size) {
698         return null;
699     },
700 
701     /**
702      * Relocates the container at the proper offset, in bounds of max/min offsets.
703      *
704      * @param animated If YES, relocation is animated
705      */
706     _relocateContainer:function (animated) {
707         var min = this.minContainerOffset();
708         var max = this.maxContainerOffset();
709         var locDirection = this._direction;
710 
711         var oldPoint = this._container.getPosition();
712         var newX = oldPoint.x;
713         var newY = oldPoint.y;
714         if (locDirection === cc.SCROLLVIEW_DIRECTION_BOTH || locDirection === cc.SCROLLVIEW_DIRECTION_HORIZONTAL) {
715             newX = Math.max(newX, min.x);
716             newX = Math.min(newX, max.x);
717         }
718 
719         if (locDirection == cc.SCROLLVIEW_DIRECTION_BOTH || locDirection == cc.SCROLLVIEW_DIRECTION_VERTICAL) {
720             newY = Math.min(newY, max.y);
721             newY = Math.max(newY, min.y);
722         }
723 
724         if (newY != oldPoint.y || newX != oldPoint.x) {
725             this.setContentOffset(cc.p(newX, newY), animated);
726         }
727     },
728     /**
729      * implements auto-scrolling behavior. change SCROLL_DEACCEL_RATE as needed to choose    <br/>
730      * deacceleration speed. it must be less than 1.0.
731      *
732      * @param {Number} dt delta
733      */
734     _deaccelerateScrolling:function (dt) {
735         if (this._dragging) {
736             this.unschedule(this._deaccelerateScrolling);
737             return;
738         }
739 
740         var maxInset, minInset;
741         var oldPosition = this._container.getPosition();
742         var locScrollDistance = this._scrollDistance;
743         this._container.setPosition(oldPosition.x + locScrollDistance.x , oldPosition.y + locScrollDistance.y);
744         if (this._bounceable) {
745             maxInset = this._maxInset;
746             minInset = this._minInset;
747         } else {
748             maxInset = this.maxContainerOffset();
749             minInset = this.minContainerOffset();
750         }
751 
752         //check to see if offset lies within the inset bounds
753         var newX = this._container.getPositionX();
754         var newY = this._container.getPositionY();
755         
756         locScrollDistance.x = locScrollDistance.x * SCROLL_DEACCEL_RATE;
757         locScrollDistance.y = locScrollDistance.y * SCROLL_DEACCEL_RATE;
758 
759         this.setContentOffset(cc.p(newX, newY));
760 
761         if ((Math.abs(locScrollDistance.x) <= SCROLL_DEACCEL_DIST &&
762             Math.abs(locScrollDistance.y) <= SCROLL_DEACCEL_DIST) ||
763             newY > maxInset.y || newY < minInset.y ||
764             newX > maxInset.x || newX < minInset.x ||
765             newX == maxInset.x || newX == minInset.x ||
766             newY == maxInset.y || newY == minInset.y) {
767             this.unschedule(this._deaccelerateScrolling);
768             this._relocateContainer(true);
769         }
770     },
771     /**
772      * This method makes sure auto scrolling causes delegate to invoke its method
773      */
774     _performedAnimatedScroll:function (dt) {
775         if (this._dragging) {
776             this.unschedule(this._performedAnimatedScroll);
777             return;
778         }
779 
780         if (this._delegate && this._delegate.scrollViewDidScroll)
781             this._delegate.scrollViewDidScroll(this);
782     },
783     /**
784      * Expire animated scroll delegate calls
785      */
786     _stoppedAnimatedScroll:function (node) {
787         this.unschedule(this._performedAnimatedScroll);
788         // After the animation stopped, "scrollViewDidScroll" should be invoked, this could fix the bug of lack of tableview cells.
789         if (this._delegate && this._delegate.scrollViewDidScroll) {
790             this._delegate.scrollViewDidScroll(this);
791         }
792     },
793 
794     /**
795      * clip this view so that outside of the visible bounds can be hidden.
796      */
797     _beforeDraw:function (context) {
798         if (this._clippingToBounds) {
799             this._scissorRestored = false;
800             var frame = this._getViewRect(), locEGLViewer = cc.view;
801 
802             var scaleX = this.getScaleX();
803             var scaleY = this.getScaleY();
804 
805             var ctx = context || cc._renderContext;
806             if (cc._renderType === cc._RENDER_TYPE_CANVAS) {
807                 var getWidth = (this._viewSize.width * scaleX) * locEGLViewer.getScaleX();
808                 var getHeight = (this._viewSize.height * scaleY) * locEGLViewer.getScaleY();
809                 var startX = 0;
810                 var startY = 0;
811 
812                 ctx.beginPath();
813                 ctx.rect(startX, startY, getWidth, -getHeight);
814                 ctx.clip();
815                 ctx.closePath();
816             } else {
817                 var EGLViewer = cc.view;
818                 if(EGLViewer.isScissorEnabled()){
819                     this._scissorRestored = true;
820                     this._parentScissorRect = EGLViewer.getScissorRect();
821                     //set the intersection of m_tParentScissorRect and frame as the new scissor rect
822                     if (cc.rectIntersection(frame, this._parentScissorRect)) {
823                         var locPSRect = this._parentScissorRect;
824                         var x = Math.max(frame.x, locPSRect.x);
825                         var y = Math.max(frame.y, locPSRect.y);
826                         var xx = Math.min(frame.x + frame.width, locPSRect.x + locPSRect.width);
827                         var yy = Math.min(frame.y + frame.height, locPSRect.y + locPSRect.height);
828                         EGLViewer.setScissorInPoints(x, y, xx - x, yy - y);
829                     }
830                 }else{
831                     ctx.enable(ctx.SCISSOR_TEST);
832                     //clip
833                     EGLViewer.setScissorInPoints(frame.x, frame.y, frame.width, frame.height);
834                 }
835             }
836         }
837     },
838     /**
839      * retract what's done in beforeDraw so that there's no side effect to
840      * other nodes.
841      */
842     _afterDraw:function (context) {
843         if (this._clippingToBounds && cc._renderType === cc._RENDER_TYPE_WEBGL) {
844             if (this._scissorRestored) {  //restore the parent's scissor rect
845                 var rect = this._parentScissorRect;
846                 cc.view.setScissorInPoints(rect.x, rect.y, rect.width, rect.height)
847             }else{
848                 var ctx = context || cc._renderContext;
849                 ctx.disable(ctx.SCISSOR_TEST);
850             }
851         }
852     },
853     /**
854      * Zoom handling
855      */
856     _handleZoom:function () {
857     },
858 
859     _getViewRect:function(){
860         var screenPos = this.convertToWorldSpace(cc.p(0,0));
861         var locViewSize = this._viewSize;
862 
863         var scaleX = this.getScaleX();
864         var scaleY = this.getScaleY();
865 
866         for (var p = this._parent; p != null; p = p.getParent()) {
867             scaleX *= p.getScaleX();
868             scaleY *= p.getScaleY();
869         }
870 
871         // Support negative scaling. Not doing so causes intersectsRect calls
872         // (eg: to check if the touch was within the bounds) to return false.
873         // Note, CCNode::getScale will assert if X and Y scales are different.
874         if (scaleX < 0) {
875             screenPos.x += locViewSize.width * scaleX;
876             scaleX = -scaleX;
877         }
878         if (scaleY < 0) {
879             screenPos.y += locViewSize.height * scaleY;
880             scaleY = -scaleY;
881         }
882 
883         var locViewRect = this._tmpViewRect;
884         locViewRect.x = screenPos.x;
885         locViewRect.y = screenPos.y;
886         locViewRect.width = locViewSize.width * scaleX;
887         locViewRect.height = locViewSize.height * scaleY;
888         return locViewRect;
889     }
890 });
891 
892 var _p = cc.ScrollView.prototype;
893 
894 // Extended properties
895 /** @expose */
896 _p.minOffset;
897 cc.defineGetterSetter(_p, "minOffset", _p.minContainerOffset);
898 /** @expose */
899 _p.maxOffset;
900 cc.defineGetterSetter(_p, "maxOffset", _p.maxContainerOffset);
901 /** @expose */
902 _p.bounceable;
903 cc.defineGetterSetter(_p, "bounceable", _p.isBounceable, _p.setBounceable);
904 /** @expose */
905 _p.viewSize;
906 cc.defineGetterSetter(_p, "viewSize", _p.getViewSize, _p.setViewSize);
907 /** @expose */
908 _p.container;
909 cc.defineGetterSetter(_p, "container", _p.getContainer, _p.setContainer);
910 /** @expose */
911 _p.direction;
912 cc.defineGetterSetter(_p, "direction", _p.getDirection, _p.setDirection);
913 /** @expose */
914 _p.delegate;
915 cc.defineGetterSetter(_p, "delegate", _p.getDelegate, _p.setDelegate);
916 /** @expose */
917 _p.clippingToBounds;
918 cc.defineGetterSetter(_p, "clippingToBounds", _p.isClippingToBounds, _p.setClippingToBounds);
919 
920 _p = null;
921 
922 /**
923  * Returns an autoreleased scroll view object.
924  *
925  * @param {cc.Size} size view size
926  * @param {cc.Node} container parent object
927  * @return {cc.ScrollView} scroll view object
928  */
929 cc.ScrollView.create = function (size, container) {
930     var pRet = new cc.ScrollView();
931     if (arguments.length == 2) {
932         if (pRet && pRet.initWithViewSize(size, container))
933             return pRet;
934     } else {
935         if (pRet && pRet.init())
936             return pRet;
937     }
938     return null;
939 };