From dc0da6f889e554154b69ea1667a96b219de192f2 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Thu, 8 Jun 2023 09:28:36 -0700 Subject: [PATCH] Extract logic to CellMetricsAggregator Summary: This extracts the state and logic VirtualizedList uses to query information related to cell metrics. We will need to modify this (and other places) when fixing up RTL support for horizontal FlatList. Changelog: [Internal] Reviewed By: javache Differential Revision: D46427052 fbshipit-source-id: c853b6c2165dd40786d3445f9b833049b5888b81 --- .../virtualized-lists/Lists/FillRateHelper.js | 30 ++- .../Lists/ListMetricsAggregator.js | 179 +++++++++++++++++ .../Lists/ViewabilityHelper.js | 27 +-- .../Lists/VirtualizeUtils.js | 30 +-- .../Lists/VirtualizedList.js | 184 +++++------------- .../Lists/VirtualizedListProps.js | 12 -- .../Lists/VirtualizedSectionList.js | 8 +- .../Lists/__tests__/FillRateHelper-test.js | 42 ++-- .../Lists/__tests__/ViewabilityHelper-test.js | 68 +++---- .../Lists/__tests__/VirtualizeUtils-test.js | 22 ++- 10 files changed, 331 insertions(+), 271 deletions(-) create mode 100644 packages/virtualized-lists/Lists/ListMetricsAggregator.js diff --git a/packages/virtualized-lists/Lists/FillRateHelper.js b/packages/virtualized-lists/Lists/FillRateHelper.js index aa4a331741c4c5..c5bfadf962ca3c 100644 --- a/packages/virtualized-lists/Lists/FillRateHelper.js +++ b/packages/virtualized-lists/Lists/FillRateHelper.js @@ -10,7 +10,8 @@ 'use strict'; -import type {CellMetricProps} from './VirtualizedListProps'; +import type {CellMetricProps} from './ListMetricsAggregator'; +import ListMetricsAggregator from './ListMetricsAggregator'; export type FillRateInfo = Info; @@ -27,13 +28,6 @@ class Info { sample_count: number = 0; } -type CellMetrics = { - inLayout?: boolean, - length: number, - offset: number, - ... -}; - const DEBUG = false; let _listeners: Array<(Info) => void> = []; @@ -51,7 +45,7 @@ let _sampleRate = DEBUG ? 1 : null; class FillRateHelper { _anyBlankStartTime: ?number = null; _enabled = false; - _getCellMetrics: (index: number, props: CellMetricProps) => ?CellMetrics; + _listMetrics: ListMetricsAggregator; _info: Info = new Info(); _mostlyBlankStartTime: ?number = null; _samplesStartTime: ?number = null; @@ -79,10 +73,8 @@ class FillRateHelper { _minSampleCount = minSampleCount; } - constructor( - getCellMetrics: (index: number, props: CellMetricProps) => ?CellMetrics, - ) { - this._getCellMetrics = getCellMetrics; + constructor(listMetrics: ListMetricsAggregator) { + this._listMetrics = listMetrics; this._enabled = (_sampleRate || 0) > Math.random(); this._resetData(); } @@ -186,12 +178,12 @@ class FillRateHelper { let blankTop = 0; let first = cellsAroundViewport.first; - let firstFrame = this._getCellMetrics(first, props); + let firstFrame = this._listMetrics.getCellMetrics(first, props); while ( first <= cellsAroundViewport.last && - (!firstFrame || !firstFrame.inLayout) + (!firstFrame || !firstFrame.isMounted) ) { - firstFrame = this._getCellMetrics(first, props); + firstFrame = this._listMetrics.getCellMetrics(first, props); first++; } // Only count blankTop if we aren't rendering the first item, otherwise we will count the header @@ -204,12 +196,12 @@ class FillRateHelper { } let blankBottom = 0; let last = cellsAroundViewport.last; - let lastFrame = this._getCellMetrics(last, props); + let lastFrame = this._listMetrics.getCellMetrics(last, props); while ( last >= cellsAroundViewport.first && - (!lastFrame || !lastFrame.inLayout) + (!lastFrame || !lastFrame.isMounted) ) { - lastFrame = this._getCellMetrics(last, props); + lastFrame = this._listMetrics.getCellMetrics(last, props); last--; } // Only count blankBottom if we aren't rendering the last item, otherwise we will count the diff --git a/packages/virtualized-lists/Lists/ListMetricsAggregator.js b/packages/virtualized-lists/Lists/ListMetricsAggregator.js new file mode 100644 index 00000000000000..e916804562da42 --- /dev/null +++ b/packages/virtualized-lists/Lists/ListMetricsAggregator.js @@ -0,0 +1,179 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import type {Props as VirtualizedListProps} from './VirtualizedListProps'; +import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; + +import invariant from 'invariant'; + +export type CellMetrics = { + /** + * Index of the item in the list + */ + index: number, + /** + * Length of the cell along the scrolling axis + */ + length: number, + /** + * Offset to the cell along the scrolling axis + */ + offset: number, + /** + * Whether the cell is last known to be mounted + */ + isMounted: boolean, +}; + +/** + * Subset of VirtualizedList props needed to calculate cell metrics + */ +export type CellMetricProps = { + data: VirtualizedListProps['data'], + getItemCount: VirtualizedListProps['getItemCount'], + getItem: VirtualizedListProps['getItem'], + getItemLayout?: VirtualizedListProps['getItemLayout'], + keyExtractor?: VirtualizedListProps['keyExtractor'], + ... +}; + +/** + * Provides an interface to query information about the metrics of a list and its cells. + */ +export default class ListMetricsAggregator { + _averageCellLength = 0; + _frames: {[string]: CellMetrics} = {}; + _highestMeasuredCellIndex = 0; + _totalCellLength = 0; + _totalCellsMeasured = 0; + + /** + * Notify the ListMetricsAggregator that a cell has been laid out. + * + * @returns whether the cell layout has changed since last notification + */ + notifyCellLayout( + cellKey: string, + index: number, + length: number, + offset: number, + ): boolean { + const next: CellMetrics = { + offset, + length, + index, + isMounted: true, + }; + const curr = this._frames[cellKey]; + if ( + !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._highestMeasuredCellIndex = Math.max( + this._highestMeasuredCellIndex, + index, + ); + return true; + } else { + this._frames[cellKey].isMounted = true; + return false; + } + } + + /** + * Notify ListMetricsAggregator that a cell has been unmounted. + */ + notifyCellUnmounted(cellKey: string): void { + const curr = this._frames[cellKey]; + if (curr) { + this._frames[cellKey] = {...curr, isMounted: false}; + } + } + + /** + * Return the average length of the cells which have been measured + */ + getAverageCellLength(): number { + return this._averageCellLength; + } + + /** + * Return the highest measured cell index + */ + getHighestMeasuredCellIndex(): number { + return this._highestMeasuredCellIndex; + } + + /** + * Returns the exact metrics of a cell if it has already been laid out, + * otherwise an estimate based on the average length of previously measured + * cells + */ + getCellMetricsApprox(index: number, props: CellMetricProps): CellMetrics { + const frame = this.getCellMetrics(index, props); + if (frame && frame.index === index) { + // check for invalid frames due to row re-ordering + return frame; + } else { + const {data, getItemCount} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); + return { + length: this._averageCellLength, + offset: this._averageCellLength * index, + index, + isMounted: false, + }; + } + } + + /** + * Returns the exact metrics of a cell if it has already been laid out + */ + getCellMetrics(index: number, props: CellMetricProps): ?CellMetrics { + const {data, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); + const keyExtractor = props.keyExtractor ?? defaultKeyExtractor; + const frame = this._frames[keyExtractor(data?.[index], index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + const {length, offset} = getItemLayout(data, index); + return {index, length, offset, isMounted: true}; + } + } + return frame; + } + + /** + * Gets an approximate offset to an item at a given index. Supports + * fractional indices. + */ + getCellOffsetApprox(index: number, props: CellMetricProps): number { + if (Number.isInteger(index)) { + return this.getCellMetricsApprox(index, props).offset; + } else { + const frameMetrics = this.getCellMetricsApprox(Math.floor(index), props); + const remainder = index - Math.floor(index); + return frameMetrics.offset + remainder * frameMetrics.length; + } + } +} diff --git a/packages/virtualized-lists/Lists/ViewabilityHelper.js b/packages/virtualized-lists/Lists/ViewabilityHelper.js index d0d1a5f82502fc..cae7c0bd685572 100644 --- a/packages/virtualized-lists/Lists/ViewabilityHelper.js +++ b/packages/virtualized-lists/Lists/ViewabilityHelper.js @@ -10,7 +10,8 @@ 'use strict'; -import type {CellMetricProps} from './VirtualizedListProps'; +import type {CellMetricProps} from './ListMetricsAggregator'; +import ListMetricsAggregator from './ListMetricsAggregator'; const invariant = require('invariant'); @@ -104,14 +105,7 @@ class ViewabilityHelper { props: CellMetricProps, scrollOffset: number, viewportHeight: number, - getCellMetrics: ( - index: number, - props: CellMetricProps, - ) => ?{ - length: number, - offset: number, - ... - }, + listMetrics: ListMetricsAggregator, // Optional optimization to reduce the scan size renderRange?: { first: number, @@ -146,7 +140,7 @@ class ViewabilityHelper { return []; } for (let idx = first; idx <= last; idx++) { - const metrics = getCellMetrics(idx, props); + const metrics = listMetrics.getCellMetrics(idx, props); if (!metrics) { continue; } @@ -181,14 +175,7 @@ class ViewabilityHelper { props: CellMetricProps, scrollOffset: number, viewportHeight: number, - getCellMetrics: ( - index: number, - props: CellMetricProps, - ) => ?{ - length: number, - offset: number, - ... - }, + listMetrics: ListMetricsAggregator, createViewToken: ( index: number, isViewable: boolean, @@ -210,7 +197,7 @@ class ViewabilityHelper { if ( (this._config.waitForInteraction && !this._hasInteracted) || itemCount === 0 || - !getCellMetrics(0, props) + !listMetrics.getCellMetrics(0, props) ) { return; } @@ -220,7 +207,7 @@ class ViewabilityHelper { props, scrollOffset, viewportHeight, - getCellMetrics, + listMetrics, renderRange, ); } diff --git a/packages/virtualized-lists/Lists/VirtualizeUtils.js b/packages/virtualized-lists/Lists/VirtualizeUtils.js index 36295aa0deb50b..3dd88d607590bf 100644 --- a/packages/virtualized-lists/Lists/VirtualizeUtils.js +++ b/packages/virtualized-lists/Lists/VirtualizeUtils.js @@ -10,7 +10,8 @@ 'use strict'; -import type {CellMetricProps} from './VirtualizedListProps'; +import type {CellMetricProps} from './ListMetricsAggregator'; +import ListMetricsAggregator from './ListMetricsAggregator'; /** * Used to find the indices of the frames that overlap the given offsets. Useful for finding the @@ -20,14 +21,7 @@ import type {CellMetricProps} from './VirtualizedListProps'; export function elementsThatOverlapOffsets( offsets: Array, props: CellMetricProps, - getCellMetrics: ( - index: number, - props: CellMetricProps, - ) => { - length: number, - offset: number, - ... - }, + listMetrics: ListMetricsAggregator, zoomScale: number = 1, ): Array { const itemCount = props.getItemCount(props.data); @@ -38,9 +32,8 @@ export function elementsThatOverlapOffsets( let right = itemCount - 1; while (left <= right) { - // eslint-disable-next-line no-bitwise - const mid = left + ((right - left) >>> 1); - const frame = getCellMetrics(mid, props); + const mid = left + Math.floor((right - left) / 2); + const frame = listMetrics.getCellMetricsApprox(mid, props); const scaledOffsetStart = frame.offset * zoomScale; const scaledOffsetEnd = (frame.offset + frame.length) * zoomScale; @@ -106,14 +99,7 @@ export function computeWindowedRenderLimits( first: number, last: number, }, - getCellMetricsApprox: ( - index: number, - props: CellMetricProps, - ) => { - length: number, - offset: number, - ... - }, + listMetrics: ListMetricsAggregator, scrollMetrics: { dt: number, offset: number, @@ -152,7 +138,7 @@ export function computeWindowedRenderLimits( const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength); const lastItemOffset = - getCellMetricsApprox(itemCount - 1, props).offset * zoomScale; + listMetrics.getCellMetricsApprox(itemCount - 1, props).offset * zoomScale; if (lastItemOffset < overscanBegin) { // Entire list is before our overscan window return { @@ -165,7 +151,7 @@ export function computeWindowedRenderLimits( let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets( [overscanBegin, visibleBegin, visibleEnd, overscanEnd], props, - getCellMetricsApprox, + listMetrics, zoomScale, ); overscanFirst = overscanFirst == null ? 0 : overscanFirst; diff --git a/packages/virtualized-lists/Lists/VirtualizedList.js b/packages/virtualized-lists/Lists/VirtualizedList.js index efc98eaf9e9172..0c078e085957c3 100644 --- a/packages/virtualized-lists/Lists/VirtualizedList.js +++ b/packages/virtualized-lists/Lists/VirtualizedList.js @@ -16,13 +16,13 @@ import type { } from 'react-native/Libraries/Types/CoreEventTypes'; import type {ViewToken} from './ViewabilityHelper'; import type { - CellMetricProps, Item, Props, RenderItemProps, RenderItemType, Separators, } from './VirtualizedListProps'; +import type {CellMetricProps} from './ListMetricsAggregator'; import { RefreshControl, @@ -37,6 +37,7 @@ import infoLog from '../Utilities/infoLog'; import {CellRenderMask} from './CellRenderMask'; import ChildListCollection from './ChildListCollection'; import FillRateHelper from './FillRateHelper'; +import ListMetricsAggregator from './ListMetricsAggregator'; import StateSafePureComponent from './StateSafePureComponent'; import ViewabilityHelper from './ViewabilityHelper'; import CellRenderer from './VirtualizedListCellRenderer'; @@ -176,7 +177,7 @@ class VirtualizedList extends StateSafePureComponent { if (veryLast < 0) { return; } - const frame = this.__getCellMetricsApprox(veryLast, this.props); + const frame = this._listMetrics.getCellMetricsApprox(veryLast, this.props); const offset = Math.max( 0, frame.offset + @@ -237,24 +238,31 @@ class VirtualizedList extends StateSafePureComponent { getItemCount(data) - 1 }`, ); - if (!getItemLayout && index > this._highestMeasuredFrameIndex) { + if ( + !getItemLayout && + index > this._listMetrics.getHighestMeasuredCellIndex() + ) { 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, + averageItemLength: this._listMetrics.getAverageCellLength(), + highestMeasuredFrameIndex: + this._listMetrics.getHighestMeasuredCellIndex(), index, }); return; } - const frame = this.__getCellMetricsApprox(Math.floor(index), this.props); + const frame = this._listMetrics.getCellMetricsApprox( + Math.floor(index), + this.props, + ); const offset = Math.max( 0, - this._getOffsetApprox(index, this.props) - + this._listMetrics.getCellOffsetApprox(index, this.props) - (viewPosition || 0) * (this._scrollMetrics.visibleLength - frame.length), ) - (viewOffset || 0); @@ -427,7 +435,7 @@ class VirtualizedList extends StateSafePureComponent { super(props); this._checkProps(props); - this._fillRateHelper = new FillRateHelper(this._getCellMetrics); + this._fillRateHelper = new FillRateHelper(this._listMetrics); this._updateCellsToRenderBatcher = new Batchinator( this._updateCellsToRender, this.props.updateCellsBatchingPeriod ?? 50, @@ -683,7 +691,7 @@ class VirtualizedList extends StateSafePureComponent { maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, - this.__getCellMetricsApprox, + this._listMetrics, this._scrollMetrics, ); invariant( @@ -1037,15 +1045,18 @@ class VirtualizedList extends StateSafePureComponent { ? clamp( section.first - 1, section.last, - this._highestMeasuredFrameIndex, + this._listMetrics.getHighestMeasuredCellIndex(), ) : section.last; - const firstMetrics = this.__getCellMetricsApprox( + const firstMetrics = this._listMetrics.getCellMetricsApprox( section.first, this.props, ); - const lastMetrics = this.__getCellMetricsApprox(last, this.props); + const lastMetrics = this._listMetrics.getCellMetricsApprox( + last, + this.props, + ); const spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset; cells.push( @@ -1223,17 +1234,9 @@ class VirtualizedList extends StateSafePureComponent { } } - _averageCellLength = 0; _cellRefs: {[string]: null | CellRenderer} = {}; _fillRateHelper: FillRateHelper; - _frames: { - [string]: { - inLayout?: boolean, - index: number, - length: number, - offset: number, - }, - } = {}; + _listMetrics: ListMetricsAggregator = new ListMetricsAggregator(); _footerLength = 0; // Used for preventing scrollToIndex from being called multiple times for initialScrollIndex _hasTriggeredInitialScrollToIndex = false; @@ -1242,7 +1245,6 @@ class VirtualizedList extends StateSafePureComponent { _hasWarned: {[string]: boolean} = {}; _headerLength = 0; _hiPriInProgress: boolean = false; // flag to prevent infinite hiPri cell limit update - _highestMeasuredFrameIndex = 0; _indicesToKeys: Map = new Map(); _lastFocusedCellKey: ?string = null; _nestedChildLists: ChildListCollection = @@ -1263,8 +1265,6 @@ class VirtualizedList extends StateSafePureComponent { _scrollRef: ?React.ElementRef = null; _sentStartForContentLength = 0; _sentEndForContentLength = 0; - _totalCellLength = 0; - _totalCellsMeasured = 0; _updateCellsToRenderBatcher: Batchinator; _viewabilityTuples: Array = []; @@ -1324,31 +1324,17 @@ class VirtualizedList extends StateSafePureComponent { _onCellLayout = (e: LayoutEvent, cellKey: string, index: number): void => { const layout = e.nativeEvent.layout; - const next = { - offset: this._selectOffset(layout), - length: this._selectLength(layout), + const offset = this._selectOffset(layout); + const length = this._selectLength(layout); + + const layoutHasChanged = this._listMetrics.notifyCellLayout( + cellKey, index, - inLayout: true, - }; - const curr = this._frames[cellKey]; - if ( - !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, - ); + length, + offset, + ); + if (layoutHasChanged) { this._scheduleCellsToRenderUpdate(); - } else { - this._frames[cellKey].inLayout = true; } this._triggerRemeasureForChildListsInCell(cellKey); @@ -1364,10 +1350,7 @@ class VirtualizedList extends StateSafePureComponent { _onCellUnmount = (cellKey: string) => { delete this._cellRefs[cellKey]; - const curr = this._frames[cellKey]; - if (curr) { - this._frames[cellKey] = {...curr, inLayout: false}; - } + this._listMetrics.notifyCellUnmounted(cellKey); }; _triggerRemeasureForChildListsInCell(cellKey: string): void { @@ -1467,19 +1450,16 @@ class VirtualizedList extends StateSafePureComponent { const framesInLayout = []; const itemCount = this.props.getItemCount(this.props.data); for (let ii = 0; ii < itemCount; ii++) { - const frame = this.__getCellMetricsApprox(ii, this.props); - /* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment - * suppresses an error found when Flow v0.68 was deployed. To see the - * error delete this comment and run Flow. */ - if (frame.inLayout) { + const frame = this._listMetrics.getCellMetricsApprox(ii, this.props); + if (frame.isMounted) { framesInLayout.push(frame); } } - const windowTop = this.__getCellMetricsApprox( + const windowTop = this._listMetrics.getCellMetricsApprox( this.state.cellsAroundViewport.first, this.props, ).offset; - const frameLast = this.__getCellMetricsApprox( + const frameLast = this._listMetrics.getCellMetricsApprox( this.state.cellsAroundViewport.last, this.props, ); @@ -1774,7 +1754,8 @@ class VirtualizedList extends StateSafePureComponent { // But only if there are items before the first rendered item if (first > 0) { const distTop = - offset - this.__getCellMetricsApprox(first, this.props).offset; + offset - + this._listMetrics.getCellMetricsApprox(first, this.props).offset; hiPri = distTop < 0 || (velocity < -2 && @@ -1785,7 +1766,7 @@ class VirtualizedList extends StateSafePureComponent { // But only if there are items after the last rendered item if (!hiPri && last >= 0 && last < itemCount - 1) { const distBottom = - this.__getCellMetricsApprox(last, this.props).offset - + this._listMetrics.getCellMetricsApprox(last, this.props).offset - (offset + visibleLength); hiPri = distBottom < 0 || @@ -1802,7 +1783,7 @@ class VirtualizedList extends StateSafePureComponent { // We shouldn't do another hipri cellToRenderUpdate if ( hiPri && - (this._averageCellLength || this.props.getItemLayout) && + (this._listMetrics.getAverageCellLength() || this.props.getItemLayout) && !this._hiPriInProgress ) { this._hiPriInProgress = true; @@ -1898,75 +1879,9 @@ class VirtualizedList extends StateSafePureComponent { }; }; - /** - * Gets an approximate offset to an item at a given index. Supports - * fractional indices. - */ - _getOffsetApprox = (index: number, props: CellMetricProps): number => { - if (Number.isInteger(index)) { - return this.__getCellMetricsApprox(index, props).offset; - } else { - const CellMetrics = this.__getCellMetricsApprox(Math.floor(index), props); - const remainder = index - Math.floor(index); - return CellMetrics.offset + remainder * CellMetrics.length; - } - }; - - __getCellMetricsApprox: ( - index: number, - props: CellMetricProps, - ) => { - length: number, - offset: number, - ... - } = (index, props) => { - const frame = this._getCellMetrics(index, props); - if (frame && frame.index === index) { - // check for invalid frames due to row re-ordering - return frame; - } else { - const {data, getItemCount, getItemLayout} = props; - 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', - ); - return { - length: this._averageCellLength, - offset: this._averageCellLength * index, - }; - } - }; - - _getCellMetrics = ( - index: number, - props: CellMetricProps, - ): ?{ - length: number, - offset: number, - index: number, - inLayout?: boolean, - ... - } => { - const {data, getItemCount, getItemLayout} = props; - invariant( - index >= 0 && index < getItemCount(data), - 'Tried to get frame for out of range index ' + index, - ); - const frame = this._frames[VirtualizedList._getItemKey(props, index)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment - * suppresses an error found when Flow v0.63 was deployed. To see the error - * delete this comment and run Flow. */ - return getItemLayout(data, index); - } - } - return frame; - }; + __getListMetrics(): ListMetricsAggregator { + return this._listMetrics; + } _getNonViewportRenderRegions = ( props: CellMetricProps, @@ -2005,7 +1920,7 @@ class VirtualizedList extends StateSafePureComponent { i-- ) { first--; - heightOfCellsBeforeFocused += this.__getCellMetricsApprox( + heightOfCellsBeforeFocused += this._listMetrics.getCellMetricsApprox( i, props, ).length; @@ -2020,7 +1935,10 @@ class VirtualizedList extends StateSafePureComponent { i++ ) { last++; - heightOfCellsAfterFocused += this.__getCellMetricsApprox(i, props).length; + heightOfCellsAfterFocused += this._listMetrics.getCellMetricsApprox( + i, + props, + ).length; } return [{first, last}]; @@ -2040,7 +1958,7 @@ class VirtualizedList extends StateSafePureComponent { props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, - this._getCellMetrics, + this._listMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport, diff --git a/packages/virtualized-lists/Lists/VirtualizedListProps.js b/packages/virtualized-lists/Lists/VirtualizedListProps.js index 1988ef753bb508..d1ec579b3a09df 100644 --- a/packages/virtualized-lists/Lists/VirtualizedListProps.js +++ b/packages/virtualized-lists/Lists/VirtualizedListProps.js @@ -292,15 +292,3 @@ export type Props = {| ...RequiredProps, ...OptionalProps, |}; - -/** - * Subset of properties needed to calculate frame metrics - */ -export type CellMetricProps = { - data: RequiredProps['data'], - getItemCount: RequiredProps['getItemCount'], - getItem: RequiredProps['getItem'], - getItemLayout?: OptionalProps['getItemLayout'], - keyExtractor?: OptionalProps['keyExtractor'], - ... -}; diff --git a/packages/virtualized-lists/Lists/VirtualizedSectionList.js b/packages/virtualized-lists/Lists/VirtualizedSectionList.js index b5bbd84597e88f..0f6b2a360b33bf 100644 --- a/packages/virtualized-lists/Lists/VirtualizedSectionList.js +++ b/packages/virtualized-lists/Lists/VirtualizedSectionList.js @@ -138,11 +138,11 @@ class VirtualizedSectionList< if (this._listRef == null) { return; } + const listRef = this._listRef; if (params.itemIndex > 0 && this.props.stickySectionHeadersEnabled) { - const frame = this._listRef.__getCellMetricsApprox( - index - params.itemIndex, - this._listRef.props, - ); + const frame = listRef + .__getListMetrics() + .getCellMetricsApprox(index - params.itemIndex, listRef.props); viewOffset += frame.length; } const toIndexParams = { diff --git a/packages/virtualized-lists/Lists/__tests__/FillRateHelper-test.js b/packages/virtualized-lists/Lists/__tests__/FillRateHelper-test.js index 3f29f3fff849b4..58d47ba68d8c90 100644 --- a/packages/virtualized-lists/Lists/__tests__/FillRateHelper-test.js +++ b/packages/virtualized-lists/Lists/__tests__/FillRateHelper-test.js @@ -23,7 +23,7 @@ const dataGlobal = [ ]; function getCellMetrics(index: number) { const frame = rowFramesGlobal[dataGlobal[index].key]; - return {length: frame.height, offset: frame.y, inLayout: frame.inLayout}; + return {length: frame.height, offset: frame.y, isMounted: frame.isMounted}; } function computeResult({helper, props, state, scroll}): number { @@ -47,11 +47,11 @@ describe('computeBlankness', function () { }); it('computes correct blankness of viewport', function () { - const helper = new FillRateHelper(getCellMetrics); + const helper = new FillRateHelper({getCellMetrics}); rowFramesGlobal = { - header: {y: 0, height: 0, inLayout: true}, - a: {y: 0, height: 50, inLayout: true}, - b: {y: 50, height: 50, inLayout: true}, + header: {y: 0, height: 0, isMounted: true}, + a: {y: 0, height: 50, isMounted: true}, + b: {y: 50, height: 50, isMounted: true}, }; let blankness = computeResult({helper}); expect(blankness).toBe(0); @@ -66,32 +66,32 @@ describe('computeBlankness', function () { }); it('skips frames that are not in layout', function () { - const helper = new FillRateHelper(getCellMetrics); + const helper = new FillRateHelper({getCellMetrics}); rowFramesGlobal = { - header: {y: 0, height: 0, inLayout: false}, - a: {y: 0, height: 10, inLayout: false}, - b: {y: 10, height: 30, inLayout: true}, - c: {y: 40, height: 40, inLayout: true}, - d: {y: 80, height: 20, inLayout: false}, - footer: {y: 100, height: 0, inLayout: false}, + header: {y: 0, height: 0, isMounted: false}, + a: {y: 0, height: 10, isMounted: false}, + b: {y: 10, height: 30, isMounted: true}, + c: {y: 40, height: 40, isMounted: true}, + d: {y: 80, height: 20, isMounted: false}, + footer: {y: 100, height: 0, isMounted: false}, }; const blankness = computeResult({helper, state: {last: 4}}); expect(blankness).toBe(0.3); }); it('sampling rate can disable', function () { - let helper = new FillRateHelper(getCellMetrics); + let helper = new FillRateHelper({getCellMetrics}); rowFramesGlobal = { - header: {y: 0, height: 0, inLayout: true}, - a: {y: 0, height: 40, inLayout: true}, - b: {y: 40, height: 40, inLayout: true}, + header: {y: 0, height: 0, isMounted: true}, + a: {y: 0, height: 40, isMounted: true}, + b: {y: 40, height: 40, isMounted: true}, }; let blankness = computeResult({helper}); expect(blankness).toBe(0.2); FillRateHelper.setSampleRate(0); - helper = new FillRateHelper(getCellMetrics); + helper = new FillRateHelper({getCellMetrics}); blankness = computeResult({helper}); expect(blankness).toBe(0); }); @@ -102,11 +102,11 @@ describe('computeBlankness', function () { FillRateHelper.addListener(listener), ); subscriptions[1].remove(); - const helper = new FillRateHelper(getCellMetrics); + const helper = new FillRateHelper({getCellMetrics}); rowFramesGlobal = { - header: {y: 0, height: 0, inLayout: true}, - a: {y: 0, height: 40, inLayout: true}, - b: {y: 40, height: 40, inLayout: true}, + header: {y: 0, height: 0, isMounted: true}, + a: {y: 0, height: 40, isMounted: true}, + b: {y: 40, height: 40, isMounted: true}, }; const blankness = computeResult({helper}); expect(blankness).toBe(0.2); diff --git a/packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js b/packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js index 2737c2a298423e..887583a6d6382e 100644 --- a/packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js +++ b/packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js @@ -38,7 +38,7 @@ describe('computeViewableItems', function () { d: {y: 150, height: 50}, }; data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}]; - expect(helper.computeViewableItems(props, 0, 200, getCellMetrics)).toEqual([ + expect(helper.computeViewableItems(props, 0, 200, {getCellMetrics})).toEqual([ 0, 1, 2, 3, ]); }); @@ -54,7 +54,7 @@ describe('computeViewableItems', function () { d: {y: 250, height: 50}, }; data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}]; - expect(helper.computeViewableItems(props, 0, 200, getCellMetrics)).toEqual([ + expect(helper.computeViewableItems(props, 0, 200, {getCellMetrics})).toEqual([ 0, 1, ]); }); @@ -70,7 +70,7 @@ describe('computeViewableItems', function () { d: {y: 250, height: 50}, }; data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}]; - expect(helper.computeViewableItems(props, 25, 200, getCellMetrics)).toEqual( + expect(helper.computeViewableItems(props, 25, 200, {getCellMetrics})).toEqual( [1], ); }); @@ -81,7 +81,7 @@ describe('computeViewableItems', function () { }); rowFrames = {}; data = []; - expect(helper.computeViewableItems(props, 0, 200, getCellMetrics)).toEqual( + expect(helper.computeViewableItems(props, 0, 200, {getCellMetrics})).toEqual( [], ); }); @@ -96,38 +96,38 @@ describe('computeViewableItems', function () { data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}]; let helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 0}); - expect(helper.computeViewableItems(props, 0, 50, getCellMetrics)).toEqual([ + expect(helper.computeViewableItems(props, 0, 50, {getCellMetrics})).toEqual([ 0, ]); - expect(helper.computeViewableItems(props, 1, 50, getCellMetrics)).toEqual([ + expect(helper.computeViewableItems(props, 1, 50, {getCellMetrics})).toEqual([ 0, 1, ]); - expect(helper.computeViewableItems(props, 199, 50, getCellMetrics)).toEqual( + expect(helper.computeViewableItems(props, 199, 50, {getCellMetrics})).toEqual( [1, 2], ); - expect(helper.computeViewableItems(props, 250, 50, getCellMetrics)).toEqual( + expect(helper.computeViewableItems(props, 250, 50, {getCellMetrics})).toEqual( [2], ); helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 100}); - expect(helper.computeViewableItems(props, 0, 200, getCellMetrics)).toEqual([ + expect(helper.computeViewableItems(props, 0, 200, {getCellMetrics})).toEqual([ 0, 1, ]); - expect(helper.computeViewableItems(props, 1, 200, getCellMetrics)).toEqual([ + expect(helper.computeViewableItems(props, 1, 200, {getCellMetrics})).toEqual([ 1, ]); expect( - helper.computeViewableItems(props, 400, 200, getCellMetrics), + helper.computeViewableItems(props, 400, 200, {getCellMetrics}), ).toEqual([2]); expect( - helper.computeViewableItems(props, 600, 200, getCellMetrics), + helper.computeViewableItems(props, 600, 200, {getCellMetrics}), ).toEqual([3]); helper = new ViewabilityHelper({viewAreaCoveragePercentThreshold: 10}); - expect(helper.computeViewableItems(props, 30, 200, getCellMetrics)).toEqual( + expect(helper.computeViewableItems(props, 30, 200, {getCellMetrics})).toEqual( [0, 1, 2], ); - expect(helper.computeViewableItems(props, 31, 200, getCellMetrics)).toEqual( + expect(helper.computeViewableItems(props, 31, 200, {getCellMetrics})).toEqual( [1, 2], ); }); @@ -141,29 +141,29 @@ describe('computeViewableItems', function () { }; data = [{key: 'a'}, {key: 'b'}, {key: 'c'}, {key: 'd'}]; let helper = new ViewabilityHelper({itemVisiblePercentThreshold: 0}); - expect(helper.computeViewableItems(props, 0, 50, getCellMetrics)).toEqual([ + expect(helper.computeViewableItems(props, 0, 50, {getCellMetrics})).toEqual([ 0, ]); - expect(helper.computeViewableItems(props, 1, 50, getCellMetrics)).toEqual([ + expect(helper.computeViewableItems(props, 1, 50, {getCellMetrics})).toEqual([ 0, 1, ]); helper = new ViewabilityHelper({itemVisiblePercentThreshold: 100}); - expect(helper.computeViewableItems(props, 0, 250, getCellMetrics)).toEqual([ + expect(helper.computeViewableItems(props, 0, 250, {getCellMetrics})).toEqual([ 0, 1, 2, ]); - expect(helper.computeViewableItems(props, 1, 250, getCellMetrics)).toEqual([ + expect(helper.computeViewableItems(props, 1, 250, {getCellMetrics})).toEqual([ 1, 2, ]); helper = new ViewabilityHelper({itemVisiblePercentThreshold: 10}); - expect(helper.computeViewableItems(props, 184, 20, getCellMetrics)).toEqual( + expect(helper.computeViewableItems(props, 184, 20, {getCellMetrics})).toEqual( [1], ); - expect(helper.computeViewableItems(props, 185, 20, getCellMetrics)).toEqual( + expect(helper.computeViewableItems(props, 185, 20, {getCellMetrics})).toEqual( [1, 2], ); - expect(helper.computeViewableItems(props, 186, 20, getCellMetrics)).toEqual( + expect(helper.computeViewableItems(props, 186, 20, {getCellMetrics})).toEqual( [2], ); }); @@ -181,7 +181,7 @@ describe('onUpdate', function () { props, 0, 200, - getCellMetrics, + {getCellMetrics}, createViewToken, onViewableItemsChanged, ); @@ -195,7 +195,7 @@ describe('onUpdate', function () { props, 0, 200, - getCellMetrics, + {getCellMetrics}, createViewToken, onViewableItemsChanged, ); @@ -204,7 +204,7 @@ describe('onUpdate', function () { props, 100, 200, - getCellMetrics, + {getCellMetrics}, createViewToken, onViewableItemsChanged, ); @@ -228,7 +228,7 @@ describe('onUpdate', function () { props, 0, 200, - getCellMetrics, + {getCellMetrics}, createViewToken, onViewableItemsChanged, ); @@ -242,7 +242,7 @@ describe('onUpdate', function () { props, 100, 200, - getCellMetrics, + {getCellMetrics}, createViewToken, onViewableItemsChanged, ); @@ -260,7 +260,7 @@ describe('onUpdate', function () { props, 200, 200, - getCellMetrics, + {getCellMetrics}, createViewToken, onViewableItemsChanged, ); @@ -287,7 +287,7 @@ describe('onUpdate', function () { props, 0, 200, - getCellMetrics, + {getCellMetrics}, createViewToken, onViewableItemsChanged, ); @@ -321,7 +321,7 @@ describe('onUpdate', function () { props, 0, 200, - getCellMetrics, + {getCellMetrics}, createViewToken, onViewableItemsChanged, ); @@ -329,7 +329,7 @@ describe('onUpdate', function () { props, 300, // scroll past item 'a' 200, - getCellMetrics, + {getCellMetrics}, createViewToken, onViewableItemsChanged, ); @@ -362,7 +362,7 @@ describe('onUpdate', function () { props, 0, 100, - getCellMetrics, + {getCellMetrics}, createViewToken, onViewableItemsChanged, ); @@ -374,7 +374,7 @@ describe('onUpdate', function () { props, 20, 100, - getCellMetrics, + {getCellMetrics}, createViewToken, onViewableItemsChanged, ); @@ -401,7 +401,7 @@ describe('onUpdate', function () { props, 0, 200, - getCellMetrics, + {getCellMetrics}, createViewToken, onViewableItemsChanged, ); @@ -426,7 +426,7 @@ describe('onUpdate', function () { props, 0, 200, - getCellMetrics, + {getCellMetrics}, createViewToken, onViewableItemsChanged, ); diff --git a/packages/virtualized-lists/Lists/__tests__/VirtualizeUtils-test.js b/packages/virtualized-lists/Lists/__tests__/VirtualizeUtils-test.js index da59c0764a8a65..9af659dac5ba9d 100644 --- a/packages/virtualized-lists/Lists/__tests__/VirtualizeUtils-test.js +++ b/packages/virtualized-lists/Lists/__tests__/VirtualizeUtils-test.js @@ -42,14 +42,19 @@ describe('newRangeCount', function () { describe('elementsThatOverlapOffsets', function () { it('handles fixed length', function () { const offsets = [0, 250, 350, 450]; - function getCellMetrics(index: number) { + function getCellMetricsApprox(index: number) { return { length: 100, offset: 100 * index, }; } expect( - elementsThatOverlapOffsets(offsets, fakeProps(100), getCellMetrics, 1), + elementsThatOverlapOffsets( + offsets, + fakeProps(100), + {getCellMetricsApprox}, + 1, + ), ).toEqual([0, 2, 3, 4]); }); it('handles variable length', function () { @@ -65,21 +70,26 @@ describe('elementsThatOverlapOffsets', function () { elementsThatOverlapOffsets( offsets, fakeProps(frames.length), - ii => frames[ii], + {getCellMetricsApprox: ii => frames[ii]}, 1, ), ).toEqual([1, 1, 3]); }); it('handles frame boundaries', function () { const offsets = [0, 100, 200, 300]; - function getCellMetrics(index: number) { + function getCellMetricsApprox(index: number) { return { length: 100, offset: 100 * index, }; } expect( - elementsThatOverlapOffsets(offsets, fakeProps(100), getCellMetrics, 1), + elementsThatOverlapOffsets( + offsets, + fakeProps(100), + {getCellMetricsApprox}, + 1, + ), ).toEqual([0, 0, 1, 2]); }); it('handles out of bounds', function () { @@ -93,7 +103,7 @@ describe('elementsThatOverlapOffsets', function () { elementsThatOverlapOffsets( offsets, fakeProps(frames.length), - ii => frames[ii], + {getCellMetricsApprox: ii => frames[ii]}, 1, ), ).toEqual([undefined, 1]);