mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Add proper support for fractional scrollIndex in VirtualizedList
Summary: Non-integer `initialScrollIndex` or values to `scrollToIndex` would produce a reasonable result, with the caveat that it always falls back to layout estimation (will only be correct when all items are the same size), and breaks if getItemLayout() is supplied. It has usage though, so this diff adds proper support for non-integer scrollIndex, to offset a given amount into the length of the specific cell. This overlaps a bit with the optional `viewOffset` and `viewPosition` arguments in `scrollToIndex`, but there isn't really the equivalent API for `initialScrollIndex`. Changelog: [General][Added]- Add proper support for fractional scrollIndex in VirtualizedList Reviewed By: yungsters Differential Revision: D39271100 fbshipit-source-id: 4d93887eed4497e9f6abcd1a6117ac7fdaebf2b1
This commit is contained in:
committed by
Facebook GitHub Bot
parent
b6bf1fd373
commit
f44dfef923
@@ -235,11 +235,11 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
|
||||
});
|
||||
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<Props, State> {
|
||||
|
||||
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<Props, State> {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
@@ -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(
|
||||
<VirtualizedList
|
||||
initialScrollIndex={1}
|
||||
initialNumToRender={4}
|
||||
ref={listRef}
|
||||
{...baseItemProps(items)}
|
||||
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<VirtualizedList
|
||||
initialScrollIndex={0.0001}
|
||||
initialNumToRender={4}
|
||||
ref={listRef}
|
||||
{...baseItemProps(items)}
|
||||
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<VirtualizedList
|
||||
initialScrollIndex={9.9999}
|
||||
initialNumToRender={4}
|
||||
ref={listRef}
|
||||
{...baseItemProps(items)}
|
||||
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<VirtualizedList
|
||||
initialScrollIndex={1.5}
|
||||
initialNumToRender={4}
|
||||
ref={listRef}
|
||||
getItemLayout={getItemLayout}
|
||||
{...baseItemProps(items)}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<VirtualizedList
|
||||
initialScrollIndex={1.5}
|
||||
initialNumToRender={4}
|
||||
ref={listRef}
|
||||
{...baseItemProps(items)}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<VirtualizedList
|
||||
initialScrollIndex={1.5}
|
||||
initialNumToRender={4}
|
||||
ref={listRef}
|
||||
{...baseItemProps(items)}
|
||||
/>,
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user