From 50fa71b5212207fa21c3ebe88260891bf436439c Mon Sep 17 00:00:00 2001 From: Ryan Lester Date: Thu, 30 Nov 2017 03:45:32 -0500 Subject: [PATCH] variable item height (#1) --- README.md | 4 ++-- src/basic.ts | 10 ++++++--- src/measurement.ts | 40 +++++++++++++++++++++++++++------- src/virtualScroll.component.ts | 15 +++++++------ 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a986e0d..ca83945 100644 --- a/README.md +++ b/README.md @@ -110,13 +110,13 @@ If you want to apply a traditional layout and wonder about the space between inl ```typescript export interface IVirtualScrollOptions { itemWidth?: number; - itemHeight: number; + itemHeight: number|Promise|((item: any, i: number) => number|Promise); numAdditionalRows?: number; numLimitColumns?: number; } ``` -The component requires either fixed-size cells (itemWidth, itemHeight) or a fixed number of cells per row (itemHeight, numLimitColumns). +The component requires either fixed-size cells (itemWidth, itemHeight) or a fixed number of cells per row (itemHeight, numLimitColumns). When using a function for itemHeight, numLimitColumns must be set to exactly 1. Further, to improve scrolling, additional rows may be requested. diff --git a/src/basic.ts b/src/basic.ts index c1e4282..eadfda5 100644 --- a/src/basic.ts +++ b/src/basic.ts @@ -1,6 +1,8 @@ +export type ItemHeightFunction = (item: any, i: number) => number|Promise; + export interface IVirtualScrollOptions { itemWidth?: number; - itemHeight: number; + itemHeight: number|Promise|ItemHeightFunction; numAdditionalRows?: number; numLimitColumns?: number; } @@ -14,7 +16,8 @@ export interface IVirtualScrollMeasurement { containerWidth: number; containerHeight: number; itemWidth?: number; - itemHeight: number; + itemHeight: number|number[]; + minItemHeight: number; numPossibleRows: number; numPossibleColumns: number; numPossibleItems: number; @@ -25,11 +28,12 @@ export interface IVirtualScrollWindow { containerWidth: number; containerHeight: number; itemWidth?: number; - itemHeight: number; + itemHeight: number|number[]; numVirtualItems: number; numVirtualRows: number; virtualHeight: number; numAdditionalRows: number; + rowShifts?: number[]; scrollTop: number; scrollPercentage: number; numActualRows: number; diff --git a/src/measurement.ts b/src/measurement.ts index b58f154..fa4766e 100644 --- a/src/measurement.ts +++ b/src/measurement.ts @@ -1,14 +1,23 @@ -import {IVirtualScrollContainer, IVirtualScrollMeasurement, IVirtualScrollOptions, IVirtualScrollWindow} from './basic'; +import {ItemHeightFunction, IVirtualScrollContainer, IVirtualScrollMeasurement, IVirtualScrollOptions, IVirtualScrollWindow} from './basic'; -export function calcMeasure(rect: IVirtualScrollContainer, options: IVirtualScrollOptions): IVirtualScrollMeasurement { - const numPossibleRows = Math.ceil(rect.height / options.itemHeight); +export async function calcMeasure(data: any[], rect: IVirtualScrollContainer, options: IVirtualScrollOptions): Promise { + if (typeof options.itemHeight === 'function' && options.numLimitColumns !== 1) { + throw new Error('numLimitColumns must equal 1 when using variable item height.'); + } + + const itemHeight = typeof options.itemHeight === 'number' ? options.itemHeight : await (typeof options.itemHeight !== 'function' ? options.itemHeight : Promise.all(data.map(async (item, i) => ( options.itemHeight)(item, i)))); + + const minItemHeight = typeof itemHeight === 'number' ? itemHeight : itemHeight.reduce((a, b) => Math.min(a, b)); + + const numPossibleRows = Math.ceil(rect.height / minItemHeight); const numPossibleColumns = options.itemWidth !== undefined ? Math.floor(rect.width / options.itemWidth) : 0; return { containerHeight: rect.height, containerWidth: rect.width, - itemHeight: options.itemHeight, + itemHeight, itemWidth: options.itemWidth, + minItemHeight, numPossibleColumns, numPossibleItems: numPossibleRows * numPossibleColumns, numPossibleRows, @@ -24,14 +33,28 @@ export function calcScrollWindow(scrollTop: number, measure: IVirtualScrollMeasu const numActualColumns = Math.min(numVirtualItems, requestedColumns); const numVirtualRows = Math.ceil(numVirtualItems / Math.max(1, numActualColumns)); - const virtualHeight = numVirtualRows * measure.itemHeight; + const virtualHeight = typeof measure.itemHeight === 'number' ? numVirtualRows * measure.itemHeight : measure.itemHeight.reduce((a, b) => a + b); const numAdditionalRows = options.numAdditionalRows !== undefined ? options.numAdditionalRows : 1; const requestedRows = measure.numPossibleRows + numAdditionalRows; const numActualRows = numActualColumns > 0 ? Math.min(requestedRows, numVirtualRows) : 0; - const actualHeight = numActualRows * measure.itemHeight; + const visibleStartRow = typeof measure.itemHeight === 'number' ? undefined : measure.itemHeight.reduce( + (a, b, i) => { + if (a >= 0) { + return a; + } + + const sum = a + b; + return sum >= 0 ? i : sum; + }, + -scrollTop + ); + + const actualHeight = typeof measure.itemHeight === 'number' ? numActualRows * measure.itemHeight : typeof visibleStartRow === 'number' ? measure.itemHeight.slice(visibleStartRow).reduce((a, b) => a + b) : 0; + + const visibleEndRow = typeof measure.itemHeight === 'number' ? (numActualColumns > 0 && numActualRows > 0 ? clamp(0, numVirtualRows - 1, Math.floor((scrollTop + actualHeight) / measure.minItemHeight) - 1) : -1) : typeof visibleStartRow === 'number' ? (visibleStartRow + numActualRows - 1) : 0; - const visibleEndRow = numActualColumns > 0 && numActualRows > 0 ? clamp(0, numVirtualRows - 1, Math.floor((scrollTop + actualHeight) / measure.itemHeight) - 1) : -1; + const rowShifts = typeof measure.itemHeight === 'number' ? undefined : measure.itemHeight.reduce((arr, n) => arr.concat(arr[arr.length - 1] + n), [0]).slice(0, -1); return { actualHeight, @@ -46,11 +69,12 @@ export function calcScrollWindow(scrollTop: number, measure: IVirtualScrollMeasu numAdditionalRows, numVirtualItems, numVirtualRows, + rowShifts, scrollPercentage: clamp(0, 100, scrollTop / (virtualHeight - measure.containerHeight)), scrollTop, virtualHeight, visibleEndRow, - visibleStartRow: visibleEndRow !== -1 ? Math.max(0, visibleEndRow - numActualRows + 1) : -1 + visibleStartRow: typeof visibleStartRow === 'number' ? visibleStartRow : visibleEndRow !== -1 ? Math.max(0, visibleEndRow - numActualRows + 1) : -1 }; } diff --git a/src/virtualScroll.component.ts b/src/virtualScroll.component.ts index 23885d3..a722386 100644 --- a/src/virtualScroll.component.ts +++ b/src/virtualScroll.component.ts @@ -23,6 +23,7 @@ import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; import 'rxjs/add/operator/filter'; import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/pairwise'; import 'rxjs/add/operator/partition'; import 'rxjs/add/operator/publish'; @@ -90,7 +91,7 @@ export class VirtualScrollComponent implements OnInit, OnDestroy { private _elem: ElementRef, private _cdr: ChangeDetectorRef, private _componentFactoryResolver: ComponentFactoryResolver, private _obsService: ScrollObservableService) {} - ngOnInit() { + async ngOnInit() { const getContainerRect = () => this._elem.nativeElement.getBoundingClientRect(); const getScrollTop = () => this._elem.nativeElement.scrollTop; @@ -118,8 +119,8 @@ export class VirtualScrollComponent implements OnInit, OnDestroy { .map(() => getScrollTop()) .startWith(0); - const measure$ = Observable.combineLatest(rect$, options$) - .map(([rect, options]) => calcMeasure(rect, options)) + const measure$ = Observable.combineLatest(data$, rect$, options$) + .mergeMap(async ([data, rect, options]) => calcMeasure(data, rect, options)) .publish(); const scrollWin$ = Observable.combineLatest(scrollTop$, measure$, dataMeta$, options$) @@ -178,7 +179,7 @@ export class VirtualScrollComponent implements OnInit, OnDestroy { const rowIndex = parseInt(key, 10); const row = createRowsMap[key]; - createRowCmds.push(new CreateRowCmd(row, rowIndex, row * curWin.itemHeight)); + createRowCmds.push(new CreateRowCmd(row, rowIndex, curWin.rowShifts !== undefined ? curWin.rowShifts[row] : typeof curWin.itemHeight === 'number' ? row * curWin.itemHeight : 0)); forColumnsIn(0, curWin.numActualColumns - 1, row, curWin.numActualColumns, curWin.numVirtualItems, (c, dataIndex) => { createItemCmds.push(new CreateItemCmd(row, rowIndex, c, dataIndex)); @@ -207,7 +208,7 @@ export class VirtualScrollComponent implements OnInit, OnDestroy { const row = existingRows[key].right; if(row !== prevRow) { - shiftRowCmds.push(new ShiftRowCmd(row, rowIndex, row * curWin.itemHeight)); + shiftRowCmds.push(new ShiftRowCmd(row, rowIndex, curWin.rowShifts !== undefined ? curWin.rowShifts[row] : typeof curWin.itemHeight === 'number' ? row * curWin.itemHeight : 0)); } if(row !== prevRow || numColumns !== 0 || prevWin.numVirtualItems <= getMaxIndex(prevWin) || curWin.numVirtualItems <= getMaxIndex(curWin) || prevWin.dataTimestamp !== curWin.dataTimestamp) { @@ -339,7 +340,7 @@ export class VirtualScrollComponent implements OnInit, OnDestroy { .withLatestFrom(scrollWin$) .map(([cmd, scrollWin]) => { const focusRow = cmd as FocusRowCmd; - return new SetScrollTopCmd(focusRow.rowIndex * scrollWin.itemHeight); + return new SetScrollTopCmd(scrollWin.rowShifts !== undefined ? scrollWin.rowShifts[focusRow.rowIndex] : typeof scrollWin.itemHeight === 'number' ? (focusRow.rowIndex * scrollWin.itemHeight) : 0); }); const focusItemSetScrollTop$ = userCmd$ @@ -347,7 +348,7 @@ export class VirtualScrollComponent implements OnInit, OnDestroy { .withLatestFrom(scrollWin$) .map(([cmd, scrollWin]) => { const focusItem = cmd as FocusItemCmd; - return new SetScrollTopCmd(Math.floor(focusItem.itemIndex / scrollWin.numActualColumns) * scrollWin.itemHeight); + return new SetScrollTopCmd(scrollWin.rowShifts !== undefined ? scrollWin.rowShifts[focusItem.itemIndex] : typeof scrollWin.itemHeight === 'number' ? (Math.floor(focusItem.itemIndex / scrollWin.numActualColumns) * scrollWin.itemHeight) : 0); }); const setScrollTopFunc$ = Observable.merge(userSetScrollTop$, focusRowSetScrollTop$, focusItemSetScrollTop$)