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