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