fastlion-amis
Version:
一种MIS页面生成工具
309 lines (257 loc) • 8.41 kB
text/typescript
/* Forked from react-virtualized 💖 */
import { ALIGNMENT } from './constants';
export type ItemSizeGetter = (index: number) => number;
export type ItemSize = number | number[] | ItemSizeGetter;
export interface SizeAndPosition {
size: number;
offset: number;
}
interface SizeAndPositionData {
[id: number]: SizeAndPosition;
}
export interface Options {
itemCount: number;
itemSizeGetter: ItemSizeGetter;
estimatedItemSize: number;
}
export default class SizeAndPositionManager {
private itemSizeGetter: ItemSizeGetter;
private itemCount: number;
private estimatedItemSize: number;
private lastMeasuredIndex: number;
private itemSizeAndPositionData: SizeAndPositionData;
constructor({ itemCount, itemSizeGetter, estimatedItemSize }: Options) {
this.itemSizeGetter = itemSizeGetter;
this.itemCount = itemCount;
this.estimatedItemSize = estimatedItemSize;
// Cache of size and position data for items, mapped by item index.
this.itemSizeAndPositionData = {};
// Measurements for items up to this index can be trusted; items afterward should be estimated.
this.lastMeasuredIndex = -1;
}
updateConfig({
itemCount,
itemSizeGetter,
estimatedItemSize
}: Partial<Options>) {
if (itemCount != null) {
this.itemCount = itemCount;
}
if (estimatedItemSize != null) {
this.estimatedItemSize = estimatedItemSize;
}
if (itemSizeGetter != null) {
this.itemSizeGetter = itemSizeGetter;
}
}
getLastMeasuredIndex() {
return this.lastMeasuredIndex;
}
/**
* This method returns the size and position for the item at the specified index.
* It just-in-time calculates (or used cached values) for items leading up to the index.
*/
getSizeAndPositionForIndex(index: number) {
if (index < 0 || index >= this.itemCount) {
throw Error(
`Requested index ${index} is outside of range 0..${this.itemCount}`
);
}
if (index > this.lastMeasuredIndex) {
const lastMeasuredSizeAndPosition =
this.getSizeAndPositionOfLastMeasuredItem();
let offset =
lastMeasuredSizeAndPosition.offset + lastMeasuredSizeAndPosition.size;
for (let i = this.lastMeasuredIndex + 1; i <= index; i++) {
const size = this.itemSizeGetter(i);
if (size == null || isNaN(size)) {
throw Error(`Invalid size returned for index ${i} of value ${size}`);
}
this.itemSizeAndPositionData[i] = {
offset,
size
};
offset += size;
}
this.lastMeasuredIndex = index;
}
return this.itemSizeAndPositionData[index];
}
getSizeAndPositionOfLastMeasuredItem() {
return this.lastMeasuredIndex >= 0
? this.itemSizeAndPositionData[this.lastMeasuredIndex]
: { offset: 0, size: 0 };
}
/**
* Total size of all items being measured.
* This value will be completedly estimated initially.
* As items as measured the estimate will be updated.
*/
getTotalSize(): number {
const lastMeasuredSizeAndPosition =
this.getSizeAndPositionOfLastMeasuredItem();
return (
lastMeasuredSizeAndPosition.offset +
lastMeasuredSizeAndPosition.size +
(this.itemCount - this.lastMeasuredIndex - 1) * this.estimatedItemSize
);
}
/**
* Determines a new offset that ensures a certain item is visible, given the alignment.
*
* @param align Desired alignment within container; one of "start" (default), "center", or "end"
* @param containerSize Size (width or height) of the container viewport
* @return Offset to use to ensure the specified item is visible
*/
getUpdatedOffsetForIndex({
align = ALIGNMENT.START,
containerSize,
currentOffset,
targetIndex
}: {
align: ALIGNMENT | undefined;
containerSize: number;
currentOffset: number;
targetIndex: number;
}): number {
if (containerSize <= 0) {
return 0;
}
const datum = this.getSizeAndPositionForIndex(targetIndex);
const maxOffset = datum.offset;
const minOffset = maxOffset - containerSize + datum.size;
let idealOffset;
switch (align) {
case ALIGNMENT.END:
idealOffset = minOffset;
break;
case ALIGNMENT.CENTER:
idealOffset = maxOffset - (containerSize - datum.size) / 2;
break;
case ALIGNMENT.START:
idealOffset = maxOffset;
break;
default:
idealOffset = Math.max(minOffset, Math.min(maxOffset, currentOffset));
}
const totalSize = this.getTotalSize();
return Math.max(0, Math.min(totalSize - containerSize, idealOffset));
}
getVisibleRange({
containerSize,
offset,
overscanCount
}: {
containerSize: number;
offset: number;
overscanCount: number;
}): { start?: number; stop?: number } {
const totalSize = this.getTotalSize();
if (totalSize === 0) {
return {};
}
const maxOffset = offset + containerSize;
let start = this.findNearestItem(offset);
if (typeof start === 'undefined') {
throw Error(`Invalid offset ${offset} specified`);
}
const datum = this.getSizeAndPositionForIndex(start);
offset = datum.offset + datum.size;
let stop = start;
while (offset < maxOffset && stop < this.itemCount - 1) {
stop++;
offset += this.getSizeAndPositionForIndex(stop).size;
}
if (overscanCount) {
start = Math.max(0, start - overscanCount);
stop = Math.min(stop + overscanCount, this.itemCount - 1);
}
return {
start,
stop
};
}
/**
* Clear all cached values for items after the specified index.
* This method should be called for any item that has changed its size.
* It will not immediately perform any calculations; they'll be performed the next time getSizeAndPositionForIndex() is called.
*/
resetItem(index: number) {
this.lastMeasuredIndex = Math.min(this.lastMeasuredIndex, index - 1);
}
/**
* Searches for the item (index) nearest the specified offset.
*
* If no exact match is found the next lowest item index will be returned.
* This allows partially visible items (with offsets just before/above the fold) to be visible.
*/
findNearestItem(offset: number) {
if (isNaN(offset)) {
throw Error(`Invalid offset ${offset} specified`);
}
// Our search algorithms find the nearest match at or below the specified offset.
// So make sure the offset is at least 0 or no match will be found.
offset = Math.max(0, offset);
const lastMeasuredSizeAndPosition =
this.getSizeAndPositionOfLastMeasuredItem();
const lastMeasuredIndex = Math.max(0, this.lastMeasuredIndex);
if (lastMeasuredSizeAndPosition.offset >= offset) {
// If we've already measured items within this range just use a binary search as it's faster.
return this.binarySearch({
high: lastMeasuredIndex,
low: 0,
offset
});
} else {
// If we haven't yet measured this high, fallback to an exponential search with an inner binary search.
// The exponential search avoids pre-computing sizes for the full set of items as a binary search would.
// The overall complexity for this approach is O(log n).
return this.exponentialSearch({
index: lastMeasuredIndex,
offset
});
}
}
private binarySearch({
low,
high,
offset
}: {
low: number;
high: number;
offset: number;
}) {
let middle = 0;
let currentOffset = 0;
while (low <= high) {
middle = low + Math.floor((high - low) / 2);
currentOffset = this.getSizeAndPositionForIndex(middle).offset;
if (currentOffset === offset) {
return middle;
} else if (currentOffset < offset) {
low = middle + 1;
} else if (currentOffset > offset) {
high = middle - 1;
}
}
if (low > 0) {
return low - 1;
}
return 0;
}
private exponentialSearch({ index, offset }: { index: number; offset: number }) {
let interval = 1;
while (
index < this.itemCount &&
this.getSizeAndPositionForIndex(index).offset < offset
) {
index += interval;
interval *= 2;
}
return this.binarySearch({
high: Math.min(index, this.itemCount - 1),
low: Math.floor(index / 2),
offset
});
}
}