From c85426e0683ff348a2434b584f0ae6b07d2a126b Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Mon, 28 Oct 2013 15:47:59 -0500 Subject: [PATCH] Working on virtual list viewport stuff --- dist/js/ionic.js | 90 ++++++++++++++++++++++------------ js/views/listViewScroll.js | 51 +++++++++++++++---- js/views/scrollView.js | 39 +++++++-------- test/biglists.html | 5 +- test/js/views/listView.unit.js | 50 +++++++++++++++++++ 5 files changed, 174 insertions(+), 61 deletions(-) create mode 100644 test/js/views/listView.unit.js diff --git a/dist/js/ionic.js b/dist/js/ionic.js index bf5b8a6544..1c3e09fa83 100644 --- a/dist/js/ionic.js +++ b/dist/js/ionic.js @@ -2726,6 +2726,17 @@ window.ionic = { initialize: function(opts) { var _this = this; + opts = ionic.extend({ + virtualRemoveThreshold: -200, + virtualAddThreshold: 200 + }, opts); + + ionic.extend(this, opts); + + if(!this.itemHeight && this.listEl) { + this.itemHeight = this.listEl.children[0] && parseInt(this.listEl.children[0].style.height); + } + ionic.views.ListView.__super__.initialize.call(this, opts); this.onRefresh = opts.onRefresh || function() {}; @@ -2755,21 +2766,43 @@ window.ionic = { }, didScroll: function(e) { - console.log('Scrolling', Date.now()); if(this.isVirtual) { var itemHeight = this.itemHeight; var totalItems = this.listEl.children.length; - var scrollHeight = e.target.scrollHeight + + var scrollHeight = e.target.scrollHeight; + var viewportHeight = this.el.parentNode.offsetHeight; + var scrollTop = e.scrollTop; - var height = this.el.parentNode.offsetHeight; - console.log('LIST VIEW SCROLLED', e, itemHeight, scrollHeight, height); - var itemsPerPage = Math.floor(height / itemHeight); - var first = parseInt(scrollTop / itemHeight); - console.log('FITS', itemsPerPage, 'per page, starting at', first); + var highWater = e.scrollTop + this.virtualRemoveThreshold; + var lowWater = Math.min(scrollHeight + viewportHeight, e.scrollTop + viewportHeight + this.virtualAddThreshold); + + //console.log('LIST VIEW SCROLLED', e, itemHeight, scrollHeight, viewportHeight); + var itemsPerViewport = Math.floor((lowWater - highWater) / itemHeight); + var first = parseInt(highWater / itemHeight); + var last = parseInt(lowWater / itemHeight); + + //console.log('FITS', itemsPerViewport, 'per page, starting at', first); + + this._virtualItemsToRemove = Array.prototype.slice.call(this.listEl.children, 0, first); + + var nodes = Array.prototype.slice.call(this.listEl.children, first, first + itemsPerViewport); + + this.renderViewport && this.renderViewport(highWater, lowWater, first, last); + } + }, + + didStopScrolling: function(e) { + if(this.isVirtual) { + //console.log('DONE SCROLLING, Need to remove', this._virtualItemsToRemove); + for(var i = 0; i < this._virtualItemsToRemove.length; i++) { + var el = this._virtualItemsToRemove[i]; + //el.parentNode.removeChild(el); + this.didHideItem && this.didHideItem(i); + } + // Once scrolling stops, check if we need to remove old items - var nodes = Array.prototype.slice.call(this.listEl.children, first, first + itemsPerPage); - console.log('Showing these', nodes.length, 'nodes:', nodes); } }, @@ -3244,11 +3277,7 @@ window.ionic = { // Execute the scrollEnd event after 400ms the wheel stopped scrolling clearTimeout(this.wheelTimeout); this.wheelTimeout = setTimeout(function () { - ionic.trigger(this.scrollEndEventName, { - target: this.el, - scrollLeft: this.x, - scrollTop: this.y - }); + that._doneScrolling(); }, 400); e.preventDefault(); @@ -3333,18 +3362,10 @@ window.ionic = { // Triggered to end scroll, once the final animation has ended if(needsWrapping && this._didEndScroll) { this._didEndScroll = false; - ionic.trigger(this.scrollEndEventName, { - target: this.el, - scrollLeft: this.x, - scrollTop: this.y - }); + this._doneScrolling(); } else if(!needsWrapping) { this._didEndScroll = false; - ionic.trigger(this.scrollEndEventName, { - target: this.el, - scrollLeft: this.x, - scrollTop: this.y - }); + this._doneScrolling(); } this.el.style.webkitTransitionDuration = '0'; @@ -3587,7 +3608,7 @@ window.ionic = { newX = momentumX.destination; newY = momentumY.destination; - // Calcualte the longest required time for the momentum animation and + // Calculate the longest required time for the momentum animation and // use that. time = Math.max(momentumX.duration, momentumY.duration); } @@ -3603,13 +3624,22 @@ window.ionic = { _this.scrollTo(newX, newY, time, easing); } else { // We are done - ionic.trigger(_this.scrollEndEventName, { - target: _this.el, - scrollLeft: _this.x, - scrollTop: _this.y - }); + _this._doneScrolling(); } }); + }, + + _doneScrolling: function() { + this.didStopScrolling && this.didStopScrolling({ + target: this.el, + scrollLeft: this.x, + scrollTop: this.y + }); + ionic.trigger(this.scrollEndEventName, { + target: this.el, + scrollLeft: this.x, + scrollTop: this.y + }); } }, { DECEL_RATE_NORMAL: 0.998, diff --git a/js/views/listViewScroll.js b/js/views/listViewScroll.js index ca5f467913..ce6dee72cd 100644 --- a/js/views/listViewScroll.js +++ b/js/views/listViewScroll.js @@ -364,6 +364,17 @@ initialize: function(opts) { var _this = this; + opts = ionic.extend({ + virtualRemoveThreshold: -200, + virtualAddThreshold: 200 + }, opts); + + ionic.extend(this, opts); + + if(!this.itemHeight && this.listEl) { + this.itemHeight = this.listEl.children[0] && parseInt(this.listEl.children[0].style.height); + } + ionic.views.ListView.__super__.initialize.call(this, opts); this.onRefresh = opts.onRefresh || function() {}; @@ -393,21 +404,43 @@ }, didScroll: function(e) { - console.log('Scrolling', Date.now()); if(this.isVirtual) { var itemHeight = this.itemHeight; var totalItems = this.listEl.children.length; - var scrollHeight = e.target.scrollHeight + + var scrollHeight = e.target.scrollHeight; + var viewportHeight = this.el.parentNode.offsetHeight; + var scrollTop = e.scrollTop; - var height = this.el.parentNode.offsetHeight; - console.log('LIST VIEW SCROLLED', e, itemHeight, scrollHeight, height); - var itemsPerPage = Math.floor(height / itemHeight); - var first = parseInt(scrollTop / itemHeight); - console.log('FITS', itemsPerPage, 'per page, starting at', first); + var highWater = e.scrollTop + this.virtualRemoveThreshold; + var lowWater = Math.min(scrollHeight + viewportHeight, e.scrollTop + viewportHeight + this.virtualAddThreshold); + + //console.log('LIST VIEW SCROLLED', e, itemHeight, scrollHeight, viewportHeight); + var itemsPerViewport = Math.floor((lowWater - highWater) / itemHeight); + var first = parseInt(highWater / itemHeight); + var last = parseInt(lowWater / itemHeight); + + //console.log('FITS', itemsPerViewport, 'per page, starting at', first); + + this._virtualItemsToRemove = Array.prototype.slice.call(this.listEl.children, 0, first); + + var nodes = Array.prototype.slice.call(this.listEl.children, first, first + itemsPerViewport); + + this.renderViewport && this.renderViewport(highWater, lowWater, first, last); + } + }, + + didStopScrolling: function(e) { + if(this.isVirtual) { + //console.log('DONE SCROLLING, Need to remove', this._virtualItemsToRemove); + for(var i = 0; i < this._virtualItemsToRemove.length; i++) { + var el = this._virtualItemsToRemove[i]; + //el.parentNode.removeChild(el); + this.didHideItem && this.didHideItem(i); + } + // Once scrolling stops, check if we need to remove old items - var nodes = Array.prototype.slice.call(this.listEl.children, first, first + itemsPerPage); - console.log('Showing these', nodes.length, 'nodes:', nodes); } }, diff --git a/js/views/scrollView.js b/js/views/scrollView.js index 3e81bee56e..53edcdfc86 100644 --- a/js/views/scrollView.js +++ b/js/views/scrollView.js @@ -214,11 +214,7 @@ // Execute the scrollEnd event after 400ms the wheel stopped scrolling clearTimeout(this.wheelTimeout); this.wheelTimeout = setTimeout(function () { - ionic.trigger(this.scrollEndEventName, { - target: this.el, - scrollLeft: this.x, - scrollTop: this.y - }); + that._doneScrolling(); }, 400); e.preventDefault(); @@ -303,18 +299,10 @@ // Triggered to end scroll, once the final animation has ended if(needsWrapping && this._didEndScroll) { this._didEndScroll = false; - ionic.trigger(this.scrollEndEventName, { - target: this.el, - scrollLeft: this.x, - scrollTop: this.y - }); + this._doneScrolling(); } else if(!needsWrapping) { this._didEndScroll = false; - ionic.trigger(this.scrollEndEventName, { - target: this.el, - scrollLeft: this.x, - scrollTop: this.y - }); + this._doneScrolling(); } this.el.style.webkitTransitionDuration = '0'; @@ -557,7 +545,7 @@ newX = momentumX.destination; newY = momentumY.destination; - // Calcualte the longest required time for the momentum animation and + // Calculate the longest required time for the momentum animation and // use that. time = Math.max(momentumX.duration, momentumY.duration); } @@ -573,13 +561,22 @@ _this.scrollTo(newX, newY, time, easing); } else { // We are done - ionic.trigger(_this.scrollEndEventName, { - target: _this.el, - scrollLeft: _this.x, - scrollTop: _this.y - }); + _this._doneScrolling(); } }); + }, + + _doneScrolling: function() { + this.didStopScrolling && this.didStopScrolling({ + target: this.el, + scrollLeft: this.x, + scrollTop: this.y + }); + ionic.trigger(this.scrollEndEventName, { + target: this.el, + scrollLeft: this.x, + scrollTop: this.y + }); } }, { DECEL_RATE_NORMAL: 0.998, diff --git a/test/biglists.html b/test/biglists.html index 692686be48..259fe4ffb8 100644 --- a/test/biglists.html +++ b/test/biglists.html @@ -1281,7 +1281,10 @@ el: list, listEl: list.children[0], isVirtual: true, - itemHeight: 50 + itemHeight: 50, + renderViewport: function(high, low, start, end) { + console.log('RENDER VIEWPORT', high, low, start, end); + } }); diff --git a/test/js/views/listView.unit.js b/test/js/views/listView.unit.js new file mode 100644 index 0000000000..35c0d090da --- /dev/null +++ b/test/js/views/listView.unit.js @@ -0,0 +1,50 @@ +describe('List View', function() { + var h, listEl; + beforeEach(function() { + h = document.createElement('div'); + h.id = 'content'; + h.className = 'scroll' + var l = document.createElement('ul'); + l.className = 'list scroll'; + h.appendChild(l); + + listEl = l; + + for(var i = 0; i < 1000; i++) { + var li = document.createElement('li'); + li.style.height = '50px'; + l.appendChild(li); + } + }); + + it('Should init', function() { + var list = new ionic.views.ListView({ + el: h, + }); + }); + + iit('Should init item height from CSS', function() { + var list = new ionic.views.ListView({ + el: h, + listEl: listEl, + isVirtual: true, + }); + + expect(list.itemHeight).toEqual(50); + }); + + iit('Should support virtual scrolling', function() { + var list = new ionic.views.ListView({ + el: h, + listEl: listEl, + isVirtual: true, + itemHeight: 50 + }); + + expect(list.itemHeight).toEqual(50); + + list.renderViewport = function(high, low, start, end) { + }; + }); + +});