Skip to content

Commit

Permalink
variable item height (dinony#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
buu700 committed Nov 30, 2017
1 parent 3447aac commit 50fa71b
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 20 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>|((item: any, i: number) => number|Promise<number>);
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.

Expand Down
10 changes: 7 additions & 3 deletions src/basic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export type ItemHeightFunction = (item: any, i: number) => number|Promise<number>;

export interface IVirtualScrollOptions {
itemWidth?: number;
itemHeight: number;
itemHeight: number|Promise<number>|ItemHeightFunction;
numAdditionalRows?: number;
numLimitColumns?: number;
}
Expand All @@ -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;
Expand All @@ -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;
Expand Down
40 changes: 32 additions & 8 deletions src/measurement.ts
Original file line number Diff line number Diff line change
@@ -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<IVirtualScrollMeasurement> {
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) => (<ItemHeightFunction> 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,
Expand All @@ -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,
Expand All @@ -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
};
}

Expand Down
15 changes: 8 additions & 7 deletions src/virtualScroll.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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$)
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -339,15 +340,15 @@ 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$
.filter(cmd => cmd.cmdType === UserCmdOption.FocusItem)
.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$)
Expand Down

0 comments on commit 50fa71b

Please sign in to comment.