OLD | NEW |
1 (function() { | 1 (function() { |
2 | 2 |
3 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); | 3 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); |
4 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; | 4 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; |
5 var DEFAULT_PHYSICAL_COUNT = 20; | 5 var DEFAULT_PHYSICAL_COUNT = 3; |
6 var MAX_PHYSICAL_COUNT = 500; | 6 var MAX_PHYSICAL_COUNT = 500; |
7 | 7 |
8 Polymer({ | 8 Polymer({ |
9 | 9 |
10 is: 'iron-list', | 10 is: 'iron-list', |
11 | 11 |
12 properties: { | 12 properties: { |
13 | 13 |
14 /** | 14 /** |
15 * An array containing items determining how many instances of the templat
e | 15 * An array containing items determining how many instances of the templat
e |
(...skipping 162 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
178 * The estimated scroll height based on `_physicalAverage` | 178 * The estimated scroll height based on `_physicalAverage` |
179 */ | 179 */ |
180 _estScrollHeight: 0, | 180 _estScrollHeight: 0, |
181 | 181 |
182 /** | 182 /** |
183 * The scroll height of the dom node | 183 * The scroll height of the dom node |
184 */ | 184 */ |
185 _scrollHeight: 0, | 185 _scrollHeight: 0, |
186 | 186 |
187 /** | 187 /** |
188 * The size of the viewport | 188 * The height of the list. This is referred as the viewport in the context o
f list. |
189 */ | 189 */ |
190 _viewportSize: 0, | 190 _viewportSize: 0, |
191 | 191 |
192 /** | 192 /** |
193 * An array of DOM nodes that are currently in the tree | 193 * An array of DOM nodes that are currently in the tree |
194 * @type {?Array<!TemplatizerNode>} | 194 * @type {?Array<!TemplatizerNode>} |
195 */ | 195 */ |
196 _physicalItems: null, | 196 _physicalItems: null, |
197 | 197 |
198 /** | 198 /** |
(...skipping 15 matching lines...) Expand all Loading... |
214 */ | 214 */ |
215 _collection: null, | 215 _collection: null, |
216 | 216 |
217 /** | 217 /** |
218 * True if the current item list was rendered for the first time | 218 * True if the current item list was rendered for the first time |
219 * after attached. | 219 * after attached. |
220 */ | 220 */ |
221 _itemsRendered: false, | 221 _itemsRendered: false, |
222 | 222 |
223 /** | 223 /** |
| 224 * The page that is currently rendered. |
| 225 */ |
| 226 _lastPage: null, |
| 227 |
| 228 /** |
| 229 * The max number of pages to render. One page is equivalent to the height o
f the list. |
| 230 */ |
| 231 _maxPages: 3, |
| 232 |
| 233 /** |
224 * The bottom of the physical content. | 234 * The bottom of the physical content. |
225 */ | 235 */ |
226 get _physicalBottom() { | 236 get _physicalBottom() { |
227 return this._physicalTop + this._physicalSize; | 237 return this._physicalTop + this._physicalSize; |
228 }, | 238 }, |
229 | 239 |
230 /** | 240 /** |
231 * The bottom of the scroll. | 241 * The bottom of the scroll. |
232 */ | 242 */ |
233 get _scrollBottom() { | 243 get _scrollBottom() { |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
285 }, | 295 }, |
286 | 296 |
287 /** | 297 /** |
288 * An optimal physical size such that we will have enough physical items | 298 * An optimal physical size such that we will have enough physical items |
289 * to fill up the viewport and recycle when the user scrolls. | 299 * to fill up the viewport and recycle when the user scrolls. |
290 * | 300 * |
291 * This default value assumes that we will at least have the equivalent | 301 * This default value assumes that we will at least have the equivalent |
292 * to a viewport of physical items above and below the user's viewport. | 302 * to a viewport of physical items above and below the user's viewport. |
293 */ | 303 */ |
294 get _optPhysicalSize() { | 304 get _optPhysicalSize() { |
295 return this._viewportSize * 3; | 305 return this._viewportSize * this._maxPages; |
296 }, | 306 }, |
297 | 307 |
298 /** | 308 /** |
299 * True if the current list is visible. | 309 * True if the current list is visible. |
300 */ | 310 */ |
301 get _isVisible() { | 311 get _isVisible() { |
302 return this._scroller && Boolean(this._scroller.offsetWidth || this._scrol
ler.offsetHeight); | 312 return this._scroller && Boolean(this._scroller.offsetWidth || this._scrol
ler.offsetHeight); |
303 }, | 313 }, |
304 | 314 |
305 /** | 315 /** |
(...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
386 | 396 |
387 /** | 397 /** |
388 * Update the models, the position of the | 398 * Update the models, the position of the |
389 * items in the viewport and recycle tiles as needed. | 399 * items in the viewport and recycle tiles as needed. |
390 */ | 400 */ |
391 _refresh: function() { | 401 _refresh: function() { |
392 // clamp the `scrollTop` value | 402 // clamp the `scrollTop` value |
393 // IE 10|11 scrollTop may go above `_maxScrollTop` | 403 // IE 10|11 scrollTop may go above `_maxScrollTop` |
394 // iOS `scrollTop` may go below 0 and above `_maxScrollTop` | 404 // iOS `scrollTop` may go below 0 and above `_maxScrollTop` |
395 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scroller.sc
rollTop)); | 405 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scroller.sc
rollTop)); |
396 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom; | 406 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; |
397 var ratio = this._ratio; | 407 var ratio = this._ratio; |
398 var delta = scrollTop - this._scrollPosition; | 408 var delta = scrollTop - this._scrollPosition; |
399 var recycledTiles = 0; | 409 var recycledTiles = 0; |
400 var hiddenContentSize = this._hiddenContentSize; | 410 var hiddenContentSize = this._hiddenContentSize; |
401 var currentRatio = ratio; | 411 var currentRatio = ratio; |
402 var movingUp = []; | 412 var movingUp = []; |
403 | 413 |
404 // track the last `scrollTop` | 414 // track the last `scrollTop` |
405 this._scrollPosition = scrollTop; | 415 this._scrollPosition = scrollTop; |
406 | 416 |
407 // clear cached visible index | 417 // clear cached visible index |
408 this._firstVisibleIndexVal = null; | 418 this._firstVisibleIndexVal = null; |
409 | 419 |
410 scrollBottom = this._scrollBottom; | 420 scrollBottom = this._scrollBottom; |
| 421 physicalBottom = this._physicalBottom; |
411 | 422 |
412 // random access | 423 // random access |
413 if (Math.abs(delta) > this._physicalSize) { | 424 if (Math.abs(delta) > this._physicalSize) { |
414 this._physicalTop += delta; | 425 this._physicalTop += delta; |
415 recycledTiles = Math.round(delta / this._physicalAverage); | 426 recycledTiles = Math.round(delta / this._physicalAverage); |
416 } | 427 } |
417 // scroll up | 428 // scroll up |
418 else if (delta < 0) { | 429 else if (delta < 0) { |
419 var topSpace = scrollTop - this._physicalTop; | 430 var topSpace = scrollTop - this._physicalTop; |
420 var virtualStart = this._virtualStart; | 431 var virtualStart = this._virtualStart; |
421 var physicalBottom = this._physicalBottom; | |
422 | 432 |
423 recycledTileSet = []; | 433 recycledTileSet = []; |
424 | 434 |
425 kth = this._physicalEnd; | 435 kth = this._physicalEnd; |
426 currentRatio = topSpace / hiddenContentSize; | 436 currentRatio = topSpace / hiddenContentSize; |
427 | 437 |
428 // move tiles from bottom to top | 438 // move tiles from bottom to top |
429 while ( | 439 while ( |
430 // approximate `currentRatio` to `ratio` | 440 // approximate `currentRatio` to `ratio` |
431 currentRatio < ratio && | 441 currentRatio < ratio && |
(...skipping 11 matching lines...) Expand all Loading... |
443 recycledTileSet.push(kth); | 453 recycledTileSet.push(kth); |
444 recycledTiles++; | 454 recycledTiles++; |
445 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; | 455 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; |
446 } | 456 } |
447 | 457 |
448 movingUp = recycledTileSet; | 458 movingUp = recycledTileSet; |
449 recycledTiles = -recycledTiles; | 459 recycledTiles = -recycledTiles; |
450 } | 460 } |
451 // scroll down | 461 // scroll down |
452 else if (delta > 0) { | 462 else if (delta > 0) { |
453 var bottomSpace = this._physicalBottom - scrollBottom; | 463 var bottomSpace = physicalBottom - scrollBottom; |
454 var virtualEnd = this._virtualEnd; | 464 var virtualEnd = this._virtualEnd; |
455 var lastVirtualItemIndex = this._virtualCount-1; | 465 var lastVirtualItemIndex = this._virtualCount-1; |
456 | 466 |
457 recycledTileSet = []; | 467 recycledTileSet = []; |
458 | 468 |
459 kth = this._physicalStart; | 469 kth = this._physicalStart; |
460 currentRatio = bottomSpace / hiddenContentSize; | 470 currentRatio = bottomSpace / hiddenContentSize; |
461 | 471 |
462 // move tiles from top to bottom | 472 // move tiles from top to bottom |
463 while ( | 473 while ( |
(...skipping 14 matching lines...) Expand all Loading... |
478 recycledTileSet.push(kth); | 488 recycledTileSet.push(kth); |
479 recycledTiles++; | 489 recycledTiles++; |
480 kth = (kth + 1) % this._physicalCount; | 490 kth = (kth + 1) % this._physicalCount; |
481 } | 491 } |
482 } | 492 } |
483 | 493 |
484 if (recycledTiles === 0) { | 494 if (recycledTiles === 0) { |
485 // If the list ever reach this case, the physical average is not signifi
cant enough | 495 // If the list ever reach this case, the physical average is not signifi
cant enough |
486 // to create all the items needed to cover the entire viewport. | 496 // to create all the items needed to cover the entire viewport. |
487 // e.g. A few items have a height that differs from the average by serve
ral order of magnitude. | 497 // e.g. A few items have a height that differs from the average by serve
ral order of magnitude. |
488 if (this._increasePoolIfNeeded()) { | 498 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { |
489 // yield and set models to the new items | 499 this.async(this._increasePool.bind(this, 1)); |
490 this.async(this._update); | |
491 } | 500 } |
492 } else { | 501 } else { |
493 this._virtualStart = this._virtualStart + recycledTiles; | 502 this._virtualStart = this._virtualStart + recycledTiles; |
494 this._update(recycledTileSet, movingUp); | 503 this._update(recycledTileSet, movingUp); |
495 } | 504 } |
496 }, | 505 }, |
497 | 506 |
498 /** | 507 /** |
499 * Update the list of items, starting from the `_virtualStartVal` item. | 508 * Update the list of items, starting from the `_virtualStartVal` item. |
500 * @param {!Array<number>=} itemSet | 509 * @param {!Array<number>=} itemSet |
(...skipping 11 matching lines...) Expand all Loading... |
512 while (movingUp.length) { | 521 while (movingUp.length) { |
513 this._physicalTop -= this._physicalSizes[movingUp.pop()]; | 522 this._physicalTop -= this._physicalSizes[movingUp.pop()]; |
514 } | 523 } |
515 } | 524 } |
516 // update the position of the items | 525 // update the position of the items |
517 this._positionItems(); | 526 this._positionItems(); |
518 | 527 |
519 // set the scroller size | 528 // set the scroller size |
520 this._updateScrollerSize(); | 529 this._updateScrollerSize(); |
521 | 530 |
522 // increase the pool of physical items if needed | 531 // increase the pool of physical items |
523 if (this._increasePoolIfNeeded()) { | 532 this._increasePoolIfNeeded(); |
524 // yield set models to the new items | |
525 this.async(this._update); | |
526 } | |
527 }, | 533 }, |
528 | 534 |
529 /** | 535 /** |
530 * Creates a pool of DOM elements and attaches them to the local dom. | 536 * Creates a pool of DOM elements and attaches them to the local dom. |
531 */ | 537 */ |
532 _createPool: function(size) { | 538 _createPool: function(size) { |
533 var physicalItems = new Array(size); | 539 var physicalItems = new Array(size); |
534 | 540 |
535 this._ensureTemplatized(); | 541 this._ensureTemplatized(); |
536 | 542 |
537 for (var i = 0; i < size; i++) { | 543 for (var i = 0; i < size; i++) { |
538 var inst = this.stamp(null); | 544 var inst = this.stamp(null); |
539 | |
540 // First element child is item; Safari doesn't support children[0] | 545 // First element child is item; Safari doesn't support children[0] |
541 // on a doc fragment | 546 // on a doc fragment |
542 physicalItems[i] = inst.root.querySelector('*'); | 547 physicalItems[i] = inst.root.querySelector('*'); |
543 Polymer.dom(this).appendChild(inst.root); | 548 Polymer.dom(this).appendChild(inst.root); |
544 } | 549 } |
545 | 550 |
546 return physicalItems; | 551 return physicalItems; |
547 }, | 552 }, |
548 | 553 |
549 /** | 554 /** |
550 * Increases the pool of physical items only if needed. | 555 * Increases the pool of physical items only if needed. |
551 * This function will allocate additional physical items | 556 * This function will allocate additional physical items |
552 * (limited by `MAX_PHYSICAL_COUNT`) if the content size is shorter than | 557 * if the physical size is shorter than `_optPhysicalSize` |
553 * `_optPhysicalSize` | |
554 * | |
555 * @return boolean | |
556 */ | 558 */ |
557 _increasePoolIfNeeded: function() { | 559 _increasePoolIfNeeded: function() { |
558 if (this._physicalAverage === 0) { | 560 if (this._viewportSize !== 0 && this._physicalSize < this._optPhysicalSize
) { |
559 return false; | 561 // 0 <= `currentPage` <= `_maxPages` |
560 } | 562 var currentPage = Math.floor(this._physicalSize / this._viewportSize); |
561 if (this._physicalBottom < this._scrollBottom || this._physicalTop > this.
_scrollPosition) { | 563 |
562 return this._increasePool(1); | 564 if (currentPage === 0) { |
563 } | 565 // fill the first page |
564 if (this._physicalSize < this._optPhysicalSize) { | 566 this.async(this._increasePool.bind(this, Math.round(this._physicalCoun
t * 0.5))); |
565 return this._increasePool(Math.round((this._optPhysicalSize - this._phys
icalSize) * 1.2 / this._physicalAverage)); | 567 } else if (this._lastPage !== currentPage) { |
| 568 // once a page is filled up, paint it and defer the next increase |
| 569 requestAnimationFrame(this._increasePool.bind(this, 1)); |
| 570 } else { |
| 571 // fill the rest of the pages |
| 572 this.async(this._increasePool.bind(this, 1)); |
| 573 } |
| 574 this._lastPage = currentPage; |
| 575 return true; |
566 } | 576 } |
567 return false; | 577 return false; |
568 }, | 578 }, |
569 | 579 |
570 /** | 580 /** |
571 * Increases the pool size. | 581 * Increases the pool size. |
572 */ | 582 */ |
573 _increasePool: function(missingItems) { | 583 _increasePool: function(missingItems) { |
574 // limit the size | 584 // limit the size |
575 var nextPhysicalCount = Math.min( | 585 var nextPhysicalCount = Math.min( |
576 this._physicalCount + missingItems, | 586 this._physicalCount + missingItems, |
577 this._virtualCount, | 587 this._virtualCount, |
578 MAX_PHYSICAL_COUNT | 588 MAX_PHYSICAL_COUNT |
579 ); | 589 ); |
580 | |
581 var prevPhysicalCount = this._physicalCount; | 590 var prevPhysicalCount = this._physicalCount; |
582 var delta = nextPhysicalCount - prevPhysicalCount; | 591 var delta = nextPhysicalCount - prevPhysicalCount; |
583 | 592 |
584 if (delta <= 0) { | 593 if (delta > 0) { |
585 return false; | 594 [].push.apply(this._physicalItems, this._createPool(delta)); |
| 595 [].push.apply(this._physicalSizes, new Array(delta)); |
| 596 |
| 597 this._physicalCount = prevPhysicalCount + delta; |
| 598 // tail call |
| 599 return this._update(); |
586 } | 600 } |
587 | |
588 [].push.apply(this._physicalItems, this._createPool(delta)); | |
589 [].push.apply(this._physicalSizes, new Array(delta)); | |
590 | |
591 this._physicalCount = prevPhysicalCount + delta; | |
592 | |
593 return true; | |
594 }, | 601 }, |
595 | 602 |
596 /** | 603 /** |
597 * Render a new list of items. This method does exactly the same as `update`
, | 604 * Render a new list of items. This method does exactly the same as `update`
, |
598 * but it also ensures that only one `update` cycle is created. | 605 * but it also ensures that only one `update` cycle is created. |
599 */ | 606 */ |
600 _render: function() { | 607 _render: function() { |
601 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; | 608 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; |
602 | 609 |
603 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { | 610 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { |
| 611 this._lastPage = 0; |
604 this._update(); | 612 this._update(); |
605 this._itemsRendered = true; | 613 this._itemsRendered = true; |
606 } | 614 } |
607 }, | 615 }, |
608 | 616 |
609 /** | 617 /** |
610 * Templetizes the user template. | 618 * Templetizes the user template. |
611 */ | 619 */ |
612 _ensureTemplatized: function() { | 620 _ensureTemplatized: function() { |
613 if (!this.ctor) { | 621 if (!this.ctor) { |
(...skipping 343 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
957 | 965 |
958 // estimate new physical offset | 966 // estimate new physical offset |
959 this._physicalTop = this._virtualStart * this._physicalAverage; | 967 this._physicalTop = this._virtualStart * this._physicalAverage; |
960 | 968 |
961 var currentTopItem = this._physicalStart; | 969 var currentTopItem = this._physicalStart; |
962 var currentVirtualItem = this._virtualStart; | 970 var currentVirtualItem = this._virtualStart; |
963 var targetOffsetTop = 0; | 971 var targetOffsetTop = 0; |
964 var hiddenContentSize = this._hiddenContentSize; | 972 var hiddenContentSize = this._hiddenContentSize; |
965 | 973 |
966 // scroll to the item as much as we can | 974 // scroll to the item as much as we can |
967 while (currentVirtualItem !== idx && targetOffsetTop < hiddenContentSize)
{ | 975 while (currentVirtualItem < idx && targetOffsetTop < hiddenContentSize) { |
968 targetOffsetTop = targetOffsetTop + this._physicalSizes[currentTopItem]; | 976 targetOffsetTop = targetOffsetTop + this._physicalSizes[currentTopItem]; |
969 currentTopItem = (currentTopItem + 1) % this._physicalCount; | 977 currentTopItem = (currentTopItem + 1) % this._physicalCount; |
970 currentVirtualItem++; | 978 currentVirtualItem++; |
971 } | 979 } |
972 | 980 |
973 // update the scroller size | 981 // update the scroller size |
974 this._updateScrollerSize(true); | 982 this._updateScrollerSize(true); |
975 | 983 |
976 // update the position of the items | 984 // update the position of the items |
977 this._positionItems(); | 985 this._positionItems(); |
978 | 986 |
979 // set the new scroll position | 987 // set the new scroll position |
980 this._resetScrollPosition(this._physicalTop + targetOffsetTop + 1); | 988 this._resetScrollPosition(this._physicalTop + targetOffsetTop + 1); |
981 | 989 |
982 // increase the pool of physical items if needed | 990 // increase the pool of physical items if needed |
983 if (this._increasePoolIfNeeded()) { | 991 this._increasePoolIfNeeded(); |
984 // yield set models to the new items | 992 |
985 this.async(this._update); | |
986 } | |
987 // clear cached visible index | 993 // clear cached visible index |
988 this._firstVisibleIndexVal = null; | 994 this._firstVisibleIndexVal = null; |
989 }, | 995 }, |
990 | 996 |
991 /** | 997 /** |
992 * Reset the physical average and the average count. | 998 * Reset the physical average and the average count. |
993 */ | 999 */ |
994 _resetAverage: function() { | 1000 _resetAverage: function() { |
995 this._physicalAverage = 0; | 1001 this._physicalAverage = 0; |
996 this._physicalAverageCount = 0; | 1002 this._physicalAverageCount = 0; |
(...skipping 158 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1155 var pidx = this._physicalIndexForKey[key]; | 1161 var pidx = this._physicalIndexForKey[key]; |
1156 | 1162 |
1157 if (pidx !== undefined) { | 1163 if (pidx !== undefined) { |
1158 this._updateMetrics([pidx]); | 1164 this._updateMetrics([pidx]); |
1159 this._positionItems(); | 1165 this._positionItems(); |
1160 } | 1166 } |
1161 } | 1167 } |
1162 }); | 1168 }); |
1163 | 1169 |
1164 })(); | 1170 })(); |
OLD | NEW |