diff --git a/Libraries/Lists/VirtualizedList_EXPERIMENTAL.js b/Libraries/Lists/VirtualizedList_EXPERIMENTAL.js index ebe059a01bd..b54c238d1e8 100644 --- a/Libraries/Lists/VirtualizedList_EXPERIMENTAL.js +++ b/Libraries/Lists/VirtualizedList_EXPERIMENTAL.js @@ -235,11 +235,11 @@ class VirtualizedList extends StateSafePureComponent { }); return; } - const frame = this.__getFrameMetricsApprox(index, this.props); + const frame = this.__getFrameMetricsApprox(Math.floor(index), this.props); const offset = Math.max( 0, - frame.offset - + this._getOffsetApprox(index, this.props) - (viewPosition || 0) * (this._scrollMetrics.visibleLength - frame.length), ) - (viewOffset || 0); @@ -564,7 +564,7 @@ class VirtualizedList extends StateSafePureComponent { static _initialRenderRegion(props: Props): {first: number, last: number} { const itemCount = props.getItemCount(props.data); - const scrollIndex = Math.max(0, props.initialScrollIndex ?? 0); + const scrollIndex = Math.floor(Math.max(0, props.initialScrollIndex ?? 0)); return { first: scrollIndex, @@ -1780,6 +1780,23 @@ class VirtualizedList extends StateSafePureComponent { }; }; + /** + * Gets an approximate offset to an item at a given index. Supports + * fractional indices. + */ + _getOffsetApprox = (index: number, props: FrameMetricProps): number => { + if (Number.isInteger(index)) { + return this.__getFrameMetricsApprox(index, props).offset; + } else { + const frameMetrics = this.__getFrameMetricsApprox( + Math.floor(index), + props, + ); + const remainder = index - Math.floor(index); + return frameMetrics.offset + remainder * frameMetrics.length; + } + }; + __getFrameMetricsApprox: ( index: number, props: FrameMetricProps, diff --git a/Libraries/Lists/__tests__/VirtualizedList-test.js b/Libraries/Lists/__tests__/VirtualizedList-test.js index 0928fd57c98..08be48906c6 100644 --- a/Libraries/Lists/__tests__/VirtualizedList-test.js +++ b/Libraries/Lists/__tests__/VirtualizedList-test.js @@ -736,6 +736,213 @@ it('renders offset cells in initial render when initialScrollIndex set', () => { expect(component).toMatchSnapshot(); }); +it('scrolls after content sizing with integer initialScrollIndex', () => { + const items = generateItems(10); + const ITEM_HEIGHT = 10; + + const listRef = React.createRef(null); + + const component = ReactTestRenderer.create( + , + ); + + const {scrollTo} = listRef.current.getScrollRef(); + + ReactTestRenderer.act(() => { + simulateLayout(component, { + viewport: {width: 10, height: 50}, + content: {width: 10, height: 200}, + }); + performAllBatches(); + }); + + expect(scrollTo).toHaveBeenLastCalledWith({y: 10, animated: false}); +}); + +it('scrolls after content sizing with near-zero initialScrollIndex', () => { + const items = generateItems(10); + const ITEM_HEIGHT = 10; + + const listRef = React.createRef(null); + + const component = ReactTestRenderer.create( + , + ); + + const {scrollTo} = listRef.current.getScrollRef(); + + ReactTestRenderer.act(() => { + simulateLayout(component, { + viewport: {width: 10, height: 50}, + content: {width: 10, height: 200}, + }); + performAllBatches(); + }); + + expect(scrollTo).toHaveBeenLastCalledWith({y: 0.001, animated: false}); +}); + +it('scrolls after content sizing with near-end initialScrollIndex', () => { + const items = generateItems(10); + const ITEM_HEIGHT = 10; + + const listRef = React.createRef(null); + + const component = ReactTestRenderer.create( + , + ); + + const {scrollTo} = listRef.current.getScrollRef(); + + ReactTestRenderer.act(() => { + simulateLayout(component, { + viewport: {width: 10, height: 50}, + content: {width: 10, height: 200}, + }); + performAllBatches(); + }); + + expect(scrollTo).toHaveBeenLastCalledWith({y: 99.999, animated: false}); +}); + +it('scrolls after content sizing with fractional initialScrollIndex (getItemLayout())', () => { + const items = generateItems(10); + const itemHeights = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const getItemLayout = (_, index) => ({ + length: itemHeights[index], + offset: itemHeights.slice(0, index).reduce((a, b) => a + b, 0), + index, + }); + + const listRef = React.createRef(null); + + const component = ReactTestRenderer.create( + , + ); + + const {scrollTo} = listRef.current.getScrollRef(); + + ReactTestRenderer.act(() => { + simulateLayout(component, { + viewport: {width: 10, height: 50}, + content: {width: 10, height: 200}, + }); + performAllBatches(); + }); + + if (useExperimentalList) { + expect(scrollTo).toHaveBeenLastCalledWith({y: 2.0, animated: false}); + } else { + // Legacy incorrect results + expect(scrollTo).toHaveBeenLastCalledWith({y: Number.NaN, animated: false}); + } +}); + +it('scrolls after content sizing with fractional initialScrollIndex (cached layout)', () => { + const items = generateItems(10); + const listRef = React.createRef(null); + + const component = ReactTestRenderer.create( + , + ); + + const {scrollTo} = listRef.current.getScrollRef(); + + ReactTestRenderer.act(() => { + let y = 0; + for (let i = 0; i < 10; ++i) { + const height = i + 1; + simulateCellLayout(component, items, i, { + width: 10, + height, + x: 0, + y, + }); + y += height; + } + + simulateLayout(component, { + viewport: {width: 10, height: 50}, + content: {width: 10, height: 200}, + }); + performAllBatches(); + }); + + if (useExperimentalList) { + expect(scrollTo).toHaveBeenLastCalledWith({y: 2.0, animated: false}); + } else { + // Legacy incorrect results + expect(scrollTo).toHaveBeenLastCalledWith({y: 8.25, animated: false}); + } +}); + +it('scrolls after content sizing with fractional initialScrollIndex (layout estimation)', () => { + const items = generateItems(10); + const listRef = React.createRef(null); + + const component = ReactTestRenderer.create( + , + ); + + const {scrollTo} = listRef.current.getScrollRef(); + + ReactTestRenderer.act(() => { + let y = 0; + for (let i = 5; i < 10; ++i) { + const height = i + 1; + simulateCellLayout(component, items, i, { + width: 10, + height, + x: 0, + y, + }); + y += height; + } + + simulateLayout(component, { + viewport: {width: 10, height: 50}, + content: {width: 10, height: 200}, + }); + performAllBatches(); + }); + + expect(scrollTo).toHaveBeenLastCalledWith({y: 12, animated: false}); +}); + it('initially renders nothing when initialNumToRender is 0', () => { const items = generateItems(10); const ITEM_HEIGHT = 10;