From 8676deb325aa0baeabe97ba35a67a8c2e7f0c3a7 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Sun, 14 Jun 2015 14:43:47 -0500 Subject: [PATCH] Start of virtual scrolling for lists --- ionic/components/content/content.js | 14 +++ ionic/components/list/list.js | 36 +++++- ionic/components/list/test/infinite/index.js | 50 ++++++++ ionic/components/list/test/infinite/main.html | 15 +++ ionic/components/list/virtual.js | 109 ++++++++++++++++++ 5 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 ionic/components/list/test/infinite/index.js create mode 100644 ionic/components/list/test/infinite/main.html create mode 100644 ionic/components/list/virtual.js diff --git a/ionic/components/content/content.js b/ionic/components/content/content.js index 26c8aca918..8015fd88cb 100644 --- a/ionic/components/content/content.js +++ b/ionic/components/content/content.js @@ -15,5 +15,19 @@ export class Content { // but we should be able to stamp out this behavior with a base IonicComponent // or something, so all elements have a domElement reference or a getElement() method this.domElement = elementRef.domElement; + + setTimeout(() => { + this.scrollElement = this.domElement.children[0]; + }); + } + + addScrollEventListener(handler) { + if(!this.scrollElement) { return; } + + this.scrollElement.addEventListener('scroll', handler); + + return () => { + this.scrollElement.removeEventListener('scroll', handler); + } } } diff --git a/ionic/components/list/list.js b/ionic/components/list/list.js index 91b64410fa..9e751e9de9 100644 --- a/ionic/components/list/list.js +++ b/ionic/components/list/list.js @@ -3,26 +3,52 @@ import {Component, Directive} from 'angular2/src/core/annotations_impl/annotatio import {View} from 'angular2/src/core/annotations_impl/view'; import {IonicComponent} from 'ionic/config/component' +import {ListVirtualScroll} from './virtual' + +import * as util from 'ionic/util'; @Component({ - selector: 'ion-list' + selector: 'ion-list', + properties: [ + 'items', + 'virtual', + 'content' + ] }) @View({ template: `` }) export class List { - constructor() { - - } constructor( elementRef: ElementRef ) { this.domElement = elementRef.domElement; this.config = List.config.invoke(this); + + setTimeout(() => { + console.log('Content', this.content); + console.log('Virtual?', this.virtual); + console.log('Items?', this.items.length, 'of \'em'); + + if(util.isDefined(this.virtual)) { + this._initVirtualScrolling(); + } + }) + } + + _initVirtualScrolling() { + if(!this.content) { + return; + } + + this._virtualScrollingManager = new ListVirtualScroll(this); + } + + setItemTemplate(item) { + this.itemTemplate = item; } } - new IonicComponent(List, { propClasses: ['inset'] }) diff --git a/ionic/components/list/test/infinite/index.js b/ionic/components/list/test/infinite/index.js new file mode 100644 index 0000000000..6477e055e2 --- /dev/null +++ b/ionic/components/list/test/infinite/index.js @@ -0,0 +1,50 @@ +import {bootstrap, NgFor, ProtoViewRef, ViewContainerRef} from 'angular2/angular2' +import {Component, Directive} from 'angular2/src/core/annotations_impl/annotations'; +import {View} from 'angular2/src/core/annotations_impl/view'; +import {Parent} from 'angular2/src/core/annotations_impl/visibility'; + +import {Content, List, Item} from 'ionic/ionic'; + + +@Component({ selector: 'ion-app' }) +@View({ + templateUrl: 'main.html', + directives: [Content, List, Item, ItemCellTemplate, NgFor] +}) +class IonicApp { + constructor() { + console.log('IonicApp Start') + + this.items = [] + for(let i = 0; i < 1000; i++) { + this.items.push({ + title: 'Item ' + i + }) + } + } +} + +/* + Used to find and register headers in a view, and this directive's + content will be moved up to the common navbar location, and created + using the same context as the view's content area. +*/ +@Directive({ + selector: 'template[cell]' +}) +export class ItemCellTemplate { + constructor(@Parent() list: List, viewContainer: ViewContainerRef, protoViewRef: ProtoViewRef) { + console.log('Item cell template', list, viewContainer, protoViewRef); + + this.protoViewRef = protoViewRef; + this.viewContainer = viewContainer; + + list.setItemTemplate(this); + } +} + + + +export function main() { + bootstrap(IonicApp); +} diff --git a/ionic/components/list/test/infinite/main.html b/ionic/components/list/test/infinite/main.html new file mode 100644 index 0000000000..049fad1bc4 --- /dev/null +++ b/ionic/components/list/test/infinite/main.html @@ -0,0 +1,15 @@ + + + + + + + + {{$item.title}} + + + + + + + diff --git a/ionic/components/list/virtual.js b/ionic/components/list/virtual.js new file mode 100644 index 0000000000..17877bf237 --- /dev/null +++ b/ionic/components/list/virtual.js @@ -0,0 +1,109 @@ + + +export class ListVirtualScroll { + constructor(list) { + this.list = list; + this.content = this.list.content; + + this.viewportHeight = this.content.domElement.offsetHeight; + + this.viewContainer = this.list.itemTemplate.viewContainer; + + this.itemHeight = 60; + + this.shownItems = {}; + this.enteringItems = []; + this.leavingItems = []; + + // Compute the initial sizes + this.resize(); + + this.content.addScrollEventListener((event) => { + this._handleVirtualScroll(event); + }); + } + + resize() { + this.viewportHeight = this.content.domElement.offsetHeight; + this.viewportScrollHeight = this.content.scrollElement.scrollHeight; + + this.virtualHeight = this.list.items.length * this.itemHeight; + this.itemsPerScreen = this.viewportHeight / this.itemHeight; + + console.log('VIRTUAL: resize(viewportHeight:', this.viewportHeight, + 'viewportScrollHeight:', this.viewportScrollHeight, 'virtualHeight:', this.virtualHeight, + ', itemsPerScreen:', this.itemsPerScreen, ')'); + } + + _handleVirtualScroll(event) { + let item; + let shownItemRef; + + let st = event.target.scrollTop; + let sh = event.target.scrollHeight; + + let topIndex = Math.floor(st / this.itemHeight); + let bottomIndex = Math.floor((st / this.itemHeight) + this.itemsPerScreen); + + let items = this.list.items; + + // Key iterate the shown items map + // and compare the index to our index range, + // pushing the items to remove to our leaving + // list if they're ouside this range. + for(let i in this.shownItems) { + if(i < topIndex || i > bottomIndex) { + this.leavingItems.push(this.shownItems[i]); + delete this.shownItems[i]; + } + } + + let realIndex = 0; + // Iterate the set of items that will be rendered, using the + // index from the actual items list as the map for the + // virtual items we draw + for(let i = topIndex, realIndex = 0; i < bottomIndex && i < items.length; i++, realIndex++) { + item = items[i]; + console.log('Drawing item', i); + + shownItemRef = this.shownItems[i]; + + // Is this a new item? + if(!shownItemRef) { + let itemView = this.viewContainer.create(this.list.itemTemplate.protoViewRef, realIndex); + itemView.setLocal('\$implicit', item); + itemView.setLocal('\$item', item); + shownItemRef = new VirtualItemRef(item, i, realIndex, itemView); + + this.shownItems[i] = shownItemRef; + this.enteringItems.push(shownItemRef); + } + + + //tuple.view = viewContainer.create(protoViewRef, tuple.record.currentIndex); + + } + + while(this.leavingItems.length) { + let itemRef = this.leavingItems.pop(); + this.viewContainer.remove(itemRef.realIndex); + } + + console.log('VIRTUAL SCROLL: scroll(scrollTop:', st, 'topIndex:', topIndex, 'bottomIndex:', bottomIndex, ')'); + console.log('Container has', this.list.domElement.children.length, 'children'); + } + + cellAtIndex(index) { + + } + +} + +class VirtualItemRef { + constructor(item, index, realIndex, view) { + this.item = item; + this.index = index; + this.realIndex = realIndex; + this.view = view; + } +}