UNPKG

@shopify/flash-list

Version:

FlashList is a more performant FlatList replacement

257 lines (232 loc) 7.41 kB
import { LayoutParams, RVDimension, RVLayout, RVLayoutInfo, RVLayoutManager, } from "./LayoutManager"; /** * GridLayoutManager implementation that arranges items in a grid pattern. * Items are placed in rows and columns, with support for items spanning multiple columns. */ export class RVGridLayoutManagerImpl extends RVLayoutManager { /** The width of the bounded area for the grid */ private boundedSize: number; /** If there's a span change for grid layout, we need to recompute all the widths */ private fullRelayoutRequired = false; constructor(params: LayoutParams, previousLayoutManager?: RVLayoutManager) { super(params, previousLayoutManager); this.boundedSize = params.windowSize.width; } /** * Updates layout parameters and triggers recomputation if necessary. * @param params New layout parameters */ updateLayoutParams(params: LayoutParams): void { const prevNumColumns = this.maxColumns; super.updateLayoutParams(params); if ( this.boundedSize !== params.windowSize.width || prevNumColumns !== params.maxColumns ) { this.boundedSize = params.windowSize.width; if (this.layouts.length > 0) { // update all widths this.updateAllWidths(); this.recomputeLayouts(0, this.layouts.length - 1); this.requiresRepaint = true; } } } /** * Processes layout information for items, updating their dimensions. * @param layoutInfo Array of layout information for items * @param itemCount Total number of items in the list */ processLayoutInfo(layoutInfo: RVLayoutInfo[], itemCount: number) { for (const info of layoutInfo) { const { index, dimensions } = info; const layout = this.layouts[index]; layout.height = dimensions.height; layout.isHeightMeasured = true; layout.isWidthMeasured = true; } // TODO: Can be optimized if (this.fullRelayoutRequired) { this.updateAllWidths(); this.fullRelayoutRequired = false; return 0; } } /** * Estimates layout dimensions for an item at the given index. * @param index Index of the item to estimate layout for */ estimateLayout(index: number) { const layout = this.layouts[index]; layout.width = this.getWidth(index); layout.height = this.getEstimatedHeight(index); layout.isWidthMeasured = true; layout.enforcedWidth = true; } /** * Handles span change for an item. * @param index Index of the item */ handleSpanChange(index: number) { this.fullRelayoutRequired = true; } /** * Returns the total size of the layout area. * @returns RVDimension containing width and height of the layout */ getLayoutSize(): RVDimension { if (this.layouts.length === 0) return { width: 0, height: 0 }; const totalHeight = this.computeTotalHeightTillRow(this.layouts.length - 1); return { width: this.boundedSize, height: totalHeight, }; } /** * Recomputes layouts for items in the given range. * @param startIndex Starting index of items to recompute * @param endIndex Ending index of items to recompute */ recomputeLayouts(startIndex: number, endIndex: number): void { const newStartIndex = this.locateFirstIndexInRow( Math.max(0, startIndex - 1) ); const startVal = this.getLayout(newStartIndex); let startX = startVal.x; let startY = startVal.y; for (let i = newStartIndex; i <= endIndex; i++) { const layout = this.getLayout(i); if (!this.checkBounds(startX, layout.width)) { const tallestItem = this.processAndReturnTallestItemInRow(i - 1); startY = tallestItem.y + tallestItem.height; startX = 0; } layout.x = startX; layout.y = startY; startX += layout.width; } if (endIndex === this.layouts.length - 1) { this.processAndReturnTallestItemInRow(endIndex); } } /** * Calculates the width of an item based on its span. * @param index Index of the item * @returns Width of the item */ private getWidth(index: number): number { return (this.boundedSize / this.maxColumns) * this.getSpan(index); } /** * Processes items in a row and returns the tallest item. * Also handles height normalization for items in the same row. * Tallest item per row helps in forcing tallest items height on neighbouring items. * @param endIndex Index of the last item in the row * @returns The tallest item in the row */ private processAndReturnTallestItemInRow(endIndex: number): RVLayout { const startIndex = this.locateFirstIndexInRow(endIndex); let tallestItem: RVLayout | undefined; let maxHeight = 0; let i = startIndex; let isMeasured = false; while (i <= endIndex) { const layout = this.layouts[i]; isMeasured = isMeasured || Boolean(layout.isHeightMeasured); maxHeight = Math.max(maxHeight, layout.height); if ( layout.height > (layout.minHeight ?? 0) && layout.height > (tallestItem?.height ?? 0) ) { tallestItem = layout; } i++; if (i >= this.layouts.length) { break; } } if (!tallestItem && maxHeight > 0) { maxHeight = Number.MAX_SAFE_INTEGER; } tallestItem = tallestItem ?? this.layouts[startIndex]; if (!isMeasured) { return tallestItem; } if (tallestItem) { let targetHeight = tallestItem.height; if (maxHeight - tallestItem.height > 1) { targetHeight = 0; this.requiresRepaint = true; } i = startIndex; while (i <= endIndex) { this.layouts[i].minHeight = targetHeight; if (targetHeight > 0) { this.layouts[i].height = targetHeight; } i++; if (i >= this.layouts.length) { break; } } tallestItem.minHeight = 0; } return tallestItem; } /** * Computes the total height of the layout. * @param endIndex Index of the last item in the row * @returns Total height of the layout */ private computeTotalHeightTillRow(endIndex: number): number { const startIndex = this.locateFirstIndexInRow(endIndex); const y = this.layouts[startIndex].y; let maxHeight = 0; let i = startIndex; while (i <= endIndex) { maxHeight = Math.max(maxHeight, this.layouts[i].height); i++; if (i >= this.layouts.length) { break; } } return y + maxHeight; } private updateAllWidths() { for (let i = 0; i < this.layouts.length; i++) { this.layouts[i].width = this.getWidth(i); } } /** * Checks if an item can fit within the bounded width. * @param itemX Starting X position of the item * @param width Width of the item * @returns True if the item fits within bounds */ private checkBounds(itemX: number, width: number): boolean { return itemX + width <= this.boundedSize + 0.9; } /** * Locates the index of the first item in the current row. * @param itemIndex Index to start searching from * @returns Index of the first item in the row */ private locateFirstIndexInRow(itemIndex: number): number { if (itemIndex === 0) { return 0; } let i = itemIndex; for (; i >= 0; i--) { if (this.layouts[i].x === 0) { break; } } return Math.max(i, 0); } }