mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-21 13:01:01 +08:00
@ -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 {
|
||||
|
@ -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 '';
|
||||
}
|
@ -53,4 +53,6 @@ class E2EApp {
|
||||
root = E2EPage;
|
||||
}
|
||||
|
||||
ionicBootstrap(E2EApp);
|
||||
ionicBootstrap(E2EApp, null, {
|
||||
prodMode: true
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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'];
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
@ -270,7 +270,7 @@ describe('VirtualScroll', () => {
|
||||
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();
|
||||
|
@ -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 ****************
|
||||
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;
|
||||
|
||||
@ -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;
|
||||
|
@ -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,18 +416,12 @@ 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];
|
||||
|
||||
transform = `translate3d(${cell.left}px,${cell.top}px,0px)`;
|
||||
|
||||
if (node.lastTransform === transform) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.lastTransform !== transform) {
|
||||
element = getElement(node);
|
||||
|
||||
if (element) {
|
||||
@ -446,6 +447,8 @@ export function writeToNodes(nodes: VirtualNode[], cells: VirtualCell[], totalRe
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user