perf(virtualScroll): improve UIWebView virtual scroll

Related #6104
This commit is contained in:
Adam Bradley
2016-06-10 11:30:21 -05:00
parent 99fdcc0749
commit ff1daa6027
10 changed files with 156 additions and 120 deletions

View File

@ -5,11 +5,18 @@
ion-img {
position: relative;
display: block;
display: flex;
overflow: hidden;
align-items: center;
justify-content: center;
}
ion-img img {
display: block;
flex-shrink: 0;
min-width: 100%;
min-height: 100%;
}
ion-img .img-placeholder {

View File

@ -1,4 +1,4 @@
import {Component, Input, ElementRef, ChangeDetectionStrategy, ViewEncapsulation, NgZone} from '@angular/core';
import {Component, Input, HostBinding, ElementRef, ChangeDetectionStrategy, ViewEncapsulation, NgZone} from '@angular/core';
import {nativeRaf} from '../../util/dom';
import {isPresent} from '../../util/util';
@ -19,6 +19,7 @@ export class Img {
private _w: string;
private _h: string;
private _enabled: boolean = true;
private _init: boolean;
constructor(private _elementRef: ElementRef, private _platform: Platform, private _zone: NgZone) {}
@ -30,11 +31,18 @@ export class Img {
this._src = isPresent(val) ? val : '';
this._normalizeSrc = tmpImg.src;
if (this._init) {
this._update();
}
}
ngOnInit() {
this._init = true;
this._update();
}
private _update() {
if (this._enabled && this._src !== '' && this.isVisible()) {
if (this._enabled && this._src !== '') {
// actively update the image
for (var i = this._imgs.length - 1; i >= 0; i--) {
@ -56,8 +64,15 @@ export class Img {
if (!this._imgs.length) {
this._zone.runOutsideAngular(() => {
let img = new Image();
img.style.width = this._w;
img.style.height = this._h;
img.style.width = this._width;
img.style.height = this._height;
if (isPresent(this.alt)) {
img.alt = this.alt;
}
if (isPresent(this.title)) {
img.title = this.title;
}
img.addEventListener('load', () => {
if (img.src === this._normalizeSrc) {
@ -92,19 +107,45 @@ export class Img {
this._update();
}
isVisible() {
let bounds = this._elementRef.nativeElement.getBoundingClientRect();
return bounds.bottom > 0 && bounds.top < this._platform.height();
}
@Input()
set width(val: string | number) {
this._w = (typeof val === 'number') ? val + 'px' : val;
this._w = getUnitValue(val);
}
@Input()
set height(val: string | number) {
this._h = (typeof val === 'number') ? val + 'px' : val;
this._h = getUnitValue(val);
}
@Input() alt: string;
@Input() title: string;
@HostBinding('style.width')
get _width(): string {
return isPresent(this._w) ? this._w : '';
}
@HostBinding('style.height')
get _height(): string {
return isPresent(this._h) ? this._h : '';
}
}
function getUnitValue(val: any): string {
if (isPresent(val)) {
if (typeof val === 'string') {
if (val.indexOf('%') > -1 || val.indexOf('px') > -1) {
return val;
}
if (val.length) {
return val + 'px';
}
} else if (typeof val === 'number') {
return val + 'px';
}
}
return '';
}

View File

@ -53,4 +53,6 @@ class E2EApp {
root = E2EPage;
}
ionicBootstrap(E2EApp);
ionicBootstrap(E2EApp, null, {
prodMode: true
});

View File

@ -7,14 +7,15 @@ import {ionicBootstrap} from '../../../../../src';
encapsulation: ViewEncapsulation.None
})
class E2EPage {
items = [];
items: any[] = [];
constructor() {
for (var i = 0; i < 500; i++) {
for (var i = 0; i < 1000; i++) {
this.items.push({
imgSrc: `../../img/img/${images[rotateImg]}.jpg?${Math.random()}`,
imgHeight: Math.floor((Math.random() * 50) + 150),
name: i + ' - ' + images[rotateImg],
imgSrc: getImgSrc(),
avatarSrc: getImgSrc(),
imgHeight: Math.floor((Math.random() * 50) + 150),
content: lorem.substring(0, (Math.random() * (lorem.length - 100)) + 100)
});
@ -33,7 +34,9 @@ class E2EApp {
root = E2EPage;
}
ionicBootstrap(E2EApp);
ionicBootstrap(E2EApp, null, {
prodMode: true
});
const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
@ -50,4 +53,11 @@ const images = [
'mirth-mobile',
];
function getImgSrc() {
let src = `../../img/img/${images[rotateImg]}.jpg?${Math.round(Math.random() * 10000000)}`;
rotateImg++;
if (rotateImg === images.length) rotateImg = 0;
return src;
}
let rotateImg = 0;

View File

@ -7,12 +7,12 @@
<ion-card *virtualItem="let item">
<div>
<ion-img [src]="item.imgSrc" [height]="item.imgHeight"></ion-img>
<ion-img [src]="item.imgSrc" [height]="item.imgHeight" [alt]="item.name"></ion-img>
</div>
<ion-item>
<ion-avatar item-left>
<ion-img [src]="item.imgSrc"></ion-img>
<ion-img [src]="item.avatarSrc" height="40" width="40"></ion-img>
</ion-avatar>
<h2>{{ item.name }}</h2>
</ion-item>

View File

@ -80,7 +80,9 @@ class E2EApp {
root = E2EPage;
}
ionicBootstrap(E2EApp);
ionicBootstrap(E2EApp, null, {
prodMode: true
});
var monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

View File

@ -6,7 +6,7 @@ import {ionicBootstrap} from '../../../../../src';
templateUrl: 'main.html'
})
class E2EPage {
items = [];
items: any[] = [];
constructor() {
@ -21,7 +21,7 @@ class E2EPage {
}
}
headerFn(record, recordIndex) {
headerFn(record: any, recordIndex: number) {
if (recordIndex > 0 && recordIndex % 100 === 0) {
return recordIndex;
}
@ -38,4 +38,6 @@ class E2EApp {
root = E2EPage;
}
ionicBootstrap(E2EApp);
ionicBootstrap(E2EApp, null, {
prodMode: true
});

View File

@ -75,11 +75,11 @@ describe('VirtualScroll', () => {
data.hdrWidth = data.viewWidth; // 100%, 1 per row
data.ftrWidth = data.viewWidth; // 100%, 1 per row
headerFn = function(record) {
headerFn = function(record: any) {
return (record === 0) ? 'Header' : null;
};
footerFn = function(record) {
footerFn = function(record: any) {
return (record === 4) ? 'Footer' : null;
};
@ -159,7 +159,7 @@ describe('VirtualScroll', () => {
data.itmWidth = 90; // 2 per row
data.hdrWidth = data.viewWidth; // 100%, 1 per row
headerFn = function(record) {
headerFn = function(record: any) {
return (record === 0) ? 'Header' : null;
};
@ -267,10 +267,10 @@ describe('VirtualScroll', () => {
let endCellIndex = 4;
populateNodeData(startCellIndex, endCellIndex, data.viewWidth, true,
cells, records, nodes, viewContainer,
itmTmp, hdrTmp, ftrTmp, true);
cells, records, nodes, viewContainer,
itmTmp, hdrTmp, ftrTmp, true);
expect(nodes.length).toBe(3);
expect(nodes.length).toBe(6);
expect(nodes[0].cell).toBe(2);
expect(nodes[1].cell).toBe(3);
@ -522,9 +522,9 @@ describe('VirtualScroll', () => {
let headerFn: Function;
let footerFn: Function;
let data: VirtualData;
let itmTmp = null;
let hdrTmp = null;
let ftrTmp = null;
let itmTmp: any = {};
let hdrTmp: any = {};
let ftrTmp: any = {};
let viewContainer: any = {
createEmbeddedView: function() {
return getView();

View File

@ -1,15 +1,15 @@
import {Directive, ContentChild, ContentChildren, QueryList, IterableDiffers, IterableDiffer, TrackByFn, Input, Optional, Renderer, ElementRef, ChangeDetectorRef, NgZone, TemplateRef, ViewContainerRef, DoCheck, AfterContentInit, OnDestroy} from '@angular/core';
import {Directive, ContentChild, ContentChildren, QueryList, IterableDiffers, IterableDiffer, TrackByFn, Input, Optional, Renderer, ElementRef, ChangeDetectorRef, NgZone, DoCheck, AfterContentInit, OnDestroy} from '@angular/core';
import {adjustRendered, calcDimensions, estimateHeight, initReadNodes, processRecords, populateNodeData, updateDimensions, writeToNodes} from './virtual-util';
import {Config} from '../../config/config';
import {Content} from '../content/content';
import {Img} from '../img/img';
import {isBlank, isPresent, isFunction} from '../../util/util';
import {nativeRaf, nativeTimeout, clearNativeTimeout} from '../../util/dom';
import {Platform} from '../../platform/platform';
import {ViewController} from '../nav/view-controller';
import {VirtualItem, VirtualHeader, VirtualFooter} from './virtual-item';
import {VirtualCell, VirtualNode, VirtualData} from './virtual-util';
import {processRecords, populateNodeData, initReadNodes, writeToNodes, updateDimensions, adjustRendered, calcDimensions, estimateHeight} from './virtual-util';
import {isBlank, isPresent, isFunction} from '../../util/util';
import {rafFrames, nativeRaf, cancelRaf, pointerCoord, nativeTimeout, clearNativeTimeout} from '../../util/dom';
import {Img} from '../img/img';
import {VirtualCell, VirtualData, VirtualNode} from './virtual-util';
import {VirtualFooter, VirtualHeader, VirtualItem} from './virtual-item';
/**
@ -410,9 +410,6 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
// ******** DOM WRITE ****************
self.renderVirtual();
// ******** DOM WRITE ****************
self._renderer.setElementClass(self._elementRef.nativeElement, 'virtual-scroll', true);
// list for scroll events
self.addScrollListener();
});
@ -436,7 +433,7 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
this._ftrTmp && this._ftrTmp.templateRef, true);
// ******** DOM WRITE ****************
this.detectChanges();
this._cd.detectChanges();
// wait a frame before trying to read and calculate the dimensions
nativeRaf(this.postRenderVirtual.bind(this));
@ -447,13 +444,6 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
* DOM READ THEN DOM WRITE
*/
postRenderVirtual() {
// ******** DOM READ ****************
calcDimensions(this._data, this._elementRef.nativeElement.parentElement,
this.approxItemWidth, this.approxItemHeight,
this.approxHeaderWidth, this.approxHeaderHeight,
this.approxFooterWidth, this.approxFooterHeight,
this.bufferRatio);
// ******** DOM READ THEN DOM WRITE ****************
initReadNodes(this._nodes, this._cells, this._data);
@ -461,6 +451,9 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
// ******** DOM READS ABOVE / DOM WRITES BELOW ****************
// ******** DOM WRITE ****************
this._renderer.setElementClass(this._elementRef.nativeElement, 'virtual-scroll', true);
// ******** DOM WRITE ****************
writeToNodes(this._nodes, this._cells, this._records.length);
@ -470,20 +463,6 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
);
}
/**
* @private
*/
detectChanges() {
let node: VirtualNode;
for (var i = 0; i < this._nodes.length; i++) {
node = this._nodes[i];
if (node.hasChanges) {
node.view['detectChanges']();
node.hasChanges = false;
}
}
}
/**
* @private
*/
@ -495,34 +474,23 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
if (this._queue === QUEUE_CHANGE_DETECTION) {
// ******** DOM WRITE ****************
this.detectChanges();
this._cd.detectChanges();
if (this._eventAssist) {
// queue updating node positions in the next frame
this._queue = QUEUE_WRITE_TO_NODES;
} else {
// update node positions right now
// ******** DOM WRITE ****************
writeToNodes(this._nodes, this._cells, this._records.length);
this._queue = null;
}
// ******** DOM WRITE ****************
writeToNodes(this._nodes, this._cells, this._records.length);
// ******** DOM WRITE ****************
this.setVirtualHeight(
estimateHeight(this._records.length, this._cells[this._cells.length - 1], this._vHeight, 0.25)
);
} else if (this._queue === QUEUE_WRITE_TO_NODES) {
// ******** DOM WRITE ****************
writeToNodes(this._nodes, this._cells, this._records.length);
this._queue = null;
} else {
data.scrollDiff = (data.scrollTop - this._lastCheck);
if (Math.abs(data.scrollDiff) > 10) {
if (Math.abs(data.scrollDiff) > SCROLL_DIFFERENCE_MINIMUM) {
// don't bother updating if the scrollTop hasn't changed much
this._lastCheck = data.scrollTop;
@ -531,7 +499,7 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
let stopAtHeight = (data.scrollTop + data.renderHeight);
processRecords(stopAtHeight, this._records, this._cells,
this._hdrFn, this._ftrFn, data);
this._hdrFn, this._ftrFn, data);
}
// ******** DOM READ ****************
@ -570,7 +538,7 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
*/
onScrollEnd() {
// scrolling is done, allow images to be updated now
this._imgs.toArray().forEach(img => {
this._imgs.forEach(img => {
img.enable(true);
});
@ -580,13 +548,14 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
adjustRendered(this._cells, this._data);
// ******** DOM WRITE ****************
this.detectChanges();
this._cd.detectChanges();
// ******** DOM WRITE ****************
this.setVirtualHeight(
estimateHeight(this._records.length, this._cells[this._cells.length - 1], this._vHeight, 0.05)
);
}
/**
* @private
* DOM WRITE
@ -595,6 +564,7 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
if (newVirtualHeight !== this._vHeight) {
// ******** DOM WRITE ****************
this._renderer.setElementStyle(this._elementRef.nativeElement, 'height', newVirtualHeight > 0 ? newVirtualHeight + 'px' : '');
this._vHeight = newVirtualHeight;
console.debug('VirtualScroll, height', newVirtualHeight);
}
@ -646,6 +616,5 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
}
const SCROLL_END_TIMEOUT_MS = 140;
const SCROLL_DIFFERENCE_MINIMUM = 20;
const QUEUE_CHANGE_DETECTION = 0;
const QUEUE_WRITE_TO_NODES = 1;

View File

@ -140,6 +140,7 @@ export function populateNodeData(startCellIndex: number, endCellIndex: number, v
let lastRecordIndex = (records.length - 1);
let viewInsertIndex: number = null;
let totalNodes = nodes.length;
let templateRef: TemplateRef<any>;
startCellIndex = Math.max(startCellIndex, 0);
endCellIndex = Math.min(endCellIndex, cells.length - 1);
@ -216,12 +217,17 @@ export function populateNodeData(startCellIndex: number, endCellIndex: number, v
}
}
// select which templateRef should be used for this cell
templateRef = cell.tmpl === TEMPLATE_HEADER ? hdrTmp : cell.tmpl === TEMPLATE_FOOTER ? ftrTmp : itmTmp;
if (!templateRef) {
console.error(`virtual${cell.tmpl === TEMPLATE_HEADER ? 'Header' : cell.tmpl === TEMPLATE_FOOTER ? 'Footer' : 'Item'} template required`);
continue;
}
availableNode = {
tmpl: cell.tmpl,
view: <EmbeddedViewRef<VirtualContext>>viewContainer.createEmbeddedView(
cell.tmpl === TEMPLATE_HEADER ? hdrTmp :
cell.tmpl === TEMPLATE_FOOTER ? ftrTmp :
itmTmp,
templateRef,
new VirtualContext(null, null, null),
viewInsertIndex
)
@ -246,9 +252,10 @@ export function populateNodeData(startCellIndex: number, endCellIndex: number, v
if (initialLoad) {
// add nodes that go at the very end, and only represent the last record
addLastNodes(nodes, viewContainer, TEMPLATE_HEADER, hdrTmp);
addLastNodes(nodes, viewContainer, TEMPLATE_ITEM, itmTmp);
addLastNodes(nodes, viewContainer, TEMPLATE_FOOTER, ftrTmp);
let lastNodeTempData: any = (records[lastRecordIndex] || {});
addLastNodes(nodes, viewContainer, TEMPLATE_HEADER, hdrTmp, lastNodeTempData);
addLastNodes(nodes, viewContainer, TEMPLATE_ITEM, itmTmp, lastNodeTempData);
addLastNodes(nodes, viewContainer, TEMPLATE_FOOTER, ftrTmp, lastNodeTempData);
}
return madeChanges;
@ -256,7 +263,7 @@ export function populateNodeData(startCellIndex: number, endCellIndex: number, v
function addLastNodes(nodes: VirtualNode[], viewContainer: ViewContainerRef,
templateType: number, templateRef: TemplateRef<Object>) {
templateType: number, templateRef: TemplateRef<Object>, temporaryData: any) {
if (templateRef) {
let node: VirtualNode = {
tmpl: templateType,
@ -264,7 +271,7 @@ function addLastNodes(nodes: VirtualNode[], viewContainer: ViewContainerRef,
isLastRecord: true,
hidden: true,
};
node.view.context.$implicit = {};
node.view.context.$implicit = temporaryData;
nodes.push(node);
}
}
@ -409,40 +416,36 @@ export function writeToNodes(nodes: VirtualNode[], cells: VirtualCell[], totalRe
for (var i = 0, ilen = nodes.length; i < ilen; i++) {
node = nodes[i];
if (node.hidden) {
continue;
}
if (!node.hidden) {
cell = cells[node.cell];
cell = cells[node.cell];
transform = `translate3d(${cell.left}px,${cell.top}px,0px)`;
transform = `translate3d(${cell.left}px,${cell.top}px,0px)`;
if (node.lastTransform !== transform) {
element = getElement(node);
if (node.lastTransform === transform) {
continue;
}
if (element) {
// ******** DOM WRITE ****************
element.style[CSS.transform] = node.lastTransform = transform;
element = getElement(node);
// ******** DOM WRITE ****************
element.classList.add('virtual-position');
if (element) {
// ******** DOM WRITE ****************
element.style[CSS.transform] = node.lastTransform = transform;
if (node.isLastRecord) {
// its the last record, now with data and safe to show
// ******** DOM WRITE ****************
element.classList.remove('virtual-hidden');
}
// ******** DOM WRITE ****************
element.classList.add('virtual-position');
// https://www.w3.org/TR/wai-aria/states_and_properties#aria-posinset
// ******** DOM WRITE ****************
element.setAttribute('aria-posinset', (node.cell + 1).toString());
if (node.isLastRecord) {
// its the last record, now with data and safe to show
// ******** DOM WRITE ****************
element.classList.remove('virtual-hidden');
// https://www.w3.org/TR/wai-aria/states_and_properties#aria-setsize
// ******** DOM WRITE ****************
element.setAttribute('aria-setsize', totalCells);
}
}
// https://www.w3.org/TR/wai-aria/states_and_properties#aria-posinset
// ******** DOM WRITE ****************
element.setAttribute('aria-posinset', (node.cell + 1).toString());
// https://www.w3.org/TR/wai-aria/states_and_properties#aria-setsize
// ******** DOM WRITE ****************
element.setAttribute('aria-setsize', totalCells);
}
}
}