UNPKG

@shopify/flash-list

Version:

FlashList is a more performant FlatList replacement

343 lines 15.4 kB
import { ErrorMessages } from "../errors/ErrorMessages"; import { WarningMessages } from "../errors/WarningMessages"; import ViewabilityManager from "./viewability/ViewabilityManager"; import { RVGridLayoutManagerImpl } from "./layout-managers/GridLayoutManager"; import { RVLinearLayoutManagerImpl } from "./layout-managers/LinearLayoutManager"; import { RVMasonryLayoutManagerImpl } from "./layout-managers/MasonryLayoutManager"; import { RVEngagedIndicesTrackerImpl, } from "./helpers/EngagedIndicesTracker"; import { RenderStackManager } from "./RenderStackManager"; // Abstracts layout manager, render stack manager and viewability manager and generates render stack (progressively on load) export class RecyclerViewManager { get animationOptimizationsEnabled() { return this._animationOptimizationsEnabled; } set animationOptimizationsEnabled(value) { this._animationOptimizationsEnabled = value; this.renderStackManager.disableRecycling = value; } get isOffsetProjectionEnabled() { return this.engagedIndicesTracker.enableOffsetProjection; } get isDisposed() { return this._isDisposed; } get numColumns() { var _a; return (_a = this.propsRef.numColumns) !== null && _a !== void 0 ? _a : 1; } constructor(props) { this.initialDrawBatchSize = 2; // Map of index to key this.isFirstLayoutComplete = false; this.hasRenderedProgressively = false; this.progressiveRenderCount = 0; this._isDisposed = false; this._isLayoutManagerDirty = false; this._animationOptimizationsEnabled = false; this.firstItemOffset = 0; this.ignoreScrollEvents = false; this.isFirstPaintOnUiComplete = false; // updates render stack based on the engaged indices which are sorted. Recycles unused keys. this.updateRenderStack = (engagedIndices) => { this.renderStackManager.sync(this.getDataKey, this.getItemType, engagedIndices, this.getDataLength()); }; this.getDataKey = this.getDataKey.bind(this); this.getItemType = this.getItemType.bind(this); this.overrideItemLayout = this.overrideItemLayout.bind(this); this.propsRef = props; this.engagedIndicesTracker = new RVEngagedIndicesTrackerImpl(); this.renderStackManager = new RenderStackManager(props.maxItemsInRecyclePool); this.itemViewabilityManager = new ViewabilityManager(this); this.checkPropsAndWarn(); } get props() { return this.propsRef; } setOffsetProjectionEnabled(value) { this.engagedIndicesTracker.enableOffsetProjection = value; } updateProps(props) { var _a, _b, _c; this.propsRef = props; this.engagedIndicesTracker.drawDistance = (_a = props.drawDistance) !== null && _a !== void 0 ? _a : this.engagedIndicesTracker.drawDistance; this.initialDrawBatchSize = (_c = (_b = this.propsRef.overrideProps) === null || _b === void 0 ? void 0 : _b.initialDrawBatchSize) !== null && _c !== void 0 ? _c : this.initialDrawBatchSize; } /** * Updates the scroll offset and returns the engaged indices if any * @param offset * @param velocity */ updateScrollOffset(offset, velocity) { if (this.layoutManager && !this._isDisposed) { const engagedIndices = this.engagedIndicesTracker.updateScrollOffset(offset - this.firstItemOffset, velocity, this.layoutManager); if (engagedIndices) { this.updateRenderStack(engagedIndices); return engagedIndices; } } return undefined; } updateAverageRenderTime(time) { this.engagedIndicesTracker.averageRenderTime = time; } getIsFirstLayoutComplete() { return this.isFirstLayoutComplete; } getLayout(index) { if (!this.layoutManager) { throw new Error(ErrorMessages.layoutManagerNotInitializedLayoutInfo); } return this.layoutManager.getLayout(index); } tryGetLayout(index) { if (this.layoutManager && index >= 0 && index < this.layoutManager.getLayoutCount()) { return this.layoutManager.getLayout(index); } return undefined; } // Doesn't include header / foot etc getChildContainerDimensions() { if (!this.layoutManager) { throw new Error(ErrorMessages.layoutManagerNotInitializedChildContainer); } return this.layoutManager.getLayoutSize(); } getRenderStack() { return this.renderStackManager.getRenderStack(); } getWindowSize() { if (!this.layoutManager) { throw new Error(ErrorMessages.layoutManagerNotInitializedWindowSize); } return this.layoutManager.getWindowsSize(); } // Includes first item offset correction getLastScrollOffset() { return this.engagedIndicesTracker.scrollOffset; } getMaxScrollOffset() { return Math.max(0, (this.propsRef.horizontal ? this.getChildContainerDimensions().width : this.getChildContainerDimensions().height) - (this.propsRef.horizontal ? this.getWindowSize().width : this.getWindowSize().height) + this.firstItemOffset); } // Doesn't include first item offset correction getAbsoluteLastScrollOffset() { return this.engagedIndicesTracker.scrollOffset + this.firstItemOffset; } setScrollDirection(scrollDirection) { this.engagedIndicesTracker.setScrollDirection(scrollDirection); } resetVelocityCompute() { this.engagedIndicesTracker.resetVelocityHistory(); } updateLayoutParams(windowSize, firstItemOffset) { var _a, _b; this.firstItemOffset = firstItemOffset; const LayoutManagerClass = this.getLayoutManagerClass(); if (this.layoutManager && Boolean((_a = this.layoutManager) === null || _a === void 0 ? void 0 : _a.isHorizontal()) !== Boolean(this.propsRef.horizontal)) { throw new Error(ErrorMessages.horizontalPropCannotBeToggled); } if (this._isLayoutManagerDirty) { this.layoutManager = undefined; this._isLayoutManagerDirty = false; } const layoutManagerParams = { windowSize, maxColumns: this.numColumns, horizontal: Boolean(this.propsRef.horizontal), optimizeItemArrangement: (_b = this.propsRef.optimizeItemArrangement) !== null && _b !== void 0 ? _b : true, overrideItemLayout: this.overrideItemLayout, getItemType: this.getItemType, }; if (!(this.layoutManager instanceof LayoutManagerClass)) { // console.log("-----> new LayoutManagerClass"); this.layoutManager = new LayoutManagerClass(layoutManagerParams, this.layoutManager); } else { this.layoutManager.updateLayoutParams(layoutManagerParams); } } hasLayout() { return this.layoutManager !== undefined; } computeVisibleIndices() { if (!this.layoutManager) { throw new Error(ErrorMessages.layoutManagerNotInitializedVisibleIndices); } return this.engagedIndicesTracker.computeVisibleIndices(this.layoutManager); } getEngagedIndices() { return this.engagedIndicesTracker.getEngagedIndices(); } modifyChildrenLayout(layoutInfo, dataLength) { var _a, _b; (_a = this.layoutManager) === null || _a === void 0 ? void 0 : _a.modifyLayout(layoutInfo, dataLength); if (dataLength === 0) { return false; } if ((_b = this.layoutManager) === null || _b === void 0 ? void 0 : _b.requiresRepaint) { // console.log("requiresRepaint triggered"); this.layoutManager.requiresRepaint = false; return true; } if (this.hasRenderedProgressively) { if (!this.isFirstPaintOnUiComplete) { return false; } return this.recomputeEngagedIndices() !== undefined; } else { this.renderProgressively(); } return !this.hasRenderedProgressively; } computeItemViewability() { // Using higher buffer for masonry to avoid missing items this.itemViewabilityManager.shouldListenToVisibleIndices && this.itemViewabilityManager.updateViewableItems(this.propsRef.masonry ? this.engagedIndicesTracker.getEngagedIndices().toArray() : this.computeVisibleIndices().toArray()); } recordInteraction() { this.itemViewabilityManager.recordInteraction(); } recomputeViewableItems() { this.itemViewabilityManager.recomputeViewableItems(); } processDataUpdate() { var _a, _b; if (this.hasLayout()) { this.modifyChildrenLayout([], (_b = (_a = this.propsRef.data) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0); if (this.hasRenderedProgressively && !this.recomputeEngagedIndices()) { // recomputeEngagedIndices will update the render stack if there are any changes in the engaged indices. // It's important to update render stack so that elements are assgined right keys incase items were deleted. this.updateRenderStack(this.engagedIndicesTracker.getEngagedIndices()); } } } recomputeEngagedIndices() { return this.updateScrollOffset(this.getAbsoluteLastScrollOffset()); } restoreIfNeeded() { if (this._isDisposed) { this._isDisposed = false; } } dispose() { this._isDisposed = true; this.itemViewabilityManager.dispose(); } markLayoutManagerDirty() { this._isLayoutManagerDirty = true; } getInitialScrollIndex() { var _a, _b; return ((_a = this.propsRef.initialScrollIndex) !== null && _a !== void 0 ? _a : (((_b = this.propsRef.maintainVisibleContentPosition) === null || _b === void 0 ? void 0 : _b.startRenderingFromBottom) ? this.getDataLength() - 1 : undefined)); } shouldMaintainVisibleContentPosition() { var _a; // Return true if maintainVisibleContentPosition is enabled and not horizontal return (!((_a = this.propsRef.maintainVisibleContentPosition) === null || _a === void 0 ? void 0 : _a.disabled) && !this.propsRef.horizontal); } getDataLength() { var _a, _b; return (_b = (_a = this.propsRef.data) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0; } hasStableDataKeys() { return Boolean(this.propsRef.keyExtractor); } getDataKey(index) { var _a, _b, _c; return ((_c = (_b = (_a = this.propsRef).keyExtractor) === null || _b === void 0 ? void 0 : _b.call(_a, this.propsRef.data[index], index)) !== null && _c !== void 0 ? _c : index.toString()); } getLayoutManagerClass() { // throw errors for incompatible props if (this.propsRef.masonry && this.propsRef.horizontal) { throw new Error(ErrorMessages.masonryAndHorizontalIncompatible); } if (this.numColumns > 1 && this.propsRef.horizontal) { throw new Error(ErrorMessages.numColumnsAndHorizontalIncompatible); } return this.propsRef.masonry ? RVMasonryLayoutManagerImpl : this.numColumns > 1 && !this.propsRef.horizontal ? RVGridLayoutManagerImpl : RVLinearLayoutManagerImpl; } applyInitialScrollAdjustment() { var _a; if (!this.layoutManager || this.getDataLength() === 0) { return; } const initialScrollIndex = this.getInitialScrollIndex(); const initialItemLayout = (_a = this.layoutManager) === null || _a === void 0 ? void 0 : _a.getLayout(initialScrollIndex !== null && initialScrollIndex !== void 0 ? initialScrollIndex : 0); const initialItemOffset = this.propsRef.horizontal ? initialItemLayout === null || initialItemLayout === void 0 ? void 0 : initialItemLayout.x : initialItemLayout === null || initialItemLayout === void 0 ? void 0 : initialItemLayout.y; if (initialScrollIndex !== undefined) { // console.log( // "initialItemOffset", // initialScrollIndex, // initialItemOffset, // this.firstItemOffset // ); this.layoutManager.recomputeLayouts(0, initialScrollIndex); this.engagedIndicesTracker.scrollOffset = initialItemOffset !== null && initialItemOffset !== void 0 ? initialItemOffset : 0 + this.firstItemOffset; } else { // console.log("initialItemOffset", initialItemOffset, this.firstItemOffset); this.engagedIndicesTracker.scrollOffset = (initialItemOffset !== null && initialItemOffset !== void 0 ? initialItemOffset : 0) - this.firstItemOffset; } } renderProgressively() { this.progressiveRenderCount++; const layoutManager = this.layoutManager; if (layoutManager) { this.applyInitialScrollAdjustment(); const visibleIndices = this.computeVisibleIndices(); // console.log("---------> visibleIndices", visibleIndices); this.hasRenderedProgressively = visibleIndices.every((index) => layoutManager.getLayout(index).isHeightMeasured && layoutManager.getLayout(index).isWidthMeasured); if (this.hasRenderedProgressively) { this.isFirstLayoutComplete = true; } const batchSize = this.numColumns * this.initialDrawBatchSize ** Math.ceil(this.progressiveRenderCount / 5); // If everything is measured then render stack will be in sync. The buffer items will get rendered in the next update // triggered by the useOnLoad hook. !this.hasRenderedProgressively && this.updateRenderStack( // pick first n indices from visible ones based on batch size visibleIndices.slice(0, Math.min(visibleIndices.length, this.getRenderStack().size + batchSize))); } } getItemType(index) { var _a, _b, _c; return ((_c = (_b = (_a = this.propsRef).getItemType) === null || _b === void 0 ? void 0 : _b.call(_a, this.propsRef.data[index], index)) !== null && _c !== void 0 ? _c : "default").toString(); } overrideItemLayout(index, layout) { var _a, _b; (_b = (_a = this.propsRef) === null || _a === void 0 ? void 0 : _a.overrideItemLayout) === null || _b === void 0 ? void 0 : _b.call(_a, layout, this.propsRef.data[index], index, this.numColumns, this.propsRef.extraData); } checkPropsAndWarn() { if (this.propsRef.onStartReached && !this.propsRef.keyExtractor) { console.warn(WarningMessages.keyExtractorNotDefinedForMVCP); } } } //# sourceMappingURL=RecyclerViewManager.js.map