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