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 
 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();
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     _selectedItemEvent: function (event) {
868         var eventEnum = (event === ccui.Widget.TOUCH_BEGAN) ? ccui.ListView.ON_SELECTED_ITEM_START : ccui.ListView.ON_SELECTED_ITEM_END;
869         if(this._listViewEventSelector){
870             if (this._listViewEventListener)
871                 this._listViewEventSelector.call(this._listViewEventListener, this, eventEnum);
872             else
873                 this._listViewEventSelector(this, eventEnum);
874         }
875         if(this._ccEventCallback)
876             this._ccEventCallback(this, eventEnum);
877     },
878 
879     /**
880      * Intercept touch event, handle its child's touch event.
881      * @param {Number} eventType
882      * @param {ccui.Widget} sender
883      * @param {cc.Touch} touch
884      */
885     interceptTouchEvent: function (eventType, sender, touch) {
886         ccui.ScrollView.prototype.interceptTouchEvent.call(this, eventType, sender, touch);
887         if(!this._touchEnabled)
888         {
889             return;
890         }
891         if (eventType !== ccui.Widget.TOUCH_MOVED) {
892             var parent = sender;
893             while (parent) {
894                 if (parent && parent.getParent() === this._innerContainer) {
895                     this._curSelectedIndex = this.getIndex(parent);
896                     break;
897                 }
898                 parent = parent.getParent();
899             }
900             if (sender.isHighlighted())
901                 this._selectedItemEvent(eventType);
902         }
903     },
904 
905     /**
906      * Returns current selected index
907      * @returns {number}
908      */
909     getCurSelectedIndex: function () {
910         return this._curSelectedIndex;
911     },
912 
913     _onSizeChanged: function () {
914         ccui.ScrollView.prototype._onSizeChanged.call(this);
915         this._refreshViewDirty = true;
916     },
917 
918     /**
919      * Returns the "class name" of ccui.ListView.
920      * @returns {string}
921      */
922     getDescription: function () {
923         return "ListView";
924     },
925 
926     _createCloneInstance: function () {
927         return new ccui.ListView();
928     },
929 
930     _copyClonedWidgetChildren: function (model) {
931         var arrayItems = model.getItems();
932         for (var i = 0; i < arrayItems.length; i++) {
933             var item = arrayItems[i];
934             this.pushBackCustomItem(item.clone());
935         }
936     },
937 
938     _copySpecialProperties: function (listView) {
939         if(listView instanceof ccui.ListView){
940             ccui.ScrollView.prototype._copySpecialProperties.call(this, listView);
941             this.setItemModel(listView._model);
942             this.setItemsMargin(listView._itemsMargin);
943             this.setGravity(listView._gravity);
944 
945             this._listViewEventListener = listView._listViewEventListener;
946             this._listViewEventSelector = listView._listViewEventSelector;
947         }
948     },
949 
950     _startAttenuatingAutoScroll: function(deltaMove, initialVelocity)
951     {
952         var adjustedDeltaMove = deltaMove;
953 
954         if(this._items.length !== 0 && this._magneticType !== ccui.ListView.MAGNETIC_NONE)
955         {
956             adjustedDeltaMove = this._flattenVectorByDirection(adjustedDeltaMove);
957 
958             var howMuchOutOfBoundary = this._getHowMuchOutOfBoundary(adjustedDeltaMove);
959             // If the destination is out of boundary, do nothing here. Because it will be handled by bouncing back.
960             if(howMuchOutOfBoundary.x === 0 && howMuchOutOfBoundary.y === 0 )
961             {
962                 var magType = this._magneticType;
963                 if(magType === ccui.ListView.MAGNETIC_BOTH_END)
964                 {
965                     if(this._direction === ccui.ScrollView.DIR_HORIZONTAL)
966                     {
967                         magType = (adjustedDeltaMove.x > 0 ? ccui.ListView.MAGNETIC_LEFT : ccui.ListView.MAGNETIC_RIGHT);
968                     }
969                     else if(this._direction === ccui.ScrollView.DIR_VERTICAL)
970                     {
971                         magType = (adjustedDeltaMove.y > 0 ? ccui.ListView.MAGNETIC_BOTTOM : ccui.ListView.MAGNETIC_TOP);
972                     }
973                 }
974 
975                 // Adjust the delta move amount according to the magnetic type
976                 var magneticAnchorPoint = this._getAnchorPointByMagneticType(magType);
977                 var magneticPosition = cc.pMult(this._innerContainer.getPosition(), -1);
978                 magneticPosition.x += this.width * magneticAnchorPoint.x;
979                 magneticPosition.y += this.height * magneticAnchorPoint.y;
980 
981                 var pTargetItem = this.getClosestItemToPosition(cc.pSub(magneticPosition, adjustedDeltaMove), magneticAnchorPoint);
982                 var itemPosition = this._calculateItemPositionWithAnchor(pTargetItem, magneticAnchorPoint);
983                 adjustedDeltaMove = cc.pSub(magneticPosition, itemPosition);
984             }
985         }
986         ccui.ScrollView.prototype._startAttenuatingAutoScroll.call(this,adjustedDeltaMove, initialVelocity);
987     },
988 
989     _getAnchorPointByMagneticType: function(magneticType)
990     {
991         switch(magneticType)
992         {
993             case ccui.ListView.MAGNETIC_NONE: return cc.p(0, 0);
994             case ccui.ListView.MAGNETIC_BOTH_END: return cc.p(0, 1);
995             case ccui.ListView.MAGNETIC_CENTER: return cc.p(0.5, 0.5);
996             case ccui.ListView.MAGNETIC_LEFT: return cc.p(0, 0.5);
997             case ccui.ListView.MAGNETIC_RIGHT: return cc.p(1, 0.5);
998             case ccui.ListView.MAGNETIC_TOP: return cc.p(0.5, 1);
999             case ccui.ListView.MAGNETIC_BOTTOM: return cc.p(0.5, 0);
1000         }
1001 
1002         return cc.p(0, 0);
1003     },
1004 
1005     _startMagneticScroll: function()
1006     {
1007         if(this._items.length === 0 || this._magneticType === ccui.ListView.MAGNETIC_NONE)
1008         {
1009             return;
1010         }
1011 
1012         // Find the closest item
1013         var magneticAnchorPoint =this._getAnchorPointByMagneticType(this._magneticType);
1014         var magneticPosition = cc.pMult(this._innerContainer.getPosition(), -1);
1015         magneticPosition.x += this.width * magneticAnchorPoint.x;
1016         magneticPosition.y += this.height * magneticAnchorPoint.y;
1017 
1018         var pTargetItem = this.getClosestItemToPosition(magneticPosition, magneticAnchorPoint);
1019         this.scrollToItem(this.getIndex(pTargetItem), magneticAnchorPoint, magneticAnchorPoint);
1020     }
1021 });
1022 
1023 /**
1024  * allocates and initializes a UIListView.
1025  * @deprecated since v3.0, please use new ccui.ListView() instead.
1026  */
1027 ccui.ListView.create = function () {
1028     return new ccui.ListView();
1029 };
1030 
1031 // Constants
1032 //listView event type
1033 /**
1034  * The flag selected item of ccui.ListView's event.
1035  * @constant
1036  * @type {number}
1037  */
1038 ccui.ListView.EVENT_SELECTED_ITEM = 0;
1039 
1040 /**
1041  * The flag selected item start of ccui.ListView's event.
1042  * @constant
1043  * @type {number}
1044  */
1045 ccui.ListView.ON_SELECTED_ITEM_START = 0;
1046 /**
1047  * The flag selected item end of ccui.ListView's event.
1048  * @constant
1049  * @type {number}
1050  */
1051 ccui.ListView.ON_SELECTED_ITEM_END = 1;
1052 
1053 //listView gravity
1054 /**
1055  * The left flag of ccui.ListView's gravity.
1056  * @constant
1057  * @type {number}
1058  */
1059 ccui.ListView.GRAVITY_LEFT = 0;
1060 /**
1061  * The right flag of ccui.ListView's gravity.
1062  * @constant
1063  * @type {number}
1064  */
1065 ccui.ListView.GRAVITY_RIGHT = 1;
1066 /**
1067  * The center horizontal flag of ccui.ListView's gravity.
1068  * @constant
1069  * @type {number}
1070  */
1071 ccui.ListView.GRAVITY_CENTER_HORIZONTAL = 2;
1072 /**
1073  * The top flag of ccui.ListView's gravity.
1074  * @constant
1075  * @type {number}
1076  */
1077 ccui.ListView.GRAVITY_TOP = 3;
1078 /**
1079  * The bottom flag of ccui.ListView's gravity.
1080  * @constant
1081  * @type {number}
1082  */
1083 ccui.ListView.GRAVITY_BOTTOM = 4;
1084 /**
1085  * The center vertical flag of ccui.ListView's gravity.
1086  * @constant
1087  * @type {number}
1088  */
1089 ccui.ListView.GRAVITY_CENTER_VERTICAL = 5;
1090 
1091 /**
1092  * The flag of ccui.ListView's magnetic none type.
1093  * @constant
1094  * @type {number}
1095  */
1096 ccui.ListView.MAGNETIC_NONE = 0;
1097 /**
1098  * The flag of ccui.ListView's magnetic center type.<br/>
1099  * ListView tries to align its items in center of current view.
1100  * @constant
1101  * @type {number}
1102  */
1103 ccui.ListView.MAGNETIC_CENTER = 1;
1104 /**
1105  * The flag of ccui.ListView's magnetic both end type.<br/>
1106  * ListView tries to align its items in left or right end if it is horizontal, top or bottom in vertical. <br/>
1107  * The aligning side (left or right, top or bottom) is determined by user's scroll direction.
1108  * @constant
1109  * @type {number}
1110  */
1111 ccui.ListView.MAGNETIC_BOTH_END = 2;
1112 /**
1113  * The flag of ccui.ListView's magnetic left type.
1114  * @constant
1115  * @type {number}
1116  */
1117 ccui.ListView.MAGNETIC_LEFT = 3;
1118 /**
1119  * The flag of ccui.ListView's magnetic right type.
1120  * @constant
1121  * @type {number}
1122  */
1123 ccui.ListView.MAGNETIC_RIGHT = 4;
1124 /**
1125  * The flag of ccui.ListView's magnetic top type.
1126  * @constant
1127  * @type {number}
1128  */
1129 ccui.ListView.MAGNETIC_TOP = 5;
1130 /**
1131  * The flag of ccui.ListView's magnetic bottom type.
1132  * @constant
1133  * @type {number}
1134  */
1135 ccui.ListView.MAGNETIC_BOTTOM = 6;