UNPKG

@bestyled/contrib-flatlist

Version:

Implementation of FlatList for the BeStyled Design System

220 lines (219 loc) 8.65 kB
/* 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 }); } }