Skip to content

Commit f44dfef

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
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
1 parent b6bf1fd commit f44dfef

File tree

2 files changed

+227
-3
lines changed

2 files changed

+227
-3
lines changed

Libraries/Lists/VirtualizedList_EXPERIMENTAL.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,11 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
235235
});
236236
return;
237237
}
238-
const frame = this.__getFrameMetricsApprox(index, this.props);
238+
const frame = this.__getFrameMetricsApprox(Math.floor(index), this.props);
239239
const offset =
240240
Math.max(
241241
0,
242-
frame.offset -
242+
this._getOffsetApprox(index, this.props) -
243243
(viewPosition || 0) *
244244
(this._scrollMetrics.visibleLength - frame.length),
245245
) - (viewOffset || 0);
@@ -564,7 +564,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
564564

565565
static _initialRenderRegion(props: Props): {first: number, last: number} {
566566
const itemCount = props.getItemCount(props.data);
567-
const scrollIndex = Math.max(0, props.initialScrollIndex ?? 0);
567+
const scrollIndex = Math.floor(Math.max(0, props.initialScrollIndex ?? 0));
568568

569569
return {
570570
first: scrollIndex,
@@ -1780,6 +1780,23 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
17801780
};
17811781
};
17821782

1783+
/**
1784+
* Gets an approximate offset to an item at a given index. Supports
1785+
* fractional indices.
1786+
*/
1787+
_getOffsetApprox = (index: number, props: FrameMetricProps): number => {
1788+
if (Number.isInteger(index)) {
1789+
return this.__getFrameMetricsApprox(index, props).offset;
1790+
} else {
1791+
const frameMetrics = this.__getFrameMetricsApprox(
1792+
Math.floor(index),
1793+
props,
1794+
);
1795+
const remainder = index - Math.floor(index);
1796+
return frameMetrics.offset + remainder * frameMetrics.length;
1797+
}
1798+
};
1799+
17831800
__getFrameMetricsApprox: (
17841801
index: number,
17851802
props: FrameMetricProps,

Libraries/Lists/__tests__/VirtualizedList-test.js

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,213 @@ it('renders offset cells in initial render when initialScrollIndex set', () => {
736736
expect(component).toMatchSnapshot();
737737
});
738738

739+
it('scrolls after content sizing with integer initialScrollIndex', () => {
740+
const items = generateItems(10);
741+
const ITEM_HEIGHT = 10;
742+
743+
const listRef = React.createRef(null);
744+
745+
const component = ReactTestRenderer.create(
746+
<VirtualizedList
747+
initialScrollIndex={1}
748+
initialNumToRender={4}
749+
ref={listRef}
750+
{...baseItemProps(items)}
751+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
752+
/>,
753+
);
754+
755+
const {scrollTo} = listRef.current.getScrollRef();
756+
757+
ReactTestRenderer.act(() => {
758+
simulateLayout(component, {
759+
viewport: {width: 10, height: 50},
760+
content: {width: 10, height: 200},
761+
});
762+
performAllBatches();
763+
});
764+
765+
expect(scrollTo).toHaveBeenLastCalledWith({y: 10, animated: false});
766+
});
767+
768+
it('scrolls after content sizing with near-zero initialScrollIndex', () => {
769+
const items = generateItems(10);
770+
const ITEM_HEIGHT = 10;
771+
772+
const listRef = React.createRef(null);
773+
774+
const component = ReactTestRenderer.create(
775+
<VirtualizedList
776+
initialScrollIndex={0.0001}
777+
initialNumToRender={4}
778+
ref={listRef}
779+
{...baseItemProps(items)}
780+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
781+
/>,
782+
);
783+
784+
const {scrollTo} = listRef.current.getScrollRef();
785+
786+
ReactTestRenderer.act(() => {
787+
simulateLayout(component, {
788+
viewport: {width: 10, height: 50},
789+
content: {width: 10, height: 200},
790+
});
791+
performAllBatches();
792+
});
793+
794+
expect(scrollTo).toHaveBeenLastCalledWith({y: 0.001, animated: false});
795+
});
796+
797+
it('scrolls after content sizing with near-end initialScrollIndex', () => {
798+
const items = generateItems(10);
799+
const ITEM_HEIGHT = 10;
800+
801+
const listRef = React.createRef(null);
802+
803+
const component = ReactTestRenderer.create(
804+
<VirtualizedList
805+
initialScrollIndex={9.9999}
806+
initialNumToRender={4}
807+
ref={listRef}
808+
{...baseItemProps(items)}
809+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
810+
/>,
811+
);
812+
813+
const {scrollTo} = listRef.current.getScrollRef();
814+
815+
ReactTestRenderer.act(() => {
816+
simulateLayout(component, {
817+
viewport: {width: 10, height: 50},
818+
content: {width: 10, height: 200},
819+
});
820+
performAllBatches();
821+
});
822+
823+
expect(scrollTo).toHaveBeenLastCalledWith({y: 99.999, animated: false});
824+
});
825+
826+
it('scrolls after content sizing with fractional initialScrollIndex (getItemLayout())', () => {
827+
const items = generateItems(10);
828+
const itemHeights = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
829+
const getItemLayout = (_, index) => ({
830+
length: itemHeights[index],
831+
offset: itemHeights.slice(0, index).reduce((a, b) => a + b, 0),
832+
index,
833+
});
834+
835+
const listRef = React.createRef(null);
836+
837+
const component = ReactTestRenderer.create(
838+
<VirtualizedList
839+
initialScrollIndex={1.5}
840+
initialNumToRender={4}
841+
ref={listRef}
842+
getItemLayout={getItemLayout}
843+
{...baseItemProps(items)}
844+
/>,
845+
);
846+
847+
const {scrollTo} = listRef.current.getScrollRef();
848+
849+
ReactTestRenderer.act(() => {
850+
simulateLayout(component, {
851+
viewport: {width: 10, height: 50},
852+
content: {width: 10, height: 200},
853+
});
854+
performAllBatches();
855+
});
856+
857+
if (useExperimentalList) {
858+
expect(scrollTo).toHaveBeenLastCalledWith({y: 2.0, animated: false});
859+
} else {
860+
// Legacy incorrect results
861+
expect(scrollTo).toHaveBeenLastCalledWith({y: Number.NaN, animated: false});
862+
}
863+
});
864+
865+
it('scrolls after content sizing with fractional initialScrollIndex (cached layout)', () => {
866+
const items = generateItems(10);
867+
const listRef = React.createRef(null);
868+
869+
const component = ReactTestRenderer.create(
870+
<VirtualizedList
871+
initialScrollIndex={1.5}
872+
initialNumToRender={4}
873+
ref={listRef}
874+
{...baseItemProps(items)}
875+
/>,
876+
);
877+
878+
const {scrollTo} = listRef.current.getScrollRef();
879+
880+
ReactTestRenderer.act(() => {
881+
let y = 0;
882+
for (let i = 0; i < 10; ++i) {
883+
const height = i + 1;
884+
simulateCellLayout(component, items, i, {
885+
width: 10,
886+
height,
887+
x: 0,
888+
y,
889+
});
890+
y += height;
891+
}
892+
893+
simulateLayout(component, {
894+
viewport: {width: 10, height: 50},
895+
content: {width: 10, height: 200},
896+
});
897+
performAllBatches();
898+
});
899+
900+
if (useExperimentalList) {
901+
expect(scrollTo).toHaveBeenLastCalledWith({y: 2.0, animated: false});
902+
} else {
903+
// Legacy incorrect results
904+
expect(scrollTo).toHaveBeenLastCalledWith({y: 8.25, animated: false});
905+
}
906+
});
907+
908+
it('scrolls after content sizing with fractional initialScrollIndex (layout estimation)', () => {
909+
const items = generateItems(10);
910+
const listRef = React.createRef(null);
911+
912+
const component = ReactTestRenderer.create(
913+
<VirtualizedList
914+
initialScrollIndex={1.5}
915+
initialNumToRender={4}
916+
ref={listRef}
917+
{...baseItemProps(items)}
918+
/>,
919+
);
920+
921+
const {scrollTo} = listRef.current.getScrollRef();
922+
923+
ReactTestRenderer.act(() => {
924+
let y = 0;
925+
for (let i = 5; i < 10; ++i) {
926+
const height = i + 1;
927+
simulateCellLayout(component, items, i, {
928+
width: 10,
929+
height,
930+
x: 0,
931+
y,
932+
});
933+
y += height;
934+
}
935+
936+
simulateLayout(component, {
937+
viewport: {width: 10, height: 50},
938+
content: {width: 10, height: 200},
939+
});
940+
performAllBatches();
941+
});
942+
943+
expect(scrollTo).toHaveBeenLastCalledWith({y: 12, animated: false});
944+
});
945+
739946
it('initially renders nothing when initialNumToRender is 0', () => {
740947
const items = generateItems(10);
741948
const ITEM_HEIGHT = 10;

0 commit comments

Comments
 (0)