1 /****************************************************************************
  2  Copyright (c) 2011-2012 cocos2d-x.org
  3  Copyright (c) 2013-2014 Chukong Technologies Inc.
  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  * The list view control of Cocos UI.
 28  * @class
 29  * @extends ccui.ScrollView
 30  * @example
 31  * var listView = new ccui.ListView();
 32  * // set list view ex direction
 33  * listView.setDirection(ccui.ScrollView.DIR_VERTICAL);
 34  * listView.setTouchEnabled(true);
 35  * listView.setBounceEnabled(true);
 36  * listView.setBackGroundImage("res/cocosui/green_edit.png");
 37  * listView.setBackGroundImageScale9Enabled(true);
 38  * listView.setContentSize(cc.size(240, 130));
 39  * this.addChild(listView);
 40  */
 41 ccui.ListView = ccui.ScrollView.extend(/** @lends ccui.ListView# */{
 42     _model: null,
 43     _items: null,
 44     _gravity: null,
 45     _itemsMargin: 0,
 46 
 47     _curSelectedIndex: 0,
 48     _refreshViewDirty: true,
 49 
 50     _listViewEventListener: null,
 51     _listViewEventSelector: null,
 52     _ccListViewEventCallback: null,
 53     _magneticAllowedOutOfBoundary: true,
 54     _magneticType: 0,
 55     _className:"ListView",
 56 
 57     /**
 58      * allocates and initializes a UIListView.
 59      * Constructor of ccui.ListView, override it to extend the construction behavior, remember to call "this._super()" in the extended "ctor" function.
 60      * @example
 61      * // example
 62      * var aListView = new ccui.ListView();
 63      */
 64     ctor: function () {
 65         this._items = [];
 66         ccui.ScrollView.prototype.ctor.call(this);
 67         this._gravity = ccui.ListView.GRAVITY_CENTER_VERTICAL;
 68         this.setTouchEnabled(true);
 69         this.setDirection(ccui.ScrollView.DIR_VERTICAL);
 70     },
 71 
 72     /**
 73      * Sets a item model for ListView. A model will be cloned for adding default item.
 74      * @param {ccui.Widget} model
 75      */
 76     setItemModel: function (model) {
 77         if (!model){
 78             cc.log("Can't set a null to item model!");
 79             return;
 80         }
 81 
 82         this._model = model;
 83     },
 84 
 85     _handleReleaseLogic: function(touch)
 86     {
 87         ccui.ScrollView.prototype._handleReleaseLogic.call(this, touch);
 88 
 89         if(!this._autoScrolling)
 90         {
 91             this._startMagneticScroll();
 92         }
 93     },
 94 
 95     _onItemListChanged: function()
 96     {
 97         this._outOfBoundaryAmountDirty = true;
 98     },
 99 
100     _updateInnerContainerSize: function () {
101         var locItems = this._items, length, i;
102         switch (this.direction) {
103             case ccui.ScrollView.DIR_VERTICAL:
104                 length = locItems.length;
105                 var totalHeight = (length - 1) * this._itemsMargin;
106                 for (i = 0; i < length; i++) {
107                     totalHeight += locItems[i].getContentSize().height;
108                 }
109                 this.setInnerContainerSize(cc.size(this._contentSize.width, totalHeight));
110                 break;
111             case ccui.ScrollView.DIR_HORIZONTAL:
112                 length = locItems.length;
113                 var totalWidth = (length - 1) * this._itemsMargin;
114                 for (i = 0; i < length; i++) {
115                     totalWidth += locItems[i].getContentSize().width;
116                 }
117                 this.setInnerContainerSize(cc.size(totalWidth, this._contentSize.height));
118                 break;
119             default:
120                 break;
121         }
122     },
123 
124     _remedyLayoutParameter: function (item) {
125         cc.assert(null != item, "ListView Item can't be nil!");
126 
127         var linearLayoutParameter = item.getLayoutParameter(ccui.LayoutParameter.LINEAR);
128         var isLayoutParameterExists = true;
129         if (!linearLayoutParameter) {
130             linearLayoutParameter = new ccui.LinearLayoutParameter();
131             isLayoutParameterExists = false;
132         }
133         var itemIndex = this.getIndex(item);
134         switch (this.direction) {
135             case ccui.ScrollView.DIR_VERTICAL:
136                 this._remedyVerticalLayoutParameter(linearLayoutParameter, itemIndex);
137                 break;
138             case ccui.ScrollView.DIR_HORIZONTAL:
139                 this._remedyHorizontalLayoutParameter(linearLayoutParameter, itemIndex);
140                 break;
141             default:
142                 break;
143         }
144         if (!isLayoutParameterExists)
145             item.setLayoutParameter(linearLayoutParameter);
146     },
147 
148     //@since v3.3
149     _remedyVerticalLayoutParameter: function (layoutParameter, itemIndex) {
150         cc.assert(null != layoutParameter, "Layout parameter can't be nil!");
151 
152         switch (this._gravity) {
153             case ccui.ListView.GRAVITY_LEFT:
154                 layoutParameter.setGravity(ccui.LinearLayoutParameter.LEFT);
155                 break;
156             case ccui.ListView.GRAVITY_RIGHT:
157                 layoutParameter.setGravity(ccui.LinearLayoutParameter.RIGHT);
158                 break;
159             case ccui.ListView.GRAVITY_CENTER_HORIZONTAL:
160                 layoutParameter.setGravity(ccui.LinearLayoutParameter.CENTER_HORIZONTAL);
161                 break;
162             default:
163                 break;
164         }
165         if (0 === itemIndex)
166             layoutParameter.setMargin(ccui.MarginZero());
167         else
168             layoutParameter.setMargin(new ccui.Margin(0.0, this._itemsMargin, 0.0, 0.0));
169     },
170 
171     //@since v3.3
172     _remedyHorizontalLayoutParameter: function (layoutParameter, itemIndex) {
173         cc.assert(null != layoutParameter, "Layout parameter can't be nil!");
174 
175         switch (this._gravity) {
176             case ccui.ListView.GRAVITY_TOP:
177                 layoutParameter.setGravity(ccui.LinearLayoutParameter.TOP);
178                 break;
179             case ccui.ListView.GRAVITY_BOTTOM:
180                 layoutParameter.setGravity(ccui.LinearLayoutParameter.BOTTOM);
181                 break;
182             case ccui.ListView.GRAVITY_CENTER_VERTICAL:
183                 layoutParameter.setGravity(ccui.LinearLayoutParameter.CENTER_VERTICAL);
184                 break;
185             default:
186                 break;
187         }
188         if (0 === itemIndex)
189             layoutParameter.setMargin(ccui.MarginZero());
190         else
191             layoutParameter.setMargin(new ccui.Margin(this._itemsMargin, 0.0, 0.0, 0.0));
192     },
193 
194     /**
195      * Push back a default item(create by a cloned model) into ListView.
196      */
197     pushBackDefaultItem: function () {
198         if (this._model == null)
199             return;
200         var newItem = this._model.clone();
201         this._remedyLayoutParameter(newItem);
202         this.addChild(newItem);
203         this._refreshViewDirty = true;
204     },
205 
206     /**
207      * Insert a default item(create by a cloned model) into ListView.
208      * @param {Number} index
209      */
210     insertDefaultItem: function (index) {
211         if (this._model == null)
212             return;
213         var newItem = this._model.clone();
214         this._items.splice(index, 0, newItem);
215         ccui.ScrollView.prototype.addChild.call(this, newItem);
216         this._remedyLayoutParameter(newItem);
217 
218         this._refreshViewDirty = true;
219     },
220 
221     /**
222      * Push back custom item into ListView.
223      * @param {ccui.Widget} item
224      */
225     pushBackCustomItem: function (item) {
226         this._remedyLayoutParameter(item);
227         this.addChild(item);
228         this._refreshViewDirty = true;
229     },
230 
231     /**
232      * add child to ListView
233      * @override
234      * @param {cc.Node} widget
235      * @param {Number} [zOrder]
236      * @param {Number|String} [tag]  tag or name
237      */
238     addChild: function (widget, zOrder, tag) {
239         if (widget) {
240             zOrder = zOrder || widget.getLocalZOrder();
241             tag = tag || widget.getName();
242             ccui.ScrollView.prototype.addChild.call(this, widget, zOrder, tag);
243             if(widget instanceof ccui.Widget)
244             {
245                 this._items.push(widget);
246                 this._onItemListChanged();
247             }
248         }
249     },
250 
251     /**
252      * remove child from ListView
253      * @override
254      * @param {cc.Node} widget
255      * @param {Boolean} [cleanup=true]
256      */
257     removeChild: function(widget, cleanup){
258         if (widget) {
259             var index = this._items.indexOf(widget);
260             if(index > -1)
261                 this._items.splice(index, 1);
262 
263             this._onItemListChanged();
264 
265             ccui.ScrollView.prototype.removeChild.call(this, widget, cleanup);
266         }
267     },
268 
269     /**
270      * Removes all children from ccui.ListView.
271      */
272     removeAllChildren: function(){
273         this.removeAllChildrenWithCleanup(true);
274     },
275 
276     /**
277      * Removes all children from ccui.ListView and do a cleanup all running actions depending on the cleanup parameter.
278      * @param {Boolean} cleanup
279      */
280     removeAllChildrenWithCleanup: function(cleanup){
281         ccui.ScrollView.prototype.removeAllChildrenWithCleanup.call(this, cleanup);
282         this._items = [];
283 
284         this._onItemListChanged();
285     },
286 
287     /**
288      * Push back custom item into ccui.ListView.
289      * @param {ccui.Widget} item
290      * @param {Number} index
291      */
292     insertCustomItem: function (item, index) {
293         this._items.splice(index, 0, item);
294 
295         this._onItemListChanged();
296         ccui.ScrollView.prototype.addChild.call(this, item);
297         this._remedyLayoutParameter(item);
298         this._refreshViewDirty = true;
299     },
300 
301     /**
302      * Removes a item whose index is same as the parameter.
303      * @param {Number} index
304      */
305     removeItem: function (index) {
306         var item = this.getItem(index);
307         if (item == null)
308             return;
309         this.removeChild(item, true);
310         this._refreshViewDirty = true;
311     },
312 
313     /**
314      * Removes the last item of ccui.ListView.
315      */
316     removeLastItem: function () {
317         this.removeItem(this._items.length - 1);
318     },
319 
320     /**
321      * Removes all items from ccui.ListView.
322      */
323     removeAllItems: function(){
324         this.removeAllChildren();
325     },
326 
327     /**
328      * Returns a item whose index is same as the parameter.
329      * @param {Number} index
330      * @returns {ccui.Widget}
331      */
332     getItem: function (index) {
333         if (index < 0 || index >= this._items.length)
334             return null;
335         return this._items[index];
336     },
337 
338     /**
339      * Returns the item container.
340      * @returns {Array}
341      */
342     getItems: function () {
343         return this._items;
344     },
345 
346     /**
347      * Returns the index of item.
348      * @param {ccui.Widget} item the item which need to be checked.
349      * @returns {Number} the index of item.
350      */
351     getIndex: function (item) {
352         if(item == null)
353             return -1;
354         return this._items.indexOf(item);
355     },
356 
357     /**
358      * Changes the gravity of ListView.
359      * @param {ccui.ListView.GRAVITY_LEFT|ccui.ListView.GRAVITY_RIGHT|ccui.ListView.GRAVITY_CENTER_HORIZONTAL|ccui.ListView.GRAVITY_BOTTOM|ccui.ListView.GRAVITY_CENTER_VERTICAL} gravity
360      */
361     setGravity: function (gravity) {
362         if (this._gravity === gravity)
363             return;
364         this._gravity = gravity;
365         this._refreshViewDirty = true;
366     },
367 
368     /**
369      * Set magnetic type of ListView.
370      * @param {ccui.ListView.MAGNETIC_NONE|ccui.ListView.MAGNETIC_CENTER,ccui.ListView.MAGNETIC_BOTH_END|ccui.ListView.MAGNETIC_LEFT|ccui.ListView.MAGNETIC_RIGHT|ccui.ListView.MAGNETIC_TOP|ccui.ListView.MAGNETIC_BOTTOM} magneticType
371      */
372     setMagneticType: function(magneticType)
373     {
374         this._magneticType = magneticType;
375         this._onItemListChanged();
376         this._startMagneticScroll();
377     },
378 
379     /**
380      * Get magnetic type of ListView.
381      * @returns {number}
382      */
383     getMagneticType: function()
384     {
385         return this._magneticType;
386     },
387 
388     /**
389      * Set magnetic allowed out of boundary.
390      * @param {boolean} magneticAllowedOutOfBoundary
391      */
392     setMagneticAllowedOutOfBoundary: function(magneticAllowedOutOfBoundary)
393     {
394         this._magneticAllowedOutOfBoundary = magneticAllowedOutOfBoundary;
395     },
396 
397     /**
398      * Query whether the magnetic out of boundary is allowed.
399      * @returns {boolean}
400      */
401     getMagneticAllowedOutOfBoundary: function()
402     {
403         return this._magneticAllowedOutOfBoundary;
404     },
405 
406     /**
407      * Changes the margin between each item.
408      * @param {Number} margin
409      */
410     setItemsMargin: function (margin) {
411         if (this._itemsMargin === margin)
412             return;
413         this._itemsMargin = margin;
414         this._refreshViewDirty = true;
415     },
416 
417     /**
418      * Returns the margin between each item.
419      * @returns {Number}
420      */
421     getItemsMargin:function(){
422         return this._itemsMargin;
423     },
424 
425     /**
426      * Changes scroll direction of ccui.ListView.
427      * @param {ccui.ScrollView.DIR_NONE | ccui.ScrollView.DIR_VERTICAL | ccui.ScrollView.DIR_HORIZONTAL | ccui.ScrollView.DIR_BOTH} dir
428      */
429     setDirection: function (dir) {
430         switch (dir) {
431             case ccui.ScrollView.DIR_VERTICAL:
432                 this.setLayoutType(ccui.Layout.LINEAR_VERTICAL);
433                 break;
434             case ccui.ScrollView.DIR_HORIZONTAL:
435                 this.setLayoutType(ccui.Layout.LINEAR_HORIZONTAL);
436                 break;
437             case ccui.ScrollView.DIR_BOTH:
438                 return;
439             default:
440                 return;
441                 break;
442         }
443         ccui.ScrollView.prototype.setDirection.call(this, dir);
444     },
445 
446     _getHowMuchOutOfBoundary: function(addition)
447     {
448         if(addition === undefined)
449             addition = cc.p(0, 0);
450 
451         if(!this._magneticAllowedOutOfBoundary || this._items.length === 0)
452         {
453             return ccui.ScrollView.prototype._getHowMuchOutOfBoundary.call(this, addition);
454         }
455         else if(this._magneticType === ccui.ListView.MAGNETIC_NONE || this._magneticType === ccui.ListView.MAGNETIC_BOTH_END)
456         {
457             return ccui.ScrollView.prototype._getHowMuchOutOfBoundary.call(this, addition);
458         }
459         else if(addition.x === 0 && addition.y === 0 && !this._outOfBoundaryAmountDirty)
460         {
461             return this._outOfBoundaryAmount;
462         }
463 
464         // If it is allowed to be out of boundary by magnetic, adjust the boundaries according to the magnetic type.
465         var leftBoundary = this._leftBoundary;
466         var rightBoundary = this._rightBoundary;
467         var topBoundary = this._topBoundary;
468         var bottomBoundary = this._bottomBoundary;
469 
470         var lastItemIndex = this._items.length - 1;
471         var contentSize = this.getContentSize();
472         var firstItemAdjustment = cc.p(0, 0);
473         var lastItemAdjustment = cc.p(0, 0);
474 
475         switch (this._magneticType)
476         {
477             case  ccui.ListView.MAGNETIC_CENTER:
478                 firstItemAdjustment.x = (contentSize.width - this._items[0].width) / 2;
479                 firstItemAdjustment.y = (contentSize.height - this._items[0].height) / 2;
480 
481                 lastItemAdjustment.x = (contentSize.width - this._items[lastItemIndex].width) / 2;
482                 lastItemAdjustment.y = (contentSize.height - this._items[lastItemIndex].height) / 2;
483 
484                 break;
485             case ccui.ListView.MAGNETIC_LEFT:
486             case ccui.ListView.MAGNETIC_TOP:
487                 lastItemAdjustment.x = contentSize.width - this._items[lastItemIndex].width;
488                 lastItemAdjustment.y = contentSize.height - this._items[lastItemIndex].height;
489                 break;
490             case ccui.ListView.MAGNETIC_RIGHT:
491             case ccui.ListView.MAGNETIC_BOTTOM:
492                 firstItemAdjustment.x = contentSize.width - this._items[0].width;
493                 firstItemAdjustment.y = contentSize.height - this._items[0].height;
494                 break;
495         }
496 
497         leftBoundary += firstItemAdjustment.x;
498         rightBoundary -= lastItemAdjustment.x;
499         topBoundary -= firstItemAdjustment.y;
500         bottomBoundary += lastItemAdjustment.y;
501 
502 
503         // Calculate the actual amount
504         var outOfBoundaryAmount = cc.p(0, 0);
505 
506         if(this._innerContainer.getLeftBoundary() + addition.x > leftBoundary)
507         {
508             outOfBoundaryAmount.x = leftBoundary - (this._innerContainer.getLeftBoundary() + addition.x);
509         }
510         else if(this._innerContainer.getRightBoundary() + addition.x < rightBoundary)
511         {
512             outOfBoundaryAmount.x = rightBoundary - (this._innerContainer.getRightBoundary() + addition.x);
513         }
514 
515         if(this._innerContainer.getTopBoundary() + addition.y < topBoundary)
516         {
517             outOfBoundaryAmount.y = topBoundary - (this._innerContainer.getTopBoundary() + addition.y);
518         }
519         else if(this._innerContainer.getBottomBoundary() + addition.y > bottomBoundary)
520         {
521             outOfBoundaryAmount.y = bottomBoundary - (this._innerContainer.getBottomBoundary() + addition.y);
522         }
523 
524         if(addition.x === 0 && addition.y === 0)
525         {
526             this._outOfBoundaryAmount = outOfBoundaryAmount;
527             this._outOfBoundaryAmountDirty = false;
528         }
529         return outOfBoundaryAmount;
530     },
531 
532     _calculateItemPositionWithAnchor: function(item, itemAnchorPoint)
533     {
534         var origin = cc.p(item.getLeftBoundary(), item.getBottomBoundary());
535         var size = item.getContentSize();
536 
537         return cc.p(origin. x + size.width * itemAnchorPoint.x, origin.y + size.height * itemAnchorPoint.y);
538     },
539 
540     _findClosestItem: function(targetPosition, items, itemAnchorPoint, firstIndex, distanceFromFirst, lastIndex, distanceFromLast)
541     {
542         cc.assert(firstIndex >= 0 && lastIndex < items.length && firstIndex <= lastIndex, "");
543         if (firstIndex === lastIndex)
544         {
545             return items[firstIndex];
546         }
547         if (lastIndex - firstIndex === 1)
548         {
549             if (distanceFromFirst <= distanceFromLast)
550             {
551                 return items[firstIndex];
552             }
553             else
554             {
555                 return items[lastIndex];
556             }
557         }
558 
559         // Binary search
560         var midIndex = Math.floor((firstIndex + lastIndex) / 2);
561         var itemPosition = this._calculateItemPositionWithAnchor(items[midIndex], itemAnchorPoint);
562         var distanceFromMid = cc.pLength(cc.pSub(targetPosition, itemPosition));
563 
564         if (distanceFromFirst <= distanceFromLast)
565         {
566             // Left half
567             return this._findClosestItem(targetPosition, items, itemAnchorPoint, firstIndex, distanceFromFirst, midIndex, distanceFromMid);
568         }
569         else
570         {
571             // Right half
572             return this._findClosestItem(targetPosition, items, itemAnchorPoint, midIndex, distanceFromMid, lastIndex, distanceFromLast);
573         }
574     },
575 
576     /**
577      * Query the closest item to a specific position in inner container.
578      *
579      * @param {cc.Point} targetPosition Specifies the target position in inner container's coordinates.
580      * @param {cc.Point} itemAnchorPoint Specifies an anchor point of each item for position to calculate distance.
581      * @returns {?ccui.Widget} A item instance if list view is not empty. Otherwise, returns null.
582      */
583     getClosestItemToPosition: function(targetPosition, itemAnchorPoint)
584     {
585         if (this._items.length === 0)
586         {
587             return null;
588         }
589 
590         // Find the closest item through binary search
591         var firstIndex = 0;
592         var firstPosition = this._calculateItemPositionWithAnchor(this._items[firstIndex], itemAnchorPoint);
593         var distanceFromFirst = cc.pLength(cc.pSub(targetPosition, firstPosition));
594 
595         var lastIndex = this._items.length - 1;
596         var lastPosition = this._calculateItemPositionWithAnchor(this._items[lastIndex], itemAnchorPoint);
597         var distanceFromLast = cc.pLength(cc.pSub(targetPosition, lastPosition));
598 
599         return this._findClosestItem(targetPosition, this._items, itemAnchorPoint, firstIndex, distanceFromFirst, lastIndex, distanceFromLast);
600     },
601 
602     /**
603      * Query the closest item to a specific position in current view.<br/>
604      * For instance, to find the item in the center of view, call 'getClosestItemToPositionInCurrentView(cc.p(0.5, 0.5), cc.p(0.5, 0.5))'.
605      *
606      * @param {cc.Point} positionRatioInView Specifies the target position with ratio in list view's content size.
607      * @param {cc.Point} itemAnchorPoint Specifies an anchor point of each item for position to calculate distance.
608      * @returns {?ccui.Widget} A item instance if list view is not empty. Otherwise, returns null.
609      */
610 
611     getClosestItemToPositionInCurrentView: function(positionRatioInView, itemAnchorPoint)
612     {
613         // Calculate the target position
614         var contentSize = this.getContentSize();
615         var targetPosition = cc.pMult(this._innerContainer.getPosition(), -1);
616         targetPosition.x += contentSize.width * positionRatioInView.x;
617         targetPosition.y += contentSize.height * positionRatioInView.y;
618 
619         return this.getClosestItemToPosition(targetPosition, itemAnchorPoint);
620     },
621 
622     /**
623      * Query the center item
624      * @returns {?ccui.Widget} A item instance.
625      */
626     getCenterItemInCurrentView: function()
627     {
628         return this.getClosestItemToPositionInCurrentView(cc.p(0.5, 0.5), cc.p(0.5, 0.5));
629     },
630 
631     /**
632      * Query the leftmost item in horizontal list
633      * @returns {?ccui.Widget} A item instance.
634      */
635     getLeftmostItemInCurrentView: function()
636     {
637         if(this._direction === ccui.ScrollView.DIR_HORIZONTAL)
638         {
639             return this.getClosestItemToPositionInCurrentView(cc.p(0, 0.5), cc.p(0.5, 0.5));
640         }
641 
642         return null;
643     },
644 
645     /**
646      * Query the rightmost item in horizontal list
647      * @returns {?ccui.Widget} A item instance.
648      */
649     getRightmostItemInCurrentView: function()
650     {
651         if(this._direction === ccui.ScrollView.DIR_HORIZONTAL)
652         {
653             return this.getClosestItemToPositionInCurrentView(cc.p(1, 0.5), cc.p(0.5, 0.5));
654         }
655 
656         return null;
657     },
658 
659     /**
660      * Query the topmost item in horizontal list
661      * @returns {?ccui.Widget} A item instance.
662      */
663     getTopmostItemInCurrentView: function()
664     {
665         if(this._direction === ccui.ScrollView.DIR_VERTICAL)
666         {
667             return this.getClosestItemToPositionInCurrentView(cc.p(0.5, 1), cc.p(0.5, 0.5));
668         }
669 
670         return null;
671     },
672 
673     /**
674      * Query the topmost item in horizontal list
675      * @returns {?ccui.Widget} A item instance.
676      */
677     getBottommostItemInCurrentView: function()
678     {
679         if(this._direction === ccui.ScrollView.DIR_VERTICAL)
680         {
681             return this.getClosestItemToPositionInCurrentView(cc.p(0.5, 0), cc.p(0.5, 0.5));
682         }
683 
684         return null;
685     },
686 
687     _calculateItemDestination: function(positionRatioInView, item, itemAnchorPoint)
688     {
689         var contentSize = this.getContentSize();
690         var positionInView = cc.p(0, 0);
691         positionInView.x += contentSize.width * positionRatioInView.x;
692         positionInView.y += contentSize.height * positionRatioInView.y;
693 
694         var itemPosition = this._calculateItemPositionWithAnchor(item, itemAnchorPoint);
695         return cc.pMult(cc.pSub(itemPosition, positionInView), -1);
696     },
697 
698     jumpToBottom: function()
699     {
700         this.doLayout();
701         ccui.ScrollView.prototype.jumpToBottom.call(this);
702     },
703 
704     jumpToTop: function()
705     {
706         this.doLayout();
707         ccui.ScrollView.prototype.jumpToTop.call(this);
708     },
709 
710     jumpToLeft: function()
711     {
712         this.doLayout();
713         ccui.ScrollView.prototype.jumpToLeft.call(this);
714     },
715 
716     jumpToRight: function()
717     {
718         this.doLayout();
719         ccui.ScrollView.prototype.jumpToRight.call(this);
720     },
721 
722     jumpToTopLeft: function()
723     {
724         this.doLayout();
725         ccui.ScrollView.prototype.jumpToTopLeft.call(this);
726     },
727 
728     jumpToTopRight: function()
729     {
730         this.doLayout();
731         ccui.ScrollView.prototype.jumpToTopRight.call(this);
732     },
733 
734     jumpToBottomLeft: function()
735     {
736         this.doLayout();
737         ccui.ScrollView.prototype.jumpToBottomLeft.call(this);
738     },
739 
740     jumpToBottomRight: function()
741     {
742         this.doLayout();
743         ccui.ScrollView.prototype.jumpToBottomRight.call(this);
744     },
745 
746     jumpToPercentVertical: function(percent)
747     {
748         this.doLayout();
749         ccui.ScrollView.prototype.jumpToPercentVertical.call(this, percent);
750     },
751 
752     jumpToPercentHorizontal: function(percent)
753     {
754         this.doLayout();
755         ccui.ScrollView.prototype.jumpToPercentHorizontal.call(this, percent);
756     },
757 
758     jumpToPercentBothDirection: function(percent)
759     {
760         this.doLayout();
761         ccui.ScrollView.prototype.jumpToPercentBothDirection.call(this, percent);
762     },
763 
764     /**
765      * Jump to specific item
766      * @param {number} itemIndex Specifies the item's index
767      * @param {cc.Point} positionRatioInView Specifies the position with ratio in list view's content size.
768      * @param {cc.Point} itemAnchorPoint Specifies an anchor point of each item for position to calculate distance.
769      */
770     jumpToItem: function(itemIndex, positionRatioInView, itemAnchorPoint)
771     {
772         var item = this.getItem(itemIndex);
773 
774         if(!item)
775             return;
776 
777         this.doLayout();
778 
779         var destination = this._calculateItemDestination(positionRatioInView, item, itemAnchorPoint);
780 
781         if(!this.bounceEnabled)
782         {
783             var delta = cc.pSub(destination, this._innerContainer.getPosition());
784             var outOfBoundary = this._getHowMuchOutOfBoundary(delta);
785             destination.x += outOfBoundary.x;
786             destination.y += outOfBoundary.y;
787         }
788 
789         this._jumpToDestination(destination);
790     },
791 
792     /**
793      * Scroll to specific item
794      * @param {number} itemIndex Specifies the item's index
795      * @param {cc.Point} positionRatioInView Specifies the position with ratio in list view's content size.
796      * @param {cc.Point} itemAnchorPoint Specifies an anchor point of each item for position to calculate distance.
797      * @param {number} [timeInSec = 1.0] Scroll time
798      */
799     scrollToItem: function(itemIndex, positionRatioInView, itemAnchorPoint, timeInSec)
800     {
801         if(timeInSec === undefined)
802             timeInSec = 1;
803 
804         var item = this.getItem(itemIndex);
805 
806         if(!item)
807             return;
808 
809         var destination = this._calculateItemDestination(positionRatioInView, item, itemAnchorPoint);
810         this._startAutoScrollToDestination(destination, timeInSec, true);
811     },
812 
813     /**
814      * Requests refresh list view.
815      * @deprecated Use method requestDoLayout() instead
816      */
817     requestRefreshView: function () {
818         this._refreshViewDirty = true;
819     },
820 
821     /**
822      * Refreshes list view.
823      * @deprecated Use method forceDoLayout() instead
824      */
825     refreshView: function () {
826         this.forceDoLayout()
827     },
828 
829     /**
830      * provides a public _doLayout function for Editor. it calls _doLayout.
831      */
832     doLayout: function(){
833         this._doLayout();
834     },
835 
836     requestDoLayout: function()
837     {
838         this._refreshViewDirty = true;
839     },
840 
841     _doLayout: function(){
842         //ccui.Layout.prototype._doLayout.call(this);
843         if (this._refreshViewDirty) {
844             var locItems = this._items;
845             for (var i = 0; i < locItems.length; i++) {
846                 var item = locItems[i];
847                 item.setLocalZOrder(i);
848                 this._remedyLayoutParameter(item);
849             }
850             this._updateInnerContainerSize();
851             this._innerContainer.forceDoLayout();
852             this._refreshViewDirty = false;
853         }
854     },
855 
856     /**
857      * Adds event listener to ccui.ListView.
858      * @param {Function} selector
859      * @param {Object} [target=]
860      * @deprecated since v3.0, please use addEventListener instead.
861      */
862     addEventListenerListView: function (selector, target) {
863         this._listViewEventListener = target;
864         this._listViewEventSelector = selector;
865     },
866 
867     /**
868      * Adds callback function called ListView event triggered
869      * @param {Function} selector
870      */
871     addEventListener: function(selector){
872         this._ccListViewEventCallback = selector;
873     },
874 
875     _selectedItemEvent: function (event) {
876         var eventEnum = (event === ccui.Widget.TOUCH_BEGAN) ? ccui.ListView.ON_SELECTED_ITEM_START : ccui.ListView.ON_SELECTED_ITEM_END;
877         if(this._listViewEventSelector){
878             if (this._listViewEventListener)
879                 this._listViewEventSelector.call(this._listViewEventListener, this, eventEnum);
880             else
881                 this._listViewEventSelector(this, eventEnum);
882         }
883         if(this._ccListViewEventCallback)
884             this._ccListViewEventCallback(this, eventEnum);
885     },
886 
887     /**
888      * Intercept touch event, handle its child's touch event.
889      * @param {Number} eventType
890      * @param {ccui.Widget} sender
891      * @param {cc.Touch} touch
892      */
893     interceptTouchEvent: function (eventType, sender, touch) {
894         ccui.ScrollView.prototype.interceptTouchEvent.call(this, eventType, sender, touch);
895         if (!this._touchEnabled) {
896             return;
897         }
898         if (eventType !== ccui.Widget.TOUCH_MOVED) {
899             var parent = sender;
900             while (parent) {
901                 if (parent && parent.getParent() === this._innerContainer) {
902                     this._curSelectedIndex = this.getIndex(parent);
903                     break;
904                 }
905                 parent = parent.getParent();
906             }
907             if (sender.isHighlighted())
908                 this._selectedItemEvent(eventType);
909         }
910     },
911 
912     /**
913      * Returns current selected index
914      * @returns {number}
915      */
916     getCurSelectedIndex: function () {
917         return this._curSelectedIndex;
918     },
919 
920     _onSizeChanged: function () {
921         ccui.ScrollView.prototype._onSizeChanged.call(this);
922         this._refreshViewDirty = true;
923     },
924 
925     /**
926      * Returns the "class name" of ccui.ListView.
927      * @returns {string}
928      */
929     getDescription: function () {
930         return "ListView";
931     },
932 
933     _createCloneInstance: function () {
934         return new ccui.ListView();
935     },
936 
937     _copyClonedWidgetChildren: function (model) {
938         var arrayItems = model.getItems();
939         for (var i = 0; i < arrayItems.length; i++) {
940             var item = arrayItems[i];
941             this.pushBackCustomItem(item.clone());
942         }
943     },
944 
945     _copySpecialProperties: function (listView) {
946         if(listView instanceof ccui.ListView){
947             ccui.ScrollView.prototype._copySpecialProperties.call(this, listView);
948             this.setItemModel(listView._model);
949             this.setItemsMargin(listView._itemsMargin);
950             this.setGravity(listView._gravity);
951 
952             this._listViewEventListener = listView._listViewEventListener;
953             this._listViewEventSelector = listView._listViewEventSelector;
954         }
955     },
956 
957     _startAttenuatingAutoScroll: function(deltaMove, initialVelocity)
958     {
959         var adjustedDeltaMove = deltaMove;
960 
961         if(this._items.length !== 0 && this._magneticType !== ccui.ListView.MAGNETIC_NONE)
962         {
963             adjustedDeltaMove = this._flattenVectorByDirection(adjustedDeltaMove);
964 
965             var howMuchOutOfBoundary = this._getHowMuchOutOfBoundary(adjustedDeltaMove);
966             // If the destination is out of boundary, do nothing here. Because it will be handled by bouncing back.
967             if(howMuchOutOfBoundary.x === 0 && howMuchOutOfBoundary.y === 0 )
968             {
969                 var magType = this._magneticType;
970                 if(magType === ccui.ListView.MAGNETIC_BOTH_END)
971                 {
972                     if(this._direction === ccui.ScrollView.DIR_HORIZONTAL)
973                     {
974                         magType = (adjustedDeltaMove.x > 0 ? ccui.ListView.MAGNETIC_LEFT : ccui.ListView.MAGNETIC_RIGHT);
975                     }
976                     else if(this._direction === ccui.ScrollView.DIR_VERTICAL)
977                     {
978                         magType = (adjustedDeltaMove.y > 0 ? ccui.ListView.MAGNETIC_BOTTOM : ccui.ListView.MAGNETIC_TOP);
979                     }
980                 }
981 
982                 // Adjust the delta move amount according to the magnetic type
983                 var magneticAnchorPoint = this._getAnchorPointByMagneticType(magType);
984                 var magneticPosition = cc.pMult(this._innerContainer.getPosition(), -1);
985                 magneticPosition.x += this.width * magneticAnchorPoint.x;
986                 magneticPosition.y += this.height * magneticAnchorPoint.y;
987 
988                 var pTargetItem = this.getClosestItemToPosition(cc.pSub(magneticPosition, adjustedDeltaMove), magneticAnchorPoint);
989                 var itemPosition = this._calculateItemPositionWithAnchor(pTargetItem, magneticAnchorPoint);
990                 adjustedDeltaMove = cc.pSub(magneticPosition, itemPosition);
991             }
992         }
993         ccui.ScrollView.prototype._startAttenuatingAutoScroll.call(this,adjustedDeltaMove, initialVelocity);
994     },
995 
996     _getAnchorPointByMagneticType: function(magneticType)
997     {
998         switch(magneticType)
999         {
1000             case ccui.ListView.MAGNETIC_NONE: return cc.p(0, 0);
1001             case ccui.ListView.MAGNETIC_BOTH_END: return cc.p(0, 1);
1002             case ccui.ListView.MAGNETIC_CENTER: return cc.p(0.5, 0.5);
1003             case ccui.ListView.MAGNETIC_LEFT: return cc.p(0, 0.5);
1004             case ccui.ListView.MAGNETIC_RIGHT: return cc.p(1, 0.5);
1005             case ccui.ListView.MAGNETIC_TOP: return cc.p(0.5, 1);
1006             case ccui.ListView.MAGNETIC_BOTTOM: return cc.p(0.5, 0);
1007         }
1008 
1009         return cc.p(0, 0);
1010     },
1011 
1012     _startMagneticScroll: function()
1013     {
1014         if(this._items.length === 0 || this._magneticType === ccui.ListView.MAGNETIC_NONE)
1015         {
1016             return;
1017         }
1018 
1019         // Find the closest item
1020         var magneticAnchorPoint =this._getAnchorPointByMagneticType(this._magneticType);
1021         var magneticPosition = cc.pMult(this._innerContainer.getPosition(), -1);
1022         magneticPosition.x += this.width * magneticAnchorPoint.x;
1023         magneticPosition.y += this.height * magneticAnchorPoint.y;
1024 
1025         var pTargetItem = this.getClosestItemToPosition(magneticPosition, magneticAnchorPoint);
1026         this.scrollToItem(this.getIndex(pTargetItem), magneticAnchorPoint, magneticAnchorPoint);
1027     }
1028 });
1029 
1030 /**
1031  * allocates and initializes a UIListView.
1032  * @deprecated since v3.0, please use new ccui.ListView() instead.
1033  */
1034 ccui.ListView.create = function () {
1035     return new ccui.ListView();
1036 };
1037 
1038 // Constants
1039 //listView event type
1040 /**
1041  * The flag selected item of ccui.ListView's event.
1042  * @constant
1043  * @type {number}
1044  */
1045 ccui.ListView.EVENT_SELECTED_ITEM = 0;
1046 
1047 /**
1048  * The flag selected item start of ccui.ListView's event.
1049  * @constant
1050  * @type {number}
1051  */
1052 ccui.ListView.ON_SELECTED_ITEM_START = 0;
1053 /**
1054  * The flag selected item end of ccui.ListView's event.
1055  * @constant
1056  * @type {number}
1057  */
1058 ccui.ListView.ON_SELECTED_ITEM_END = 1;
1059 
1060 //listView gravity
1061 /**
1062  * The left flag of ccui.ListView's gravity.
1063  * @constant
1064  * @type {number}
1065  */
1066 ccui.ListView.GRAVITY_LEFT = 0;
1067 /**
1068  * The right flag of ccui.ListView's gravity.
1069  * @constant
1070  * @type {number}
1071  */
1072 ccui.ListView.GRAVITY_RIGHT = 1;
1073 /**
1074  * The center horizontal flag of ccui.ListView's gravity.
1075  * @constant
1076  * @type {number}
1077  */
1078 ccui.ListView.GRAVITY_CENTER_HORIZONTAL = 2;
1079 /**
1080  * The top flag of ccui.ListView's gravity.
1081  * @constant
1082  * @type {number}
1083  */
1084 ccui.ListView.GRAVITY_TOP = 3;
1085 /**
1086  * The bottom flag of ccui.ListView's gravity.
1087  * @constant
1088  * @type {number}
1089  */
1090 ccui.ListView.GRAVITY_BOTTOM = 4;
1091 /**
1092  * The center vertical flag of ccui.ListView's gravity.
1093  * @constant
1094  * @type {number}
1095  */
1096 ccui.ListView.GRAVITY_CENTER_VERTICAL = 5;
1097 
1098 /**
1099  * The flag of ccui.ListView's magnetic none type.
1100  * @constant
1101  * @type {number}
1102  */
1103 ccui.ListView.MAGNETIC_NONE = 0;
1104 /**
1105  * The flag of ccui.ListView's magnetic center type.<br/>
1106  * ListView tries to align its items in center of current view.
1107  * @constant
1108  * @type {number}
1109  */
1110 ccui.ListView.MAGNETIC_CENTER = 1;
1111 /**
1112  * The flag of ccui.ListView's magnetic both end type.<br/>
1113  * ListView tries to align its items in left or right end if it is horizontal, top or bottom in vertical. <br/>
1114  * The aligning side (left or right, top or bottom) is determined by user's scroll direction.
1115  * @constant
1116  * @type {number}
1117  */
1118 ccui.ListView.MAGNETIC_BOTH_END = 2;
1119 /**
1120  * The flag of ccui.ListView's magnetic left type.
1121  * @constant
1122  * @type {number}
1123  */
1124 ccui.ListView.MAGNETIC_LEFT = 3;
1125 /**
1126  * The flag of ccui.ListView's magnetic right type.
1127  * @constant
1128  * @type {number}
1129  */
1130 ccui.ListView.MAGNETIC_RIGHT = 4;
1131 /**
1132  * The flag of ccui.ListView's magnetic top type.
1133  * @constant
1134  * @type {number}
1135  */
1136 ccui.ListView.MAGNETIC_TOP = 5;
1137 /**
1138  * The flag of ccui.ListView's magnetic bottom type.
1139  * @constant
1140  * @type {number}
1141  */
1142 ccui.ListView.MAGNETIC_BOTTOM = 6;
1143