UNPKG

@tamagui/react-native-web-lite

Version:
1,017 lines (1,016 loc) 44.7 kB
import { invariant, StyleSheet } from "@tamagui/react-native-web-internals"; import ViewabilityHelper from "../ViewabilityHelper"; import { CellRenderMask } from "./CellRenderMask"; import StateSafePureComponent from "./StateSafePureComponent"; import { VirtualizedListCellContextProvider, VirtualizedListContext, VirtualizedListContextProvider } from "./VirtualizedListContext"; import ScrollView from "../../../ScrollView/ScrollViewBase"; import View from "../../../View"; import CellRenderer from "./VirtualizedListCellRenderer"; import FillRateHelper from "../FillRateHelper"; import ChildListCollection from "./ChildListCollection"; import React from "react"; import clamp from "../Utilities/clamp"; import { keyExtractor as defaultKeyExtractor } from "../VirtualizeUtils"; import Batchinator from "../../../Batchinator"; import RefreshControl from "../../../RefreshControl"; import { jsx, jsxs } from "react/jsx-runtime"; const __DEV__ = process.env.NODE_ENV !== "production", ON_EDGE_REACHED_EPSILON = 1e-3; let _usedIndexForKey = !1, _keylessItemComponentName = ""; function horizontalOrDefault(horizontal) { return horizontal ?? !1; } function initialNumToRenderOrDefault(initialNumToRender) { return initialNumToRender ?? 10; } function maxToRenderPerBatchOrDefault(maxToRenderPerBatch) { return maxToRenderPerBatch ?? 10; } function onStartReachedThresholdOrDefault(onStartReachedThreshold) { return onStartReachedThreshold ?? 2; } function onEndReachedThresholdOrDefault(onEndReachedThreshold) { return onEndReachedThreshold ?? 2; } function getScrollingThreshold(threshold, visibleLength) { return threshold * visibleLength / 2; } function scrollEventThrottleOrDefault(scrollEventThrottle) { return scrollEventThrottle ?? 50; } function windowSizeOrDefault(windowSize) { return windowSize ?? 21; } function findLastWhere(arr, predicate) { for (let i = arr.length - 1; i >= 0; i--) if (predicate(arr[i])) return arr[i]; return null; } class VirtualizedList extends StateSafePureComponent { static contextType = VirtualizedListContext; // scrollToEnd may be janky without getItemLayout prop scrollToEnd(params) { const animated = params ? params.animated : !0, veryLast = this.props.getItemCount(this.props.data) - 1; if (veryLast < 0) return; const frame = this.__getFrameMetricsApprox(veryLast, this.props), offset = Math.max( 0, frame.offset + frame.length + this._footerLength - this._scrollMetrics.visibleLength ); if (this._scrollRef != null) { if (this._scrollRef.scrollTo == null) { console.warn( "No scrollTo method provided. This may be because you have two nested VirtualizedLists with the same orientation, or because you are using a custom component that does not implement scrollTo." ); return; } this._scrollRef.scrollTo( horizontalOrDefault(this.props.horizontal) ? { x: offset, animated } : { y: offset, animated } ); } } // scrollToIndex may be janky without getItemLayout prop scrollToIndex(params) { const { data, horizontal, getItemCount, getItemLayout, onScrollToIndexFailed } = this.props, { animated, index, viewOffset, viewPosition } = params; if (invariant( index >= 0, `scrollToIndex out of range: requested index ${index} but minimum is 0` ), invariant( getItemCount(data) >= 1, `scrollToIndex out of range: item length ${getItemCount(data)} but minimum is 1` ), invariant( index < getItemCount(data), `scrollToIndex out of range: requested index ${index} is out of 0 to ${getItemCount(data) - 1}` ), !getItemLayout && index > this._highestMeasuredFrameIndex) { invariant( !!onScrollToIndexFailed, "scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, otherwise there is no way to know the location of offscreen indices or handle failures." ), onScrollToIndexFailed({ averageItemLength: this._averageCellLength, highestMeasuredFrameIndex: this._highestMeasuredFrameIndex, index }); return; } const frame = this.__getFrameMetricsApprox(Math.floor(index), this.props), offset = Math.max( 0, // @ts-ignore this._getOffsetApprox(index, this.props) - (viewPosition || 0) * (this._scrollMetrics.visibleLength - frame.length) ) - (viewOffset || 0); if (this._scrollRef != null) { if (this._scrollRef.scrollTo == null) { console.warn( "No scrollTo method provided. This may be because you have two nested VirtualizedLists with the same orientation, or because you are using a custom component that does not implement scrollTo." ); return; } this._scrollRef.scrollTo( horizontal ? { x: offset, animated } : { y: offset, animated } ); } } // scrollToItem may be janky without getItemLayout prop. Required linear scan through items - // use scrollToIndex instead if possible. scrollToItem(params) { const { item } = params, { data, getItem, getItemCount } = this.props, itemCount = getItemCount(data); for (let index = 0; index < itemCount; index++) if (getItem(data, index) === item) { this.scrollToIndex({ ...params, index }); break; } } /** * Scroll to a specific content pixel offset in the list. * * Param `offset` expects the offset to scroll to. * In case of `horizontal` is true, the offset is the x-value, * in any other case the offset is the y-value. * * Param `animated` (`true` by default) defines whether the list * should do an animation while scrolling. */ scrollToOffset(params) { const { animated, offset } = params; if (this._scrollRef != null) { if (this._scrollRef.scrollTo == null) { console.warn( "No scrollTo method provided. This may be because you have two nested VirtualizedLists with the same orientation, or because you are using a custom component that does not implement scrollTo." ); return; } this._scrollRef.scrollTo( horizontalOrDefault(this.props.horizontal) ? { x: offset, animated } : { y: offset, animated } ); } } recordInteraction() { this._nestedChildLists.forEach((childList) => { childList.recordInteraction(); }), this._viewabilityTuples.forEach((t) => { t.viewabilityHelper.recordInteraction(); }), this._updateViewableItems(this.props, this.state.cellsAroundViewport); } flashScrollIndicators() { this._scrollRef != null && this._scrollRef.flashScrollIndicators(); } /** * Provides a handle to the underlying scroll responder. * Note that `this._scrollRef` might not be a `ScrollView`, so we * need to check that it responds to `getScrollResponder` before calling it. */ getScrollResponder() { return this._scrollRef && this._scrollRef.getScrollResponder ? this._scrollRef.getScrollResponder() : null; } getScrollableNode() { return this._scrollRef && this._scrollRef.getScrollableNode ? this._scrollRef.getScrollableNode() : this._scrollRef; } getScrollRef() { return this._scrollRef && this._scrollRef.getScrollRef ? this._scrollRef.getScrollRef() : this._scrollRef; } _getCellKey() { return this.context?.cellKey || "rootList"; } _getScrollMetrics = () => this._scrollMetrics; hasMore() { return this._hasMore; } _getOutermostParentListRef = () => this._isNestedWithSameOrientation() ? this.context.getOutermostParentListRef() : this; _registerAsNestedChild = (childList) => { this._nestedChildLists.add(childList.ref, childList.cellKey), this._hasInteracted && childList.ref.recordInteraction(); }; _unregisterAsNestedChild = (childList) => { this._nestedChildLists.remove(childList.ref); }; invertedWheelEventHandler; constructor(props) { if (super(props), this._checkProps(props), this._fillRateHelper = new FillRateHelper(this._getFrameMetrics), this._updateCellsToRenderBatcher = new Batchinator( this._updateCellsToRender, this.props.updateCellsBatchingPeriod ?? 50 ), this.props.viewabilityConfigCallbackPairs) this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map((pair) => ({ viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig), onViewableItemsChanged: pair.onViewableItemsChanged })); else { const { onViewableItemsChanged, viewabilityConfig } = this.props; onViewableItemsChanged && this._viewabilityTuples.push({ viewabilityHelper: new ViewabilityHelper(viewabilityConfig), onViewableItemsChanged }); } const initialRenderRegion = VirtualizedList._initialRenderRegion(props); this.state = { cellsAroundViewport: initialRenderRegion, renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion) }, this.invertedWheelEventHandler = (ev) => { const scrollOffset = this.props.horizontal ? ev.target.scrollLeft : ev.target.scrollTop, scrollLength = this.props.horizontal ? ev.target.scrollWidth : ev.target.scrollHeight, clientLength = this.props.horizontal ? ev.target.clientWidth : ev.target.clientHeight, isEventTargetScrollable = scrollLength > clientLength, delta = this.props.horizontal ? ev.deltaX || ev.wheelDeltaX : ev.deltaY || ev.wheelDeltaY; let leftoverDelta = delta; isEventTargetScrollable && (leftoverDelta = delta < 0 ? Math.min(delta + scrollOffset, 0) : Math.max(delta - (scrollLength - clientLength - scrollOffset), 0)); const targetDelta = delta - leftoverDelta; if (this.props.inverted && this._scrollRef && this._scrollRef.getScrollableNode) { const node = this._scrollRef.getScrollableNode(); if (this.props.horizontal) { ev.target.scrollLeft += targetDelta; const nextScrollLeft = node.scrollLeft - leftoverDelta; node.scrollLeft = this.props.getItemLayout ? nextScrollLeft : Math.min(nextScrollLeft, this._totalCellLength); } else { ev.target.scrollTop += targetDelta; const nextScrollTop = node.scrollTop - leftoverDelta; node.scrollTop = this.props.getItemLayout ? nextScrollTop : Math.min(nextScrollTop, this._totalCellLength); } ev.preventDefault(); } }; } _checkProps(props) { const { onScroll, windowSize, getItemCount, data, initialScrollIndex } = props; invariant( !(onScroll && onScroll.__isNative), "Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent to support native onScroll events with useNativeDriver" ), invariant( windowSizeOrDefault(windowSize) > 0, "VirtualizedList: The windowSize prop must be present and set to a value greater than 0." ), invariant(getItemCount, 'VirtualizedList: The "getItemCount" prop must be provided'); const itemCount = getItemCount(data); if (initialScrollIndex != null && !this._hasTriggeredInitialScrollToIndex && (initialScrollIndex < 0 || itemCount > 0 && initialScrollIndex >= itemCount) && !this._hasWarned.initialScrollIndex && (console.warn( `initialScrollIndex "${initialScrollIndex}" is not valid (list has ${itemCount} items)` ), this._hasWarned.initialScrollIndex = !0), __DEV__ && !this._hasWarned.flexWrap) { const flatStyles = StyleSheet.flatten(this.props.contentContainerStyle); flatStyles != null && flatStyles.flexWrap === "wrap" && (console.warn( "`flexWrap: `wrap`` is not supported with the `VirtualizedList` components.Consider using `numColumns` with `FlatList` instead." ), this._hasWarned.flexWrap = !0); } } static _createRenderMask(props, cellsAroundViewport, additionalRegions) { const itemCount = props.getItemCount(props.data); invariant( cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, `Invalid cells around viewport "[${cellsAroundViewport.first}, ${cellsAroundViewport.last}]" was passed to VirtualizedList._createRenderMask` ); const renderMask = new CellRenderMask(itemCount); if (itemCount > 0) { const allRegions = [cellsAroundViewport, ...additionalRegions ?? []]; for (const region of allRegions) renderMask.addCells(region); if (props.initialScrollIndex == null || props.initialScrollIndex <= 0) { const initialRegion = VirtualizedList._initialRenderRegion(props); renderMask.addCells(initialRegion); } const stickyIndicesSet = new Set(props.stickyHeaderIndices); VirtualizedList._ensureClosestStickyHeader( props, stickyIndicesSet, renderMask, cellsAroundViewport.first ); } return renderMask; } static _initialRenderRegion(props) { const itemCount = props.getItemCount(props.data), firstCellIndex = Math.max( 0, Math.min(itemCount - 1, Math.floor(props.initialScrollIndex ?? 0)) ), lastCellIndex = Math.min( itemCount, firstCellIndex + initialNumToRenderOrDefault(props.initialNumToRender) ) - 1; return { first: firstCellIndex, last: lastCellIndex }; } static _ensureClosestStickyHeader(props, stickyIndicesSet, renderMask, cellIdx) { const stickyOffset = props.ListHeaderComponent ? 1 : 0; for (let itemIdx = cellIdx - 1; itemIdx >= 0; itemIdx--) if (stickyIndicesSet.has(itemIdx + stickyOffset)) { renderMask.addCells({ first: itemIdx, last: itemIdx }); break; } } _adjustCellsAroundViewport(props, cellsAroundViewport) { const { data, getItemCount } = props, onEndReachedThreshold = onEndReachedThresholdOrDefault( props.onEndReachedThreshold ), { contentLength, offset, visibleLength } = this._scrollMetrics, distanceFromEnd = contentLength - visibleLength - offset; if (visibleLength <= 0 || contentLength <= 0) return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; let newCellsAroundViewport; if (props.disableVirtualization) { const renderAhead = distanceFromEnd < onEndReachedThreshold * visibleLength ? maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch) : 0; newCellsAroundViewport = { first: 0, last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) }; } else { if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; newCellsAroundViewport = computeWindowedRenderLimits( props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics ), invariant( newCellsAroundViewport.last < getItemCount(data), "computeWindowedRenderLimits() should return range in-bounds" ); } if (this._nestedChildLists.size() > 0) { const childIdx = this._findFirstChildWithMore( newCellsAroundViewport.first, newCellsAroundViewport.last ); newCellsAroundViewport.last = childIdx ?? newCellsAroundViewport.last; } return newCellsAroundViewport; } _findFirstChildWithMore(first, last) { for (let ii = first; ii <= last; ii++) { const cellKeyForIndex = this._indicesToKeys.get(ii); if (cellKeyForIndex != null && this._nestedChildLists.anyInCell( cellKeyForIndex, (childList) => childList.hasMore() )) return ii; } return null; } componentDidMount() { this._isNestedWithSameOrientation() && this.context.registerAsNestedChild({ ref: this, cellKey: this.context.cellKey }), this.setupWebWheelHandler(); } componentWillUnmount() { this._isNestedWithSameOrientation() && this.context.unregisterAsNestedChild({ ref: this }), this._updateCellsToRenderBatcher.dispose({ abort: !0 }), this._viewabilityTuples.forEach((tuple) => { tuple.viewabilityHelper.dispose(); }), this._fillRateHelper.deactivateAndFlush(), this.teardownWebWheelHandler(); } setupWebWheelHandler() { if (this._scrollRef && this._scrollRef.getScrollableNode) this._scrollRef.getScrollableNode().addEventListener("wheel", this.invertedWheelEventHandler); else { setTimeout(() => this.setupWebWheelHandler(), 50); return; } } teardownWebWheelHandler() { this._scrollRef && this._scrollRef.getScrollableNode && this._scrollRef.getScrollableNode().removeEventListener("wheel", this.invertedWheelEventHandler); } static getDerivedStateFromProps(newProps, prevState) { if (newProps.getItemCount(newProps.data) === prevState.renderMask.numCells()) return prevState; const constrainedCells = VirtualizedList._constrainToItemCount( prevState.cellsAroundViewport, newProps ); return { cellsAroundViewport: constrainedCells, renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) }; } _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { const { CellRendererComponent, ItemSeparatorComponent, ListHeaderComponent, ListItemComponent, data, debug, getItem, getItemCount, getItemLayout, horizontal, renderItem } = this.props, stickyOffset = ListHeaderComponent ? 1 : 0, end = getItemCount(data) - 1; let prevCellKey; last = Math.min(end, last); for (let ii = first; ii <= last; ii++) { const item = getItem(data, ii), key = this._keyExtractor(item, ii, this.props); this._indicesToKeys.set(ii, key), stickyIndicesFromProps.has(ii + stickyOffset) && stickyHeaderIndices.push(cells.length); const shouldListenForLayout = getItemLayout == null || debug || this._fillRateHelper.enabled(); cells.push( /* @__PURE__ */ jsx( CellRenderer, { CellRendererComponent, ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : void 0, ListItemComponent, cellKey: key, horizontal, index: ii, inversionStyle, item, prevCellKey, onUpdateSeparators: this._onUpdateSeparators, onCellFocusCapture: (e) => this._onCellFocusCapture(key), onUnmount: this._onCellUnmount, ref: (ref) => { this._cellRefs[key] = ref; }, renderItem, ...shouldListenForLayout && { onCellLayout: this._onCellLayout } }, key ) ), prevCellKey = key; } } static _constrainToItemCount(cells, props) { const itemCount = props.getItemCount(props.data), last = Math.min(itemCount - 1, cells.last), maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); return { first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), last }; } _onUpdateSeparators = (keys, newProps) => { keys.forEach((key) => { const ref = key != null && this._cellRefs[key]; ref && ref.updateSeparatorProps(newProps); }); }; _isNestedWithSameOrientation() { const nestedContext = this.context; return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); } _getSpacerKey = (isVertical) => isVertical ? "height" : "width"; _keyExtractor(item, index, props) { if (props.keyExtractor != null) return props.keyExtractor(item, index); const key = defaultKeyExtractor(item, index); return key === String(index) && (_usedIndexForKey = !0, item.type && item.type.displayName && (_keylessItemComponentName = item.type.displayName)), key; } render() { this._checkProps(this.props); const { ListEmptyComponent, ListFooterComponent, ListHeaderComponent } = this.props, { data, horizontal } = this.props, inversionStyle = this.props.inverted ? horizontalOrDefault(this.props.horizontal) ? styles.horizontallyInverted : styles.verticallyInverted : null, cells = [], stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices), stickyHeaderIndices = []; if (ListHeaderComponent) { stickyIndicesFromProps.has(0) && stickyHeaderIndices.push(0); const element = React.isValidElement(ListHeaderComponent) ? ListHeaderComponent : /* @__PURE__ */ jsx(ListHeaderComponent, {}); cells.push( /* @__PURE__ */ jsx( VirtualizedListCellContextProvider, { cellKey: this._getCellKey() + "-header", children: /* @__PURE__ */ jsx( View, { onLayout: this._onLayoutHeader, style: [inversionStyle, this.props.ListHeaderComponentStyle], children: element } ) }, "$header" ) ); } const itemCount = this.props.getItemCount(data); if (itemCount === 0 && ListEmptyComponent) { const element = React.isValidElement( ListEmptyComponent ) ? ListEmptyComponent : /* @__PURE__ */ jsx(ListEmptyComponent, {}); cells.push( /* @__PURE__ */ jsx( VirtualizedListCellContextProvider, { cellKey: this._getCellKey() + "-empty", children: React.cloneElement(element, { onLayout: (event) => { this._onLayoutEmpty(event), element.props.onLayout && element.props.onLayout(event); }, style: [inversionStyle, element.props.style] }) }, "$empty" ) ); } if (itemCount > 0) { _usedIndexForKey = !1, _keylessItemComponentName = ""; const spacerKey = this._getSpacerKey(!horizontal), renderRegions = this.state.renderMask.enumerateRegions(), lastSpacer = findLastWhere(renderRegions, (r) => r.isSpacer); for (const section of renderRegions) if (section.isSpacer) { if (this.props.disableVirtualization) continue; const last = section === lastSpacer && !this.props.getItemLayout ? clamp(section.first - 1, section.last, this._highestMeasuredFrameIndex) : section.last, firstMetrics = this.__getFrameMetricsApprox(section.first, this.props), lastMetrics = this.__getFrameMetricsApprox(last, this.props), spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset; cells.push( /* @__PURE__ */ jsx(View, { style: { [spacerKey]: spacerSize } }, `$spacer-${section.first}`) ); } else this._pushCells( cells, stickyHeaderIndices, stickyIndicesFromProps, section.first, section.last, inversionStyle ); !this._hasWarned.keys && _usedIndexForKey && (console.warn( "VirtualizedList: missing keys for items, make sure to specify a key or id property on each item or provide a custom keyExtractor.", _keylessItemComponentName ), this._hasWarned.keys = !0); } if (ListFooterComponent) { const element = React.isValidElement(ListFooterComponent) ? ListFooterComponent : /* @__PURE__ */ jsx(ListFooterComponent, {}); cells.push( /* @__PURE__ */ jsx( VirtualizedListCellContextProvider, { cellKey: this._getFooterCellKey(), children: /* @__PURE__ */ jsx( View, { onLayout: this._onLayoutFooter, style: [inversionStyle, this.props.ListFooterComponentStyle], children: element } ) }, "$footer" ) ); } const scrollProps = { ...this.props, onContentSizeChange: this._onContentSizeChange, onLayout: this._onLayout, onScroll: this._onScroll, onScrollBeginDrag: this._onScrollBeginDrag, onScrollEndDrag: this._onScrollEndDrag, onMomentumScrollBegin: this._onMomentumScrollBegin, onMomentumScrollEnd: this._onMomentumScrollEnd, scrollEventThrottle: scrollEventThrottleOrDefault(this.props.scrollEventThrottle), invertStickyHeaders: this.props.invertStickyHeaders !== void 0 ? this.props.invertStickyHeaders : this.props.inverted, stickyHeaderIndices, style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style }; return this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1, /* @__PURE__ */ jsx( VirtualizedListContextProvider, { value: { cellKey: null, getScrollMetrics: this._getScrollMetrics, horizontal: horizontalOrDefault(this.props.horizontal), getOutermostParentListRef: this._getOutermostParentListRef, registerAsNestedChild: this._registerAsNestedChild, unregisterAsNestedChild: this._unregisterAsNestedChild }, children: React.cloneElement( (this.props.renderScrollComponent || this._defaultRenderScrollComponent)( scrollProps ), { ref: this._captureScrollRef }, cells ) } ); } componentDidUpdate(prevProps) { const { data, extraData } = this.props; (data !== prevProps.data || extraData !== prevProps.extraData) && this._viewabilityTuples.forEach((tuple) => { tuple.viewabilityHelper.resetViewableIndices(); }); const hiPriInProgress = this._hiPriInProgress; this._scheduleCellsToRenderUpdate(), hiPriInProgress && (this._hiPriInProgress = !1); } _averageCellLength = 0; _cellRefs = {}; _fillRateHelper; _frames = {}; _footerLength = 0; _hasTriggeredInitialScrollToIndex = !1; _hasInteracted = !1; _hasMore = !1; _hasWarned = {}; _headerLength = 0; _hiPriInProgress = !1; _highestMeasuredFrameIndex = 0; _indicesToKeys = /* @__PURE__ */ new Map(); _lastFocusedCellKey = null; _nestedChildLists = new ChildListCollection(); _offsetFromParentVirtualizedList = 0; _prevParentOffset = 0; _scrollMetrics = { contentLength: 0, dOffset: 0, dt: 10, offset: 0, timestamp: 0, velocity: 0, visibleLength: 0, zoomScale: 1 }; _scrollRef = null; _sentStartForContentLength = 0; _sentEndForContentLength = 0; _totalCellLength = 0; _totalCellsMeasured = 0; _updateCellsToRenderBatcher; _viewabilityTuples = []; _captureScrollRef = (ref) => { this._scrollRef = ref; }; _computeBlankness() { this._fillRateHelper.computeBlankness( // @ts-ignore this.props, this.state.cellsAroundViewport, this._scrollMetrics ); } _defaultRenderScrollComponent = (props) => { const onRefresh = props.onRefresh; return this._isNestedWithSameOrientation() ? /* @__PURE__ */ jsx(View, { ...props }) : onRefresh ? (invariant( typeof props.refreshing == "boolean", "`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `" + JSON.stringify(props.refreshing ?? "undefined") + "`" ), /* @__PURE__ */ jsx( ScrollView, { ...props, refreshControl: props.refreshControl == null ? /* @__PURE__ */ jsx( RefreshControl, { refreshing: props.refreshing, onRefresh, progressViewOffset: props.progressViewOffset } ) : props.refreshControl } )) : /* @__PURE__ */ jsx(ScrollView, { ...props }); }; _onCellLayout = (e, cellKey, index) => { const layout = e.nativeEvent.layout, next = { offset: this._selectOffset(layout), length: this._selectLength(layout), index, inLayout: !0 }, curr = this._frames[cellKey]; !curr || next.offset !== curr.offset || next.length !== curr.length || index !== curr.index ? (this._totalCellLength += next.length - (curr ? curr.length : 0), this._totalCellsMeasured += curr ? 0 : 1, this._averageCellLength = this._totalCellLength / this._totalCellsMeasured, this._frames[cellKey] = next, this._highestMeasuredFrameIndex = Math.max(this._highestMeasuredFrameIndex, index), this._scheduleCellsToRenderUpdate()) : this._frames[cellKey].inLayout = !0, this._triggerRemeasureForChildListsInCell(cellKey), this._computeBlankness(), this._updateViewableItems( // @ts-ignore this.props, this.state.cellsAroundViewport ); }; _onCellFocusCapture(cellKey) { this._lastFocusedCellKey = cellKey, this._updateCellsToRender(); } _onCellUnmount = (cellKey) => { delete this._cellRefs[cellKey]; const curr = this._frames[cellKey]; curr && (this._frames[cellKey] = { ...curr, inLayout: !1 }); }; _triggerRemeasureForChildListsInCell(cellKey) { this._nestedChildLists.forEachInCell(cellKey, (childList) => { childList.measureLayoutRelativeToContainingList(); }); } measureLayoutRelativeToContainingList() { try { if (!this._scrollRef) return; this._scrollRef.measureLayout( this.context.getOutermostParentListRef().getScrollRef(), (x, y, width, height) => { this._offsetFromParentVirtualizedList = this._selectOffset({ x, y }), this._scrollMetrics.contentLength = this._selectLength({ width, height }); const scrollMetrics = this._convertParentScrollMetrics( this.context.getScrollMetrics() ); (this._scrollMetrics.visibleLength !== scrollMetrics.visibleLength || this._scrollMetrics.offset !== scrollMetrics.offset) && (this._scrollMetrics.visibleLength = scrollMetrics.visibleLength, this._scrollMetrics.offset = scrollMetrics.offset, this._nestedChildLists.forEach((childList) => { childList.measureLayoutRelativeToContainingList(); })); }, (error) => { console.warn( "VirtualizedList: Encountered an error while measuring a list's offset from its containing VirtualizedList." ); } ); } catch (error) { console.warn("measureLayoutRelativeToContainingList threw an error", error.stack); } } _onLayout = (e) => { this._isNestedWithSameOrientation() ? this.measureLayoutRelativeToContainingList() : this._scrollMetrics.visibleLength = this._selectLength(e.nativeEvent.layout), this.props.onLayout && this.props.onLayout(e), this._scheduleCellsToRenderUpdate(), this._maybeCallOnEdgeReached(); }; _onLayoutEmpty = (e) => { this.props.onLayout && this.props.onLayout(e); }; _getFooterCellKey() { return this._getCellKey() + "-footer"; } _onLayoutFooter = (e) => { this._triggerRemeasureForChildListsInCell(this._getFooterCellKey()), this._footerLength = this._selectLength(e.nativeEvent.layout); }; _onLayoutHeader = (e) => { this._headerLength = this._selectLength(e.nativeEvent.layout); }; _renderDebugOverlay() { const normalize = this._scrollMetrics.visibleLength / (this._scrollMetrics.contentLength || 1), framesInLayout = [], itemCount = this.props.getItemCount(this.props.data); for (let ii = 0; ii < itemCount; ii++) { const frame = this.__getFrameMetricsApprox(ii, this.props); frame.inLayout && framesInLayout.push(frame); } const windowTop = this.__getFrameMetricsApprox( this.state.cellsAroundViewport.first, this.props ).offset, frameLast = this.__getFrameMetricsApprox( this.state.cellsAroundViewport.last, this.props ), windowLen = frameLast.offset + frameLast.length - windowTop, visTop = this._scrollMetrics.offset, visLen = this._scrollMetrics.visibleLength; return /* @__PURE__ */ jsxs(View, { style: [styles.debugOverlayBase, styles.debugOverlay], children: [ framesInLayout.map((f, ii) => /* @__PURE__ */ jsx( View, { style: [ styles.debugOverlayBase, styles.debugOverlayFrame, { top: f.offset * normalize, height: f.length * normalize } ] }, "f" + ii )), /* @__PURE__ */ jsx( View, { style: [ styles.debugOverlayBase, styles.debugOverlayFrameLast, { top: windowTop * normalize, height: windowLen * normalize } ] } ), /* @__PURE__ */ jsx( View, { style: [ styles.debugOverlayBase, styles.debugOverlayFrameVis, { top: visTop * normalize, height: visLen * normalize } ] } ) ] }); } _selectLength(metrics) { return horizontalOrDefault(this.props.horizontal) ? metrics.width : metrics.height; } _selectOffset(metrics) { return horizontalOrDefault(this.props.horizontal) ? metrics.x : metrics.y; } _maybeCallOnEdgeReached() { const { data, getItemCount, onStartReached, onStartReachedThreshold, onEndReached, onEndReachedThreshold, initialScrollIndex } = this.props, { contentLength, visibleLength, offset } = this._scrollMetrics; let distanceFromStart = offset, distanceFromEnd = contentLength - visibleLength - offset; distanceFromStart < ON_EDGE_REACHED_EPSILON && (distanceFromStart = 0), distanceFromEnd < ON_EDGE_REACHED_EPSILON && (distanceFromEnd = 0); const DEFAULT_THRESHOLD_PX = 2, startThreshold = onStartReachedThreshold != null ? onStartReachedThreshold * visibleLength : DEFAULT_THRESHOLD_PX, endThreshold = onEndReachedThreshold != null ? onEndReachedThreshold * visibleLength : DEFAULT_THRESHOLD_PX, isWithinStartThreshold = distanceFromStart <= startThreshold, isWithinEndThreshold = distanceFromEnd <= endThreshold; onEndReached && this.state.cellsAroundViewport.last === getItemCount(data) - 1 && isWithinEndThreshold && this._scrollMetrics.contentLength !== this._sentEndForContentLength ? (this._sentEndForContentLength = this._scrollMetrics.contentLength, onEndReached({ distanceFromEnd })) : onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength ? (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) && (this._sentStartForContentLength = this._scrollMetrics.contentLength, onStartReached({ distanceFromStart })) : (this._sentStartForContentLength = isWithinStartThreshold ? this._sentStartForContentLength : 0, this._sentEndForContentLength = isWithinEndThreshold ? this._sentEndForContentLength : 0); } _onContentSizeChange = (width, height) => { width > 0 && height > 0 && this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 && !this._hasTriggeredInitialScrollToIndex && (this.props.contentOffset == null && (this.props.initialScrollIndex < this.props.getItemCount(this.props.data) ? this.scrollToIndex({ animated: !1, index: nullthrows(this.props.initialScrollIndex) }) : this.scrollToEnd({ animated: !1 })), this._hasTriggeredInitialScrollToIndex = !0), this.props.onContentSizeChange && this.props.onContentSizeChange(width, height), this._scrollMetrics.contentLength = this._selectLength({ height, width }), this._scheduleCellsToRenderUpdate(), this._maybeCallOnEdgeReached(); }; _convertParentScrollMetrics = (metrics) => { const offset = metrics.offset - this._offsetFromParentVirtualizedList, visibleLength = metrics.visibleLength, dOffset = offset - this._scrollMetrics.offset, contentLength = this._scrollMetrics.contentLength; return { visibleLength, contentLength, offset, dOffset }; }; _onScroll = (e) => { this._nestedChildLists.forEach((childList) => { childList._onScroll(e); }), this.props.onScroll && this.props.onScroll(e); const timestamp = e.timeStamp; let visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement), contentLength = this._selectLength(e.nativeEvent.contentSize), offset = this._selectOffset(e.nativeEvent.contentOffset), dOffset = offset - this._scrollMetrics.offset; if (this._isNestedWithSameOrientation()) { if (this._scrollMetrics.contentLength === 0) return; ({ visibleLength, contentLength, offset, dOffset } = this._convertParentScrollMetrics({ visibleLength, offset })); } const dt = this._scrollMetrics.timestamp ? Math.max(1, timestamp - this._scrollMetrics.timestamp) : 1, velocity = dOffset / dt; dt > 500 && this._scrollMetrics.dt > 500 && contentLength > 5 * visibleLength && !this._hasWarned.perf && (infoLog( "VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc.", { dt, prevDt: this._scrollMetrics.dt, contentLength } ), this._hasWarned.perf = !0); const zoomScale = e.nativeEvent.zoomScale < 0 ? 1 : e.nativeEvent.zoomScale; this._scrollMetrics = { contentLength, dt, dOffset, offset, timestamp, velocity, visibleLength, zoomScale }, this._updateViewableItems(this.props, this.state.cellsAroundViewport), this.props && (this._maybeCallOnEdgeReached(), velocity !== 0 && this._fillRateHelper.activate(), this._computeBlankness(), this._scheduleCellsToRenderUpdate()); }; _scheduleCellsToRenderUpdate() { const { first, last } = this.state.cellsAroundViewport, { offset, visibleLength, velocity } = this._scrollMetrics, itemCount = this.props.getItemCount(this.props.data); let hiPri = !1; const onStartReachedThreshold = onStartReachedThresholdOrDefault( this.props.onStartReachedThreshold ), onEndReachedThreshold = onEndReachedThresholdOrDefault( this.props.onEndReachedThreshold ); if (first > 0) { const distTop = offset - this.__getFrameMetricsApprox(first, this.props).offset; hiPri = distTop < 0 || velocity < -2 && distTop < getScrollingThreshold(onStartReachedThreshold, visibleLength); } if (!hiPri && last >= 0 && last < itemCount - 1) { const distBottom = this.__getFrameMetricsApprox(last, this.props).offset - (offset + visibleLength); hiPri = distBottom < 0 || velocity > 2 && distBottom < getScrollingThreshold(onEndReachedThreshold, visibleLength); } if (hiPri && (this._averageCellLength || this.props.getItemLayout) && !this._hiPriInProgress) { this._hiPriInProgress = !0, this._updateCellsToRenderBatcher.dispose({ abort: !0 }), this._updateCellsToRender(); return; } else this._updateCellsToRenderBatcher.schedule(); } _onScrollBeginDrag = (e) => { this._nestedChildLists.forEach((childList) => { childList._onScrollBeginDrag(e); }), this._viewabilityTuples.forEach((tuple) => { tuple.viewabilityHelper.recordInteraction(); }), this._hasInteracted = !0, this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e); }; _onScrollEndDrag = (e) => { this._nestedChildLists.forEach((childList) => { childList._onScrollEndDrag(e); }); const { velocity } = e.nativeEvent; velocity && (this._scrollMetrics.velocity = this._selectOffset(velocity)), this._computeBlankness(), this.props.onScrollEndDrag && this.props.onScrollEndDrag(e); }; _onMomentumScrollBegin = (e) => { this._nestedChildLists.forEach((childList) => { childList._onMomentumScrollBegin(e); }), this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e); }; _onMomentumScrollEnd = (e) => { this._nestedChildLists.forEach((childList) => { childList._onMomentumScrollEnd(e); }), this._scrollMetrics.velocity = 0, this._computeBlankness(), this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e); }; _updateCellsToRender = () => { this._updateViewableItems(this.props, this.state.cellsAroundViewport), this.setState((state, props) => { const cellsAroundViewport = this._adjustCellsAroundViewport( props, state.cellsAroundViewport ), renderMask = VirtualizedList._createRenderMask( props, cellsAroundViewport, this._getNonViewportRenderRegions(props) ); return cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask) ? null : { cellsAroundViewport, renderMask }; }); }; _createViewToken = (index, isViewable, props) => { const { data, getItem } = props, item = getItem(data, index); return { index, item, key: this._keyExtractor(item, index, props), isViewable }; }; _getOffsetApprox = (index, props) => { if (Number.isInteger(index)) return this.__getFrameMetricsApprox(index, props).offset; { const frameMetrics = this.__getFrameMetricsApprox(Math.floor(index), props), remainder = index - Math.floor(index); return frameMetrics.offset + remainder * frameMetrics.length; } }; __getFrameMetricsApprox = (index, props) => { const frame = this._getFrameMetrics(index, props); if (frame && frame.index === index) return frame; { const { data, getItemCount, getItemLayout } = props; return invariant( index >= 0 && index < getItemCount(data), "Tried to get frame for out of range index " + index ), invariant( !getItemLayout, "Should not have to estimate frames when a measurement metrics function is provided" ), { length: this._averageCellLength, offset: this._averageCellLength * index }; } }; _getFrameMetrics = (index, props) => { const { data, getItem, getItemCount, getItemLayout } = props; invariant( index >= 0 && index < getItemCount(data), "Tried to get frame for out of range index " + index ); const item = getItem(data, index), frame = this._frames[this._keyExtractor(item, index, props)]; return (!frame || frame.index !== index) && getItemLayout ? getItemLayout(data, index) : frame; }; _getNonViewportRenderRegions = (props) => { if (!(this._lastFocusedCellKey && this._cellRefs[this._lastFocusedCellKey])) return []; const focusedCellIndex = this._cellRefs[this._lastFocusedCellKey].props.index, itemCount = props.getItemCount(props.data); if (focusedCellIndex >= itemCount || this._keyExtractor( props.getItem(props.data, focusedCellIndex), focusedCellIndex, props ) !== this._lastFocusedCellKey) return []; let first = focusedCellIndex, heightOfCellsBeforeFocused = 0; for (let i = first - 1; i >= 0 && heightOfCellsBeforeFocused < this._scrollMetrics.visibleLength; i--) first--, heightOfCellsBeforeFocused += this.__getFrameMetricsApprox(i, props).length; let last = focusedCellIndex, heightOfCellsAfterFocused = 0; for (let i = last + 1; i < itemCount && heightOfCellsAfterFocused < this._scrollMetrics.visibleLength; i++) last++, heightOfCellsAfterFocused += this.__getFrameMetricsApprox(i, props).length; return [{ first, last }]; }; _updateViewableItems(props, cellsAroundViewport) { this._viewabilityTuples.forEach((tuple) => { tuple.viewabilityHelper.onUpdate( props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport ); }); } } const styles = StyleSheet.create({ verticallyInverted: { transform: "scaleY(-1)" }, horizontallyInverted: { transform: "scaleX(-1)" }, debug: { flex: 1 }, debugOverlayBase: { position: "absolute", top: 0, right: 0 }, debugOverlay: { bottom: 0, width: 20, borderColor: "blue", borderWidth: 1 }, debugOverlayFrame: { left: 0, backgroundColor: "orange" }, debugOverlayFrameLast: { left: 0, borderColor: "green", borderWidth: 2 }, debugOverlayFrameVis: { left: 0, borderColor: "red", borderWidth: 2 } }); var VirtualizedList_default = VirtualizedList; export { VirtualizedList_default as default }; //# sourceMappingURL=index.js.map