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