1 /**************************************************************************** 2 Copyright (c) 2008-2010 Ricardo Quesada 3 Copyright (c) 2011-2012 cocos2d-x.org 4 Copyright (c) 2013-2014 Chukong Technologies Inc. 5 Copyright (c) 2010 Sangwoo Im 6 7 http://www.cocos2d-x.org 8 9 Permission is hereby granted, free of charge, to any person obtaining a copy 10 of this software and associated documentation files (the "Software"), to deal 11 in the Software without restriction, including without limitation the rights 12 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 copies of the Software, and to permit persons to whom the Software is 14 furnished to do so, subject to the following conditions: 15 16 The above copyright notice and this permission notice shall be included in 17 all copies or substantial portions of the Software. 18 19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 THE SOFTWARE. 26 ****************************************************************************/ 27 28 /** 29 * The constant value of the fill style from top to bottom for cc.TableView 30 * @constant 31 * @type {number} 32 */ 33 cc.TABLEVIEW_FILL_TOPDOWN = 0; 34 35 /** 36 * The constant value of the fill style from bottom to top for cc.TableView 37 * @constant 38 * @type {number} 39 */ 40 cc.TABLEVIEW_FILL_BOTTOMUP = 1; 41 42 /** 43 * Abstract class for SWTableView cell node 44 * @class 45 * @abstract 46 * @extends cc.Node 47 * 48 * @property {Number} objectId - The index used internally by SWTableView and its subclasses 49 */ 50 cc.TableViewCell = cc.Node.extend(/** @lends cc.TableViewCell# */{ 51 _idx:0, 52 _className:"TableViewCell", 53 54 /** 55 * The index used internally by SWTableView and its subclasses 56 */ 57 getIdx:function () { 58 return this._idx; 59 }, 60 setIdx:function (idx) { 61 this._idx = idx; 62 }, 63 64 /** 65 * Cleans up any resources linked to this cell and resets <code>idx</code> property. 66 */ 67 reset:function () { 68 this._idx = cc.INVALID_INDEX; 69 }, 70 71 setObjectID:function (idx) { 72 this._idx = idx; 73 }, 74 getObjectID:function () { 75 return this._idx; 76 } 77 }); 78 79 var _p = cc.TableViewCell.prototype; 80 81 /** @expose */ 82 _p.objectId; 83 cc.defineGetterSetter(_p, "objectId", _p.getObjectID, _p.setObjectID); 84 85 _p = null; 86 87 /** 88 * Sole purpose of this delegate is to single touch event in this version. 89 */ 90 cc.TableViewDelegate = cc.ScrollViewDelegate.extend(/** @lends cc.TableViewDelegate# */{ 91 /** 92 * Delegate to respond touch event 93 * 94 * @param {cc.TableView} table table contains the given cell 95 * @param {cc.TableViewCell} cell cell that is touched 96 */ 97 tableCellTouched:function (table, cell) { 98 }, 99 100 /** 101 * Delegate to respond a table cell press event. 102 * 103 * @param {cc.TableView} table table contains the given cell 104 * @param {cc.TableViewCell} cell cell that is pressed 105 */ 106 tableCellHighlight:function(table, cell){ 107 }, 108 109 /** 110 * Delegate to respond a table cell release event 111 * 112 * @param {cc.TableView} table table contains the given cell 113 * @param {cc.TableViewCell} cell cell that is pressed 114 */ 115 tableCellUnhighlight:function(table, cell){ 116 117 }, 118 119 /** 120 * <p> 121 * Delegate called when the cell is about to be recycled. Immediately <br/> 122 * after this call the cell will be removed from the scene graph and <br/> 123 * recycled. 124 * </p> 125 * @param table table contains the given cell 126 * @param cell cell that is pressed 127 */ 128 tableCellWillRecycle:function(table, cell){ 129 130 } 131 }); 132 133 /** 134 * Data source that governs table backend data. 135 */ 136 cc.TableViewDataSource = cc.Class.extend(/** @lends cc.TableViewDataSource# */{ 137 /** 138 * cell size for a given index 139 * @param {cc.TableView} table table to hold the instances of Class 140 * @param {Number} idx the index of a cell to get a size 141 * @return {cc.Size} size of a cell at given index 142 */ 143 tableCellSizeForIndex:function(table, idx){ 144 return this.cellSizeForTable(table); 145 }, 146 /** 147 * cell height for a given table. 148 * 149 * @param {cc.TableView} table table to hold the instances of Class 150 * @return {cc.Size} cell size 151 */ 152 cellSizeForTable:function (table) { 153 return cc.size(0,0); 154 }, 155 156 /** 157 * a cell instance at a given index 158 * @param {cc.TableView} table table to hold the instances of Class 159 * @param idx index to search for a cell 160 * @return {cc.TableView} cell found at idx 161 */ 162 tableCellAtIndex:function (table, idx) { 163 return null; 164 }, 165 166 /** 167 * Returns number of cells in a given table view. 168 * @param {cc.TableView} table table to hold the instances of Class 169 * @return {Number} number of cells 170 */ 171 numberOfCellsInTableView:function (table) { 172 return 0; 173 } 174 }); 175 176 /** 177 * UITableView counterpart for cocos2d for iphone. 178 * this is a very basic, minimal implementation to bring UITableView-like component into cocos2d world. 179 * 180 * @class 181 * @extends cc.ScrollView 182 * 183 * @property {cc.TableViewDataSource} dataSource - The data source of the table view 184 * @property {cc.TableViewDelegate} delegate - The event delegate of the table view 185 * @property {Number} verticalFillOrder - The index to determine how cell is ordered and filled in the view 186 * 187 */ 188 cc.TableView = cc.ScrollView.extend(/** @lends cc.TableView# */{ 189 _vOrdering:null, 190 _indices:null, 191 _cellsFreed:null, 192 _dataSource:null, 193 _tableViewDelegate:null, 194 _oldDirection:null, 195 _cellsPositions:null, //vector with all cell positions 196 _touchedCell:null, 197 198 /** 199 * The 200 * @param dataSource 201 * @param size 202 * @param container 203 */ 204 ctor:function (dataSource, size, container) { 205 cc.ScrollView.prototype.ctor.call(this); 206 this._oldDirection = cc.SCROLLVIEW_DIRECTION_NONE; 207 this._cellsPositions = []; 208 209 this.initWithViewSize(size, container); 210 this.setDataSource(dataSource); 211 this._updateCellPositions(); 212 this._updateContentSize(); 213 }, 214 215 __indexFromOffset:function (offset) { 216 var low = 0; 217 var high = this._dataSource.numberOfCellsInTableView(this) - 1; 218 var search; 219 switch (this.getDirection()) { 220 case cc.SCROLLVIEW_DIRECTION_HORIZONTAL: 221 search = offset.x; 222 break; 223 default: 224 search = offset.y; 225 break; 226 } 227 228 var locCellsPositions = this._cellsPositions; 229 while (high >= low){ 230 var index = 0|(low + (high - low) / 2); 231 var cellStart = locCellsPositions[index]; 232 var cellEnd = locCellsPositions[index + 1]; 233 234 if (search >= cellStart && search <= cellEnd){ 235 return index; 236 } else if (search < cellStart){ 237 high = index - 1; 238 }else { 239 low = index + 1; 240 } 241 } 242 243 if (low <= 0) 244 return 0; 245 return -1; 246 }, 247 248 _indexFromOffset:function (offset) { 249 var locOffset = {x: offset.x, y: offset.y}; 250 var locDataSource = this._dataSource; 251 var maxIdx = locDataSource.numberOfCellsInTableView(this) - 1; 252 253 if (this._vOrdering === cc.TABLEVIEW_FILL_TOPDOWN) 254 locOffset.y = this.getContainer().getContentSize().height - locOffset.y; 255 256 var index = this.__indexFromOffset(locOffset); 257 if (index !== -1) { 258 index = Math.max(0, index); 259 if (index > maxIdx) 260 index = cc.INVALID_INDEX; 261 } 262 return index; 263 }, 264 265 __offsetFromIndex:function (index) { 266 var offset; 267 switch (this.getDirection()) { 268 case cc.SCROLLVIEW_DIRECTION_HORIZONTAL: 269 offset = cc.p(this._cellsPositions[index], 0); 270 break; 271 default: 272 offset = cc.p(0, this._cellsPositions[index]); 273 break; 274 } 275 276 return offset; 277 }, 278 279 _offsetFromIndex:function (index) { 280 var offset = this.__offsetFromIndex(index); 281 282 var cellSize = this._dataSource.tableCellSizeForIndex(this, index); 283 if (this._vOrdering === cc.TABLEVIEW_FILL_TOPDOWN) 284 offset.y = this.getContainer().getContentSize().height - offset.y - cellSize.height; 285 286 return offset; 287 }, 288 289 _updateCellPositions:function(){ 290 var cellsCount = this._dataSource.numberOfCellsInTableView(this); 291 var locCellsPositions = this._cellsPositions; 292 293 if (cellsCount > 0){ 294 var currentPos = 0; 295 var cellSize, locDataSource = this._dataSource; 296 for (var i=0; i < cellsCount; i++) { 297 locCellsPositions[i] = currentPos; 298 cellSize = locDataSource.tableCellSizeForIndex(this, i); 299 switch (this.getDirection()) { 300 case cc.SCROLLVIEW_DIRECTION_HORIZONTAL: 301 currentPos += cellSize.width; 302 break; 303 default: 304 currentPos += cellSize.height; 305 break; 306 } 307 } 308 this._cellsPositions[cellsCount] = currentPos;//1 extra value allows us to get right/bottom of the last cell 309 } 310 }, 311 312 _updateContentSize:function () { 313 var size = cc.size(0, 0); 314 315 var cellsCount = this._dataSource.numberOfCellsInTableView(this); 316 317 if(cellsCount > 0){ 318 var maxPosition = this._cellsPositions[cellsCount]; 319 switch (this.getDirection()) { 320 case cc.SCROLLVIEW_DIRECTION_HORIZONTAL: 321 size = cc.size(maxPosition, this._viewSize.height); 322 break; 323 default: 324 size = cc.size(this._viewSize.width, maxPosition); 325 break; 326 } 327 } 328 329 this.setContentSize(size); 330 331 if (this._oldDirection !== this._direction) { 332 if (this._direction === cc.SCROLLVIEW_DIRECTION_HORIZONTAL) { 333 this.setContentOffset(cc.p(0, 0)); 334 } else { 335 this.setContentOffset(cc.p(0, this.minContainerOffset().y)); 336 } 337 this._oldDirection = this._direction; 338 } 339 }, 340 341 _moveCellOutOfSight:function (cell) { 342 if(this._tableViewDelegate && this._tableViewDelegate.tableCellWillRecycle) 343 this._tableViewDelegate.tableCellWillRecycle(this, cell); 344 345 this._cellsFreed.addObject(cell); 346 this._cellsUsed.removeSortedObject(cell); 347 cc.arrayRemoveObject(this._indices, cell.getIdx()); 348 349 cell.reset(); 350 if (cell.getParent() === this.getContainer()) { 351 this.getContainer().removeChild(cell, true); 352 } 353 }, 354 355 _setIndexForCell:function (index, cell) { 356 cell.setAnchorPoint(0, 0); 357 cell.setPosition(this._offsetFromIndex(index)); 358 cell.setIdx(index); 359 }, 360 361 _addCellIfNecessary:function (cell) { 362 if (cell.getParent() !== this.getContainer()) { 363 this.getContainer().addChild(cell); 364 } 365 this._cellsUsed.insertSortedObject(cell); 366 var locIndices = this._indices, addIdx = cell.getIdx(); 367 if(locIndices.indexOf(addIdx) === -1){ 368 locIndices.push(addIdx); 369 //sort 370 locIndices.sort(function(a,b){return a-b;}); 371 } 372 }, 373 374 /** 375 * data source 376 */ 377 getDataSource:function () { 378 return this._dataSource; 379 }, 380 setDataSource:function (source) { 381 this._dataSource = source; 382 }, 383 384 /** 385 * delegate 386 */ 387 getDelegate:function () { 388 return this._tableViewDelegate; 389 }, 390 391 setDelegate:function (delegate) { 392 this._tableViewDelegate = delegate; 393 }, 394 395 /** 396 * determines how cell is ordered and filled in the view. 397 */ 398 setVerticalFillOrder:function (fillOrder) { 399 if (this._vOrdering !== fillOrder) { 400 this._vOrdering = fillOrder; 401 if (this._cellsUsed.count() > 0) { 402 this.reloadData(); 403 } 404 } 405 }, 406 getVerticalFillOrder:function () { 407 return this._vOrdering; 408 }, 409 410 initWithViewSize:function (size, container) { 411 if (cc.ScrollView.prototype.initWithViewSize.call(this, size, container)) { 412 this._cellsUsed = new cc.ArrayForObjectSorting(); 413 this._cellsFreed = new cc.ArrayForObjectSorting(); 414 this._indices = []; 415 this._tableViewDelegate = null; 416 this._vOrdering = cc.TABLEVIEW_FILL_BOTTOMUP; 417 this.setDirection(cc.SCROLLVIEW_DIRECTION_VERTICAL); 418 419 cc.ScrollView.prototype.setDelegate.call(this, this); 420 return true; 421 } 422 return false; 423 }, 424 425 /** 426 * Updates the content of the cell at a given index. 427 * 428 * @param idx index to find a cell 429 */ 430 updateCellAtIndex:function (idx) { 431 if (idx === cc.INVALID_INDEX || idx > this._dataSource.numberOfCellsInTableView(this) - 1) 432 return; 433 434 var cell = this.cellAtIndex(idx); 435 if (cell) 436 this._moveCellOutOfSight(cell); 437 438 cell = this._dataSource.tableCellAtIndex(this, idx); 439 this._setIndexForCell(idx, cell); 440 this._addCellIfNecessary(cell); 441 }, 442 443 /** 444 * Inserts a new cell at a given index 445 * 446 * @param idx location to insert 447 */ 448 insertCellAtIndex:function (idx) { 449 if (idx === cc.INVALID_INDEX || idx > this._dataSource.numberOfCellsInTableView(this) - 1) 450 return; 451 452 var newIdx, locCellsUsed = this._cellsUsed; 453 var cell = locCellsUsed.objectWithObjectID(idx); 454 if (cell) { 455 newIdx = locCellsUsed.indexOfSortedObject(cell); 456 for (var i = newIdx; i < locCellsUsed.count(); i++) { 457 cell = locCellsUsed.objectAtIndex(i); 458 this._setIndexForCell(cell.getIdx() + 1, cell); 459 } 460 } 461 462 //insert a new cell 463 cell = this._dataSource.tableCellAtIndex(this, idx); 464 this._setIndexForCell(idx, cell); 465 this._addCellIfNecessary(cell); 466 467 this._updateCellPositions(); 468 this._updateContentSize(); 469 }, 470 471 /** 472 * Removes a cell at a given index 473 * 474 * @param idx index to find a cell 475 */ 476 removeCellAtIndex:function (idx) { 477 if (idx === cc.INVALID_INDEX || idx > this._dataSource.numberOfCellsInTableView(this) - 1) 478 return; 479 480 var cell = this.cellAtIndex(idx); 481 if (!cell) 482 return; 483 484 var locCellsUsed = this._cellsUsed; 485 var newIdx = locCellsUsed.indexOfSortedObject(cell); 486 487 //remove first 488 this._moveCellOutOfSight(cell); 489 cc.arrayRemoveObject(this._indices, idx); 490 this._updateCellPositions(); 491 492 for (var i = locCellsUsed.count() - 1; i > newIdx; i--) { 493 cell = locCellsUsed.objectAtIndex(i); 494 this._setIndexForCell(cell.getIdx() - 1, cell); 495 } 496 }, 497 498 /** 499 * reloads data from data source. the view will be refreshed. 500 */ 501 reloadData:function () { 502 this._oldDirection = cc.SCROLLVIEW_DIRECTION_NONE; 503 var locCellsUsed = this._cellsUsed, locCellsFreed = this._cellsFreed, locContainer = this.getContainer(); 504 for (var i = 0, len = locCellsUsed.count(); i < len; i++) { 505 var cell = locCellsUsed.objectAtIndex(i); 506 507 if(this._tableViewDelegate && this._tableViewDelegate.tableCellWillRecycle) 508 this._tableViewDelegate.tableCellWillRecycle(this, cell); 509 510 locCellsFreed.addObject(cell); 511 cell.reset(); 512 if (cell.getParent() === locContainer) 513 locContainer.removeChild(cell, true); 514 } 515 516 this._indices = []; 517 this._cellsUsed = new cc.ArrayForObjectSorting(); 518 519 this._updateCellPositions(); 520 this._updateContentSize(); 521 if (this._dataSource.numberOfCellsInTableView(this) > 0) 522 this.scrollViewDidScroll(this); 523 }, 524 525 /** 526 * Dequeues a free cell if available. nil if not. 527 * 528 * @return {TableViewCell} free cell 529 */ 530 dequeueCell:function () { 531 if (this._cellsFreed.count() === 0) { 532 return null; 533 } else { 534 var cell = this._cellsFreed.objectAtIndex(0); 535 this._cellsFreed.removeObjectAtIndex(0); 536 return cell; 537 } 538 }, 539 540 /** 541 * Returns an existing cell at a given index. Returns nil if a cell is nonexistent at the moment of query. 542 * 543 * @param idx index 544 * @return {cc.TableViewCell} a cell at a given index 545 */ 546 cellAtIndex:function (idx) { 547 var i = this._indices.indexOf(idx); 548 if (i === -1) 549 return null; 550 return this._cellsUsed.objectWithObjectID(idx); 551 }, 552 553 scrollViewDidScroll:function (view) { 554 var locDataSource = this._dataSource; 555 var countOfItems = locDataSource.numberOfCellsInTableView(this); 556 if (0 === countOfItems) 557 return; 558 559 if (this._tableViewDelegate !== null && this._tableViewDelegate.scrollViewDidScroll) 560 this._tableViewDelegate.scrollViewDidScroll(this); 561 562 var idx = 0, locViewSize = this._viewSize, locContainer = this.getContainer(); 563 var offset = this.getContentOffset(); 564 offset.x *= -1; 565 offset.y *= -1; 566 567 var maxIdx = Math.max(countOfItems-1, 0); 568 569 if (this._vOrdering === cc.TABLEVIEW_FILL_TOPDOWN) 570 offset.y = offset.y + locViewSize.height/locContainer.getScaleY(); 571 var startIdx = this._indexFromOffset(offset); 572 if (startIdx === cc.INVALID_INDEX) 573 startIdx = countOfItems - 1; 574 575 if (this._vOrdering === cc.TABLEVIEW_FILL_TOPDOWN) 576 offset.y -= locViewSize.height/locContainer.getScaleY(); 577 else 578 offset.y += locViewSize.height/locContainer.getScaleY(); 579 offset.x += locViewSize.width/locContainer.getScaleX(); 580 581 var endIdx = this._indexFromOffset(offset); 582 if (endIdx === cc.INVALID_INDEX) 583 endIdx = countOfItems - 1; 584 585 var cell, locCellsUsed = this._cellsUsed; 586 if (locCellsUsed.count() > 0) { 587 cell = locCellsUsed.objectAtIndex(0); 588 idx = cell.getIdx(); 589 while (idx < startIdx) { 590 this._moveCellOutOfSight(cell); 591 if (locCellsUsed.count() > 0) { 592 cell = locCellsUsed.objectAtIndex(0); 593 idx = cell.getIdx(); 594 } else 595 break; 596 } 597 } 598 599 if (locCellsUsed.count() > 0) { 600 cell = locCellsUsed.lastObject(); 601 idx = cell.getIdx(); 602 while (idx <= maxIdx && idx > endIdx) { 603 this._moveCellOutOfSight(cell); 604 if (locCellsUsed.count() > 0) { 605 cell = locCellsUsed.lastObject(); 606 idx = cell.getIdx(); 607 } else 608 break; 609 } 610 } 611 612 var locIndices = this._indices; 613 for (var i = startIdx; i <= endIdx; i++) { 614 if (locIndices.indexOf(i) !== -1) 615 continue; 616 this.updateCellAtIndex(i); 617 } 618 }, 619 620 scrollViewDidZoom:function (view) { 621 }, 622 623 onTouchEnded:function (touch, event) { 624 if (!this.isVisible()) 625 return; 626 627 if (this._touchedCell){ 628 var bb = this.getBoundingBox(); 629 var tmpOrigin = cc.p(bb.x, bb.y); 630 tmpOrigin = this._parent.convertToWorldSpace(tmpOrigin); 631 bb.x = tmpOrigin.x; 632 bb.y = tmpOrigin.y; 633 var locTableViewDelegate = this._tableViewDelegate; 634 if (cc.rectContainsPoint(bb, touch.getLocation()) && locTableViewDelegate !== null){ 635 if(locTableViewDelegate.tableCellUnhighlight) 636 locTableViewDelegate.tableCellUnhighlight(this, this._touchedCell); 637 if(locTableViewDelegate.tableCellTouched) 638 locTableViewDelegate.tableCellTouched(this, this._touchedCell); 639 } 640 this._touchedCell = null; 641 } 642 cc.ScrollView.prototype.onTouchEnded.call(this, touch, event); 643 }, 644 645 onTouchBegan:function(touch, event){ 646 if (!this.isVisible()) 647 return false; 648 649 var touchResult = cc.ScrollView.prototype.onTouchBegan.call(this, touch, event); 650 651 if(this._touches.length === 1) { 652 var index, point; 653 654 point = this.getContainer().convertTouchToNodeSpace(touch); 655 656 index = this._indexFromOffset(point); 657 if (index === cc.INVALID_INDEX) 658 this._touchedCell = null; 659 else 660 this._touchedCell = this.cellAtIndex(index); 661 662 if (this._touchedCell && this._tableViewDelegate !== null && this._tableViewDelegate.tableCellHighlight) 663 this._tableViewDelegate.tableCellHighlight(this, this._touchedCell); 664 } else if(this._touchedCell) { 665 if(this._tableViewDelegate !== null && this._tableViewDelegate.tableCellUnhighlight) 666 this._tableViewDelegate.tableCellUnhighlight(this, this._touchedCell); 667 this._touchedCell = null; 668 } 669 670 return touchResult; 671 }, 672 673 onTouchMoved: function(touch, event){ 674 cc.ScrollView.prototype.onTouchMoved.call(this, touch, event); 675 676 if (this._touchedCell && this.isTouchMoved()) { 677 if(this._tableViewDelegate !== null && this._tableViewDelegate.tableCellUnhighlight) 678 this._tableViewDelegate.tableCellUnhighlight(this, this._touchedCell); 679 this._touchedCell = null; 680 } 681 }, 682 683 onTouchCancelled: function(touch, event){ 684 cc.ScrollView.prototype.onTouchCancelled.call(this, touch, event); 685 686 if (this._touchedCell) { 687 if(this._tableViewDelegate !== null && this._tableViewDelegate.tableCellUnhighlight) 688 this._tableViewDelegate.tableCellUnhighlight(this, this._touchedCell); 689 this._touchedCell = null; 690 } 691 } 692 }); 693 694 var _p = cc.TableView.prototype; 695 696 /** @expose */ 697 _p.dataSource; 698 cc.defineGetterSetter(_p, "dataSource", _p.getDataSource, _p.setDataSource); 699 /** @expose */ 700 _p.delegate; 701 cc.defineGetterSetter(_p, "delegate", _p.getDelegate, _p.setDelegate); 702 /** @expose */ 703 _p.verticalFillOrder; 704 cc.defineGetterSetter(_p, "verticalFillOrder", _p.getVerticalFillOrder, _p.setVerticalFillOrder); 705 706 _p = null; 707 708 /** 709 * An initialized table view object 710 * @deprecated 711 * @param {cc.TableViewDataSource} dataSource data source; 712 * @param {cc.Size} size view size 713 * @param {cc.Node} [container] parent object for cells 714 * @return {cc.TableView} table view 715 */ 716 cc.TableView.create = function (dataSource, size, container) { 717 return new cc.TableView(dataSource, size, container); 718 }; 719