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 * @extend 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 * @extend 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 ctor:function () { 199 cc.ScrollView.prototype.ctor.call(this); 200 this._oldDirection = cc.SCROLLVIEW_DIRECTION_NONE; 201 this._cellsPositions = []; 202 }, 203 204 __indexFromOffset:function (offset) { 205 var low = 0; 206 var high = this._dataSource.numberOfCellsInTableView(this) - 1; 207 var search; 208 switch (this.getDirection()) { 209 case cc.SCROLLVIEW_DIRECTION_HORIZONTAL: 210 search = offset.x; 211 break; 212 default: 213 search = offset.y; 214 break; 215 } 216 217 var locCellsPositions = this._cellsPositions; 218 while (high >= low){ 219 var index = 0|(low + (high - low) / 2); 220 var cellStart = locCellsPositions[index]; 221 var cellEnd = locCellsPositions[index + 1]; 222 223 if (search >= cellStart && search <= cellEnd){ 224 return index; 225 } else if (search < cellStart){ 226 high = index - 1; 227 }else { 228 low = index + 1; 229 } 230 } 231 232 if (low <= 0) 233 return 0; 234 return -1; 235 }, 236 237 _indexFromOffset:function (offset) { 238 var locOffset = {x: offset.x, y: offset.y}; 239 var locDataSource = this._dataSource; 240 var maxIdx = locDataSource.numberOfCellsInTableView(this) - 1; 241 242 if (this._vOrdering === cc.TABLEVIEW_FILL_TOPDOWN) 243 locOffset.y = this.getContainer().getContentSize().height - locOffset.y; 244 245 var index = this.__indexFromOffset(locOffset); 246 if (index != -1) { 247 index = Math.max(0, index); 248 if (index > maxIdx) 249 index = cc.INVALID_INDEX; 250 } 251 return index; 252 }, 253 254 __offsetFromIndex:function (index) { 255 var offset; 256 switch (this.getDirection()) { 257 case cc.SCROLLVIEW_DIRECTION_HORIZONTAL: 258 offset = cc.p(this._cellsPositions[index], 0); 259 break; 260 default: 261 offset = cc.p(0, this._cellsPositions[index]); 262 break; 263 } 264 265 return offset; 266 }, 267 268 _offsetFromIndex:function (index) { 269 var offset = this.__offsetFromIndex(index); 270 271 var cellSize = this._dataSource.tableCellSizeForIndex(this, index); 272 if (this._vOrdering === cc.TABLEVIEW_FILL_TOPDOWN) 273 offset.y = this.getContainer().getContentSize().height - offset.y - cellSize.height; 274 275 return offset; 276 }, 277 278 _updateCellPositions:function(){ 279 var cellsCount = this._dataSource.numberOfCellsInTableView(this); 280 var locCellsPositions = this._cellsPositions; 281 282 if (cellsCount > 0){ 283 var currentPos = 0; 284 var cellSize, locDataSource = this._dataSource; 285 for (var i=0; i < cellsCount; i++) { 286 locCellsPositions[i] = currentPos; 287 cellSize = locDataSource.tableCellSizeForIndex(this, i); 288 switch (this.getDirection()) { 289 case cc.SCROLLVIEW_DIRECTION_HORIZONTAL: 290 currentPos += cellSize.width; 291 break; 292 default: 293 currentPos += cellSize.height; 294 break; 295 } 296 } 297 this._cellsPositions[cellsCount] = currentPos;//1 extra value allows us to get right/bottom of the last cell 298 } 299 }, 300 301 _updateContentSize:function () { 302 var size = cc.size(0, 0); 303 304 var cellsCount = this._dataSource.numberOfCellsInTableView(this); 305 306 if(cellsCount > 0){ 307 var maxPosition = this._cellsPositions[cellsCount]; 308 switch (this.getDirection()) { 309 case cc.SCROLLVIEW_DIRECTION_HORIZONTAL: 310 size = cc.size(maxPosition, this._viewSize.height); 311 break; 312 default: 313 size = cc.size(this._viewSize.width, maxPosition); 314 break; 315 } 316 } 317 318 this.setContentSize(size); 319 320 if (this._oldDirection != this._direction) { 321 if (this._direction == cc.SCROLLVIEW_DIRECTION_HORIZONTAL) { 322 this.setContentOffset(cc.p(0, 0)); 323 } else { 324 this.setContentOffset(cc.p(0, this.minContainerOffset().y)); 325 } 326 this._oldDirection = this._direction; 327 } 328 }, 329 330 _moveCellOutOfSight:function (cell) { 331 if(this._tableViewDelegate && this._tableViewDelegate.tableCellWillRecycle) 332 this._tableViewDelegate.tableCellWillRecycle(this, cell); 333 334 this._cellsFreed.addObject(cell); 335 this._cellsUsed.removeSortedObject(cell); 336 cc.arrayRemoveObject(this._indices, cell.getIdx()); 337 338 cell.reset(); 339 if (cell.getParent() == this.getContainer()) { 340 this.getContainer().removeChild(cell, true); 341 } 342 }, 343 344 _setIndexForCell:function (index, cell) { 345 cell.setAnchorPoint(0, 0); 346 cell.setPosition(this._offsetFromIndex(index)); 347 cell.setIdx(index); 348 }, 349 350 _addCellIfNecessary:function (cell) { 351 if (cell.getParent() != this.getContainer()) { 352 this.getContainer().addChild(cell); 353 } 354 this._cellsUsed.insertSortedObject(cell); 355 var locIndices = this._indices, addIdx = cell.getIdx(); 356 if(locIndices.indexOf(addIdx) == -1){ 357 locIndices.push(addIdx); 358 //sort 359 locIndices.sort(function(a,b){return a-b;}); 360 } 361 }, 362 363 /** 364 * data source 365 */ 366 getDataSource:function () { 367 return this._dataSource; 368 }, 369 setDataSource:function (source) { 370 this._dataSource = source; 371 }, 372 373 /** 374 * delegate 375 */ 376 getDelegate:function () { 377 return this._tableViewDelegate; 378 }, 379 380 setDelegate:function (delegate) { 381 this._tableViewDelegate = delegate; 382 }, 383 384 /** 385 * determines how cell is ordered and filled in the view. 386 */ 387 setVerticalFillOrder:function (fillOrder) { 388 if (this._vOrdering != fillOrder) { 389 this._vOrdering = fillOrder; 390 if (this._cellsUsed.count() > 0) { 391 this.reloadData(); 392 } 393 } 394 }, 395 getVerticalFillOrder:function () { 396 return this._vOrdering; 397 }, 398 399 initWithViewSize:function (size, container) { 400 if (cc.ScrollView.prototype.initWithViewSize.call(this, size, container)) { 401 this._cellsUsed = new cc.ArrayForObjectSorting(); 402 this._cellsFreed = new cc.ArrayForObjectSorting(); 403 this._indices = []; 404 this._tableViewDelegate = null; 405 this._vOrdering = cc.TABLEVIEW_FILL_BOTTOMUP; 406 this.setDirection(cc.SCROLLVIEW_DIRECTION_VERTICAL); 407 408 cc.ScrollView.prototype.setDelegate.call(this, this); 409 return true; 410 } 411 return false; 412 }, 413 414 /** 415 * Updates the content of the cell at a given index. 416 * 417 * @param idx index to find a cell 418 */ 419 updateCellAtIndex:function (idx) { 420 if (idx == cc.INVALID_INDEX || idx > this._dataSource.numberOfCellsInTableView(this) - 1) 421 return; 422 423 var cell = this.cellAtIndex(idx); 424 if (cell) 425 this._moveCellOutOfSight(cell); 426 427 cell = this._dataSource.tableCellAtIndex(this, idx); 428 this._setIndexForCell(idx, cell); 429 this._addCellIfNecessary(cell); 430 }, 431 432 /** 433 * Inserts a new cell at a given index 434 * 435 * @param idx location to insert 436 */ 437 insertCellAtIndex:function (idx) { 438 if (idx == cc.INVALID_INDEX || idx > this._dataSource.numberOfCellsInTableView(this) - 1) 439 return; 440 441 var newIdx, locCellsUsed = this._cellsUsed; 442 var cell = locCellsUsed.objectWithObjectID(idx); 443 if (cell) { 444 newIdx = locCellsUsed.indexOfSortedObject(cell); 445 for (var i = newIdx; i < locCellsUsed.count(); i++) { 446 cell = locCellsUsed.objectAtIndex(i); 447 this._setIndexForCell(cell.getIdx() + 1, cell); 448 } 449 } 450 451 //insert a new cell 452 cell = this._dataSource.tableCellAtIndex(this, idx); 453 this._setIndexForCell(idx, cell); 454 this._addCellIfNecessary(cell); 455 456 this._updateCellPositions(); 457 this._updateContentSize(); 458 }, 459 460 /** 461 * Removes a cell at a given index 462 * 463 * @param idx index to find a cell 464 */ 465 removeCellAtIndex:function (idx) { 466 if (idx == cc.INVALID_INDEX || idx > this._dataSource.numberOfCellsInTableView(this) - 1) 467 return; 468 469 var cell = this.cellAtIndex(idx); 470 if (!cell) 471 return; 472 473 var locCellsUsed = this._cellsUsed; 474 var newIdx = locCellsUsed.indexOfSortedObject(cell); 475 476 //remove first 477 this._moveCellOutOfSight(cell); 478 cc.arrayRemoveObject(this._indices, idx); 479 this._updateCellPositions(); 480 481 for (var i = locCellsUsed.count() - 1; i > newIdx; i--) { 482 cell = locCellsUsed.objectAtIndex(i); 483 this._setIndexForCell(cell.getIdx() - 1, cell); 484 } 485 }, 486 487 /** 488 * reloads data from data source. the view will be refreshed. 489 */ 490 reloadData:function () { 491 this._oldDirection = cc.SCROLLVIEW_DIRECTION_NONE; 492 var locCellsUsed = this._cellsUsed, locCellsFreed = this._cellsFreed, locContainer = this.getContainer(); 493 for (var i = 0, len = locCellsUsed.count(); i < len; i++) { 494 var cell = locCellsUsed.objectAtIndex(i); 495 496 if(this._tableViewDelegate && this._tableViewDelegate.tableCellWillRecycle) 497 this._tableViewDelegate.tableCellWillRecycle(this, cell); 498 499 locCellsFreed.addObject(cell); 500 cell.reset(); 501 if (cell.getParent() == locContainer) 502 locContainer.removeChild(cell, true); 503 } 504 505 this._indices = []; 506 this._cellsUsed = new cc.ArrayForObjectSorting(); 507 508 this._updateCellPositions(); 509 this._updateContentSize(); 510 if (this._dataSource.numberOfCellsInTableView(this) > 0) 511 this.scrollViewDidScroll(this); 512 }, 513 514 /** 515 * Dequeues a free cell if available. nil if not. 516 * 517 * @return {TableViewCell} free cell 518 */ 519 dequeueCell:function () { 520 if (this._cellsFreed.count() === 0) { 521 return null; 522 } else { 523 var cell = this._cellsFreed.objectAtIndex(0); 524 this._cellsFreed.removeObjectAtIndex(0); 525 return cell; 526 } 527 }, 528 529 /** 530 * Returns an existing cell at a given index. Returns nil if a cell is nonexistent at the moment of query. 531 * 532 * @param idx index 533 * @return {cc.TableViewCell} a cell at a given index 534 */ 535 cellAtIndex:function (idx) { 536 var i = this._indices.indexOf(idx); 537 if (i == -1) 538 return null; 539 return this._cellsUsed.objectWithObjectID(idx); 540 }, 541 542 scrollViewDidScroll:function (view) { 543 var locDataSource = this._dataSource; 544 var countOfItems = locDataSource.numberOfCellsInTableView(this); 545 if (0 === countOfItems) 546 return; 547 548 if (this._tableViewDelegate != null && this._tableViewDelegate.scrollViewDidScroll) 549 this._tableViewDelegate.scrollViewDidScroll(this); 550 551 var idx = 0, locViewSize = this._viewSize, locContainer = this.getContainer(); 552 var offset = this.getContentOffset(); 553 offset.x *= -1; 554 offset.y *= -1; 555 556 var maxIdx = Math.max(countOfItems-1, 0); 557 558 if (this._vOrdering === cc.TABLEVIEW_FILL_TOPDOWN) 559 offset.y = offset.y + locViewSize.height/locContainer.getScaleY(); 560 var startIdx = this._indexFromOffset(offset); 561 if (startIdx === cc.INVALID_INDEX) 562 startIdx = countOfItems - 1; 563 564 if (this._vOrdering === cc.TABLEVIEW_FILL_TOPDOWN) 565 offset.y -= locViewSize.height/locContainer.getScaleY(); 566 else 567 offset.y += locViewSize.height/locContainer.getScaleY(); 568 offset.x += locViewSize.width/locContainer.getScaleX(); 569 570 var endIdx = this._indexFromOffset(offset); 571 if (endIdx === cc.INVALID_INDEX) 572 endIdx = countOfItems - 1; 573 574 var cell, locCellsUsed = this._cellsUsed; 575 if (locCellsUsed.count() > 0) { 576 cell = locCellsUsed.objectAtIndex(0); 577 idx = cell.getIdx(); 578 while (idx < startIdx) { 579 this._moveCellOutOfSight(cell); 580 if (locCellsUsed.count() > 0) { 581 cell = locCellsUsed.objectAtIndex(0); 582 idx = cell.getIdx(); 583 } else 584 break; 585 } 586 } 587 588 if (locCellsUsed.count() > 0) { 589 cell = locCellsUsed.lastObject(); 590 idx = cell.getIdx(); 591 while (idx <= maxIdx && idx > endIdx) { 592 this._moveCellOutOfSight(cell); 593 if (locCellsUsed.count() > 0) { 594 cell = locCellsUsed.lastObject(); 595 idx = cell.getIdx(); 596 } else 597 break; 598 } 599 } 600 601 var locIndices = this._indices; 602 for (var i = startIdx; i <= endIdx; i++) { 603 if (locIndices.indexOf(i) != -1) 604 continue; 605 this.updateCellAtIndex(i); 606 } 607 }, 608 609 scrollViewDidZoom:function (view) { 610 }, 611 612 onTouchEnded:function (touch, event) { 613 if (!this.isVisible()) 614 return; 615 616 if (this._touchedCell){ 617 var bb = this.getBoundingBox(); 618 var tmpOrigin = cc.p(bb.x, bb.y); 619 tmpOrigin = this._parent.convertToWorldSpace(tmpOrigin); 620 bb.x = tmpOrigin.x; 621 bb.y = tmpOrigin.y; 622 var locTableViewDelegate = this._tableViewDelegate; 623 if (cc.rectContainsPoint(bb, touch.getLocation()) && locTableViewDelegate != null){ 624 if(locTableViewDelegate.tableCellUnhighlight) 625 locTableViewDelegate.tableCellUnhighlight(this, this._touchedCell); 626 if(locTableViewDelegate.tableCellTouched) 627 locTableViewDelegate.tableCellTouched(this, this._touchedCell); 628 } 629 this._touchedCell = null; 630 } 631 cc.ScrollView.prototype.onTouchEnded.call(this, touch, event); 632 }, 633 634 onTouchBegan:function(touch, event){ 635 if (!this.isVisible()) 636 return false; 637 638 var touchResult = cc.ScrollView.prototype.onTouchBegan.call(this, touch, event); 639 640 if(this._touches.length === 1) { 641 var index, point; 642 643 point = this.getContainer().convertTouchToNodeSpace(touch); 644 645 index = this._indexFromOffset(point); 646 if (index === cc.INVALID_INDEX) 647 this._touchedCell = null; 648 else 649 this._touchedCell = this.cellAtIndex(index); 650 651 if (this._touchedCell && this._tableViewDelegate != null && this._tableViewDelegate.tableCellHighlight) 652 this._tableViewDelegate.tableCellHighlight(this, this._touchedCell); 653 } else if(this._touchedCell) { 654 if(this._tableViewDelegate != null && this._tableViewDelegate.tableCellUnhighlight) 655 this._tableViewDelegate.tableCellUnhighlight(this, this._touchedCell); 656 this._touchedCell = null; 657 } 658 659 return touchResult; 660 }, 661 662 onTouchMoved: function(touch, event){ 663 cc.ScrollView.prototype.onTouchMoved.call(this, touch, event); 664 665 if (this._touchedCell && this.isTouchMoved()) { 666 if(this._tableViewDelegate != null && this._tableViewDelegate.tableCellUnhighlight) 667 this._tableViewDelegate.tableCellUnhighlight(this, this._touchedCell); 668 this._touchedCell = null; 669 } 670 }, 671 672 onTouchCancelled: function(touch, event){ 673 cc.ScrollView.prototype.onTouchCancelled.call(this, touch, event); 674 675 if (this._touchedCell) { 676 if(this._tableViewDelegate != null && this._tableViewDelegate.tableCellUnhighlight) 677 this._tableViewDelegate.tableCellUnhighlight(this, this._touchedCell); 678 this._touchedCell = null; 679 } 680 } 681 }); 682 683 var _p = cc.TableView.prototype; 684 685 /** @expose */ 686 _p.dataSource; 687 cc.defineGetterSetter(_p, "dataSource", _p.getDataSource, _p.setDataSource); 688 /** @expose */ 689 _p.delegate; 690 cc.defineGetterSetter(_p, "delegate", _p.getDelegate, _p.setDelegate); 691 /** @expose */ 692 _p.verticalFillOrder; 693 cc.defineGetterSetter(_p, "verticalFillOrder", _p.getVerticalFillOrder, _p.setVerticalFillOrder); 694 695 _p = null; 696 697 /** 698 * An initialized table view object 699 * 700 * @param {cc.TableViewDataSource} dataSource data source; 701 * @param {cc.Size} size view size 702 * @param {cc.Node} [container] parent object for cells 703 * @return {cc.TableView} table view 704 */ 705 cc.TableView.create = function (dataSource, size, container) { 706 var table = new cc.TableView(); 707 table.initWithViewSize(size, container); 708 table.setDataSource(dataSource); 709 table._updateCellPositions(); 710 table._updateContentSize(); 711 return table; 712 }; 713