@bestyled/contrib-flatlist
Version:
Implementation of FlatList for the BeStyled Design System
220 lines (219 loc) • 8.65 kB
JavaScript
/* Forked from react-virtualized and react-tiny-virtual-list 💖 */
export var ALIGNMENT;
(function (ALIGNMENT) {
ALIGNMENT["AUTO"] = "auto";
ALIGNMENT["START"] = "start";
ALIGNMENT["CENTER"] = "center";
ALIGNMENT["END"] = "end";
})(ALIGNMENT || (ALIGNMENT = {}));
export default class SizeAndPositionManager {
constructor({ itemCount, estimatedItemSize }) {
this.itemCount = itemCount;
this.estimatedItemSize = estimatedItemSize;
// Cache of size and position data for items, mapped by item index.
this.itemSizeAndPositionData = {};
// Offsets for items up to this index can be trusted; items afterward should be estimated.
this.lastMeasuredIndex = -1;
}
resize({ itemCount, estimatedItemSize }) {
if (itemCount != null) {
this.itemCount = itemCount;
this.lastMeasuredIndex = Math.min(this.lastMeasuredIndex, this.itemCount, itemCount);
}
if (estimatedItemSize != null) {
this.estimatedItemSize = estimatedItemSize;
}
}
getSizeForIndex(index) {
const cachedItem = this.itemSizeAndPositionData[index];
return cachedItem ? cachedItem.size : this.estimatedItemSize;
}
/**
* This method returns the size and position for the item at the specified index.
* It calculates (or used cached values) for items leading up to the index.
*/
getSizeAndPositionForIndex(index) {
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.getSizeForIndex(i);
this.itemSizeAndPositionData[i] = {
offset,
size
};
offset += size;
}
this.lastMeasuredIndex = index;
}
return this.itemSizeAndPositionData[index];
}
setItem(index, size) {
const cachedItem = this.itemSizeAndPositionData[index];
if (cachedItem) {
if (cachedItem.size !== size) {
this.itemSizeAndPositionData[index] = {
offset: cachedItem.offset,
size
};
this.lastMeasuredIndex = Math.min(index, this.lastMeasuredIndex);
}
else {
return false;
}
}
else {
this.itemSizeAndPositionData[index] = {
offset: null,
size
};
if (index)
this.lastMeasuredIndex = Math.min(index - 1, this.lastMeasuredIndex);
}
return true;
}
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() {
const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();
/* TODO consider whether to measure to end */
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 }) {
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 }) {
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);
let new_offset = datum.offset + datum.size;
let stop = start;
while (new_offset < maxOffset && stop < this.itemCount - 1) {
stop++;
new_offset += this.getSizeAndPositionForIndex(stop).size;
}
if (overscanCount) {
start = Math.max(0, start - overscanCount);
stop = Math.min(stop + overscanCount, this.itemCount - 1);
}
const paddingTop = this.getSizeAndPositionForIndex(start).offset;
return {
start,
stop,
paddingTop
};
}
/**
* 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) {
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
});
}
// 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
});
}
binarySearch({ low, high, offset }) {
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;
}
if (currentOffset < offset) {
low = middle + 1;
}
else if (currentOffset > offset) {
high = middle - 1;
}
}
if (low > 0) {
return low - 1;
}
return 0;
}
exponentialSearch({ index, offset }) {
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
});
}
}