@shopify/flash-list 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +159 -0
- package/LICENSE.md +7 -0
- package/README.md +65 -0
- package/RNFlashList.podspec +26 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutShadow.kt +94 -0
- package/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutView.kt +79 -0
- package/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutViewManager.kt +69 -0
- package/android/src/main/kotlin/com/shopify/reactnative/flash_list/CellContainer.java +16 -0
- package/android/src/main/kotlin/com/shopify/reactnative/flash_list/CellContainerImpl.kt +16 -0
- package/android/src/main/kotlin/com/shopify/reactnative/flash_list/CellContainerManager.kt +27 -0
- package/android/src/main/kotlin/com/shopify/reactnative/flash_list/FlashListPackage.kt +19 -0
- package/android/src/test/java/com/shopify/reactnative/flash_list/AutoLayoutShadowTest.kt +146 -0
- package/android/src/test/java/com/shopify/reactnative/flash_list/models/Rect.kt +59 -0
- package/android/src/test/java/com/shopify/reactnative/flash_list/models/TestCollection.kt +6 -0
- package/android/src/test/java/com/shopify/reactnative/flash_list/models/TestDataModel.kt +8 -0
- package/android/src/test/resources/LayoutTestData.json +708 -0
- package/dist/AnimatedFlashList.d.ts +6 -0
- package/dist/AnimatedFlashList.d.ts.map +1 -0
- package/dist/AnimatedFlashList.js +8 -0
- package/dist/AnimatedFlashList.js.map +1 -0
- package/dist/FlashList.d.ts +121 -0
- package/dist/FlashList.d.ts.map +1 -0
- package/dist/FlashList.js +502 -0
- package/dist/FlashList.js.map +1 -0
- package/dist/FlashListProps.d.ts +251 -0
- package/dist/FlashListProps.d.ts.map +1 -0
- package/dist/FlashListProps.js +3 -0
- package/dist/FlashListProps.js.map +1 -0
- package/dist/GridLayoutProviderWithProps.d.ts +30 -0
- package/dist/GridLayoutProviderWithProps.d.ts.map +1 -0
- package/dist/GridLayoutProviderWithProps.js +80 -0
- package/dist/GridLayoutProviderWithProps.js.map +1 -0
- package/dist/PureComponentWrapper.d.ts +22 -0
- package/dist/PureComponentWrapper.d.ts.map +1 -0
- package/dist/PureComponentWrapper.js +37 -0
- package/dist/PureComponentWrapper.js.map +1 -0
- package/dist/__tests__/AverageWindow.test.d.ts +2 -0
- package/dist/__tests__/AverageWindow.test.d.ts.map +1 -0
- package/dist/__tests__/AverageWindow.test.js +69 -0
- package/dist/__tests__/AverageWindow.test.js.map +1 -0
- package/dist/__tests__/FlashList.test.d.ts +2 -0
- package/dist/__tests__/FlashList.test.d.ts.map +1 -0
- package/dist/__tests__/FlashList.test.js +656 -0
- package/dist/__tests__/FlashList.test.js.map +1 -0
- package/dist/__tests__/GridLayoutProviderWithProps.test.d.ts +2 -0
- package/dist/__tests__/GridLayoutProviderWithProps.test.d.ts.map +1 -0
- package/dist/__tests__/GridLayoutProviderWithProps.test.js +133 -0
- package/dist/__tests__/GridLayoutProviderWithProps.test.js.map +1 -0
- package/dist/__tests__/PlatformHelper.web.test.d.ts +2 -0
- package/dist/__tests__/PlatformHelper.web.test.d.ts.map +1 -0
- package/dist/__tests__/PlatformHelper.web.test.js +25 -0
- package/dist/__tests__/PlatformHelper.web.test.js.map +1 -0
- package/dist/__tests__/ViewabilityHelper.test.d.ts +2 -0
- package/dist/__tests__/ViewabilityHelper.test.d.ts.map +1 -0
- package/dist/__tests__/ViewabilityHelper.test.js +187 -0
- package/dist/__tests__/ViewabilityHelper.test.js.map +1 -0
- package/dist/__tests__/helpers/mountFlashList.d.ts +20 -0
- package/dist/__tests__/helpers/mountFlashList.d.ts.map +1 -0
- package/dist/__tests__/helpers/mountFlashList.js +44 -0
- package/dist/__tests__/helpers/mountFlashList.js.map +1 -0
- package/dist/__tests__/useBlankAreaTracker.test.d.ts +2 -0
- package/dist/__tests__/useBlankAreaTracker.test.d.ts.map +1 -0
- package/dist/__tests__/useBlankAreaTracker.test.js +179 -0
- package/dist/__tests__/useBlankAreaTracker.test.js.map +1 -0
- package/dist/benchmark/AutoScrollHelper.d.ts +18 -0
- package/dist/benchmark/AutoScrollHelper.d.ts.map +1 -0
- package/dist/benchmark/AutoScrollHelper.js +68 -0
- package/dist/benchmark/AutoScrollHelper.js.map +1 -0
- package/dist/benchmark/JSFPSMonitor.d.ts +23 -0
- package/dist/benchmark/JSFPSMonitor.d.ts.map +1 -0
- package/dist/benchmark/JSFPSMonitor.js +65 -0
- package/dist/benchmark/JSFPSMonitor.js.map +1 -0
- package/dist/benchmark/roundToDecimalPlaces.d.ts +2 -0
- package/dist/benchmark/roundToDecimalPlaces.d.ts.map +1 -0
- package/dist/benchmark/roundToDecimalPlaces.js +9 -0
- package/dist/benchmark/roundToDecimalPlaces.js.map +1 -0
- package/dist/benchmark/useBenchmark.d.ts +35 -0
- package/dist/benchmark/useBenchmark.d.ts.map +1 -0
- package/dist/benchmark/useBenchmark.js +167 -0
- package/dist/benchmark/useBenchmark.js.map +1 -0
- package/dist/benchmark/useBlankAreaTracker.d.ts +34 -0
- package/dist/benchmark/useBlankAreaTracker.d.ts.map +1 -0
- package/dist/benchmark/useBlankAreaTracker.js +67 -0
- package/dist/benchmark/useBlankAreaTracker.js.map +1 -0
- package/dist/benchmark/useDataMultiplier.d.ts +9 -0
- package/dist/benchmark/useDataMultiplier.d.ts.map +1 -0
- package/dist/benchmark/useDataMultiplier.js +25 -0
- package/dist/benchmark/useDataMultiplier.js.map +1 -0
- package/dist/benchmark/useFlatListBenchmark.d.ts +13 -0
- package/dist/benchmark/useFlatListBenchmark.d.ts.map +1 -0
- package/dist/benchmark/useFlatListBenchmark.js +100 -0
- package/dist/benchmark/useFlatListBenchmark.js.map +1 -0
- package/dist/errors/CustomError.d.ts +8 -0
- package/dist/errors/CustomError.d.ts.map +1 -0
- package/dist/errors/CustomError.js +14 -0
- package/dist/errors/CustomError.js.map +1 -0
- package/dist/errors/ExceptionList.d.ts +20 -0
- package/dist/errors/ExceptionList.d.ts.map +1 -0
- package/dist/errors/ExceptionList.js +22 -0
- package/dist/errors/ExceptionList.js.map +1 -0
- package/dist/errors/Warnings.d.ts +10 -0
- package/dist/errors/Warnings.d.ts.map +1 -0
- package/dist/errors/Warnings.js +15 -0
- package/dist/errors/Warnings.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/native/auto-layout/AutoLayoutView.d.ts +21 -0
- package/dist/native/auto-layout/AutoLayoutView.d.ts.map +1 -0
- package/dist/native/auto-layout/AutoLayoutView.js +48 -0
- package/dist/native/auto-layout/AutoLayoutView.js.map +1 -0
- package/dist/native/auto-layout/AutoLayoutViewNativeComponent.d.ts +4 -0
- package/dist/native/auto-layout/AutoLayoutViewNativeComponent.d.ts.map +1 -0
- package/dist/native/auto-layout/AutoLayoutViewNativeComponent.js +6 -0
- package/dist/native/auto-layout/AutoLayoutViewNativeComponent.js.map +1 -0
- package/dist/native/auto-layout/AutoLayoutViewNativeComponent.web.d.ts +5 -0
- package/dist/native/auto-layout/AutoLayoutViewNativeComponent.web.d.ts.map +1 -0
- package/dist/native/auto-layout/AutoLayoutViewNativeComponent.web.js +6 -0
- package/dist/native/auto-layout/AutoLayoutViewNativeComponent.web.js.map +1 -0
- package/dist/native/auto-layout/AutoLayoutViewNativeComponentProps.d.ts +14 -0
- package/dist/native/auto-layout/AutoLayoutViewNativeComponentProps.d.ts.map +1 -0
- package/dist/native/auto-layout/AutoLayoutViewNativeComponentProps.js +3 -0
- package/dist/native/auto-layout/AutoLayoutViewNativeComponentProps.js.map +1 -0
- package/dist/native/cell-container/CellContainer.d.ts +6 -0
- package/dist/native/cell-container/CellContainer.d.ts.map +1 -0
- package/dist/native/cell-container/CellContainer.js +9 -0
- package/dist/native/cell-container/CellContainer.js.map +1 -0
- package/dist/native/cell-container/CellContainer.web.d.ts +7 -0
- package/dist/native/cell-container/CellContainer.web.d.ts.map +1 -0
- package/dist/native/cell-container/CellContainer.web.js +13 -0
- package/dist/native/cell-container/CellContainer.web.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/utils/AverageWindow.d.ts +21 -0
- package/dist/utils/AverageWindow.d.ts.map +1 -0
- package/dist/utils/AverageWindow.js +49 -0
- package/dist/utils/AverageWindow.js.map +1 -0
- package/dist/utils/PlatformHelper.d.ts +14 -0
- package/dist/utils/PlatformHelper.d.ts.map +1 -0
- package/dist/utils/PlatformHelper.js +16 -0
- package/dist/utils/PlatformHelper.js.map +1 -0
- package/dist/utils/PlatformHelper.web.d.ts +14 -0
- package/dist/utils/PlatformHelper.web.d.ts.map +1 -0
- package/dist/utils/PlatformHelper.web.js +18 -0
- package/dist/utils/PlatformHelper.web.js.map +1 -0
- package/dist/viewability/ViewToken.d.ts +8 -0
- package/dist/viewability/ViewToken.d.ts.map +1 -0
- package/dist/viewability/ViewToken.js +3 -0
- package/dist/viewability/ViewToken.js.map +1 -0
- package/dist/viewability/ViewabilityHelper.d.ts +25 -0
- package/dist/viewability/ViewabilityHelper.d.ts.map +1 -0
- package/dist/viewability/ViewabilityHelper.js +104 -0
- package/dist/viewability/ViewabilityHelper.js.map +1 -0
- package/dist/viewability/ViewabilityManager.d.ts +24 -0
- package/dist/viewability/ViewabilityManager.d.ts.map +1 -0
- package/dist/viewability/ViewabilityManager.js +94 -0
- package/dist/viewability/ViewabilityManager.js.map +1 -0
- package/ios/RNFlashList.xcodeproj/project.pbxproj +3 -0
- package/ios/RNFlashList.xcodeproj/project.xcworkspace/contents.xcworkspacedata +4 -0
- package/ios/Sources/AutoLayoutView.swift +218 -0
- package/ios/Sources/AutoLayoutViewManager.m +14 -0
- package/ios/Sources/AutoLayoutViewManager.swift +12 -0
- package/ios/Sources/CellContainer.swift +9 -0
- package/ios/Sources/CellContainerManager.m +8 -0
- package/ios/Sources/CellContainerManager.swift +12 -0
- package/ios/Sources/FlatListPro-Bridging-Header.h +8 -0
- package/ios/Tests/AutoLayoutViewTests.swift +113 -0
- package/jestSetup.js +15 -0
- package/package.json +75 -0
- package/src/AnimatedFlashList.ts +11 -0
- package/src/FlashList.tsx +801 -0
- package/src/FlashListProps.ts +312 -0
- package/src/GridLayoutProviderWithProps.ts +137 -0
- package/src/PureComponentWrapper.tsx +42 -0
- package/src/__tests__/AverageWindow.test.ts +80 -0
- package/src/__tests__/FlashList.test.tsx +738 -0
- package/src/__tests__/GridLayoutProviderWithProps.test.ts +150 -0
- package/src/__tests__/PlatformHelper.web.test.ts +29 -0
- package/src/__tests__/ViewabilityHelper.test.ts +283 -0
- package/src/__tests__/helpers/mountFlashList.tsx +62 -0
- package/src/__tests__/useBlankAreaTracker.test.tsx +206 -0
- package/src/benchmark/AutoScrollHelper.ts +70 -0
- package/src/benchmark/JSFPSMonitor.ts +74 -0
- package/src/benchmark/roundToDecimalPlaces.ts +4 -0
- package/src/benchmark/useBenchmark.ts +240 -0
- package/src/benchmark/useBlankAreaTracker.ts +117 -0
- package/src/benchmark/useDataMultiplier.ts +19 -0
- package/src/benchmark/useFlatListBenchmark.ts +107 -0
- package/src/errors/CustomError.ts +10 -0
- package/src/errors/ExceptionList.ts +23 -0
- package/src/errors/Warnings.ts +18 -0
- package/src/index.ts +32 -0
- package/src/native/auto-layout/AutoLayoutView.tsx +72 -0
- package/src/native/auto-layout/AutoLayoutViewNativeComponent.ts +7 -0
- package/src/native/auto-layout/AutoLayoutViewNativeComponent.web.ts +8 -0
- package/src/native/auto-layout/AutoLayoutViewNativeComponentProps.ts +14 -0
- package/src/native/cell-container/CellContainer.ts +7 -0
- package/src/native/cell-container/CellContainer.web.tsx +9 -0
- package/src/utils/AverageWindow.ts +49 -0
- package/src/utils/PlatformHelper.ts +16 -0
- package/src/utils/PlatformHelper.web.ts +20 -0
- package/src/viewability/ViewToken.ts +7 -0
- package/src/viewability/ViewabilityHelper.ts +162 -0
- package/src/viewability/ViewabilityManager.ts +133 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { mount } from "@quilted/react-testing";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
BlankAreaTrackerConfig,
|
|
6
|
+
BlankAreaTrackerResult,
|
|
7
|
+
useBlankAreaTracker,
|
|
8
|
+
} from "../benchmark/useBlankAreaTracker";
|
|
9
|
+
import FlashList from "../FlashList";
|
|
10
|
+
|
|
11
|
+
import { MockFlashListProps, renderFlashList } from "./helpers/mountFlashList";
|
|
12
|
+
|
|
13
|
+
type BlankTrackingFlashListProps = MockFlashListProps & {
|
|
14
|
+
onCumulativeBlankAreaResult?: (result: BlankAreaTrackerResult) => void;
|
|
15
|
+
onCumulativeBlankAreaChange?: (updatedResult: BlankAreaTrackerResult) => void;
|
|
16
|
+
blankAreaTrackerConfig?: BlankAreaTrackerConfig;
|
|
17
|
+
instance?: React.RefObject<FlashList<any>>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const BlankTrackingFlashList = (props?: BlankTrackingFlashListProps) => {
|
|
21
|
+
const ref = props?.instance!;
|
|
22
|
+
const [blankAreaTrackerResult, onBlankArea] = useBlankAreaTracker(
|
|
23
|
+
ref,
|
|
24
|
+
props?.onCumulativeBlankAreaChange,
|
|
25
|
+
{
|
|
26
|
+
startDelayInMs: props?.blankAreaTrackerConfig?.startDelayInMs ?? 500,
|
|
27
|
+
sumNegativeValues:
|
|
28
|
+
props?.blankAreaTrackerConfig?.sumNegativeValues ?? false,
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
return () => {
|
|
33
|
+
props?.onCumulativeBlankAreaResult?.(blankAreaTrackerResult);
|
|
34
|
+
};
|
|
35
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
36
|
+
}, []);
|
|
37
|
+
return renderFlashList(
|
|
38
|
+
{ ...props, onBlankArea, estimatedItemSize: 400 },
|
|
39
|
+
ref
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const mountBlankTrackingFlashList = (props?: BlankTrackingFlashListProps) => {
|
|
44
|
+
const flashListRef: React.RefObject<FlashList<any>> = {
|
|
45
|
+
current: null,
|
|
46
|
+
};
|
|
47
|
+
const blankTrackingFlashList = mount(
|
|
48
|
+
<BlankTrackingFlashList {...props} instance={flashListRef} />
|
|
49
|
+
);
|
|
50
|
+
return {
|
|
51
|
+
root: blankTrackingFlashList,
|
|
52
|
+
instance: flashListRef.current!,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
describe("useBlankAreaTracker Hook", () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
jest.clearAllMocks();
|
|
59
|
+
jest.useFakeTimers();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("ignores blank events for 500ms on mount and if content is not enough to fill the list", () => {
|
|
63
|
+
const onCumulativeBlankAreaChange = jest.fn();
|
|
64
|
+
const flashList = mountBlankTrackingFlashList({
|
|
65
|
+
onCumulativeBlankAreaChange,
|
|
66
|
+
});
|
|
67
|
+
flashList.instance.props.onBlankArea!({
|
|
68
|
+
blankArea: 100,
|
|
69
|
+
offsetEnd: 100,
|
|
70
|
+
offsetStart: 0,
|
|
71
|
+
});
|
|
72
|
+
flashList.instance.props.onBlankArea!({
|
|
73
|
+
blankArea: 100,
|
|
74
|
+
offsetEnd: 100,
|
|
75
|
+
offsetStart: 0,
|
|
76
|
+
});
|
|
77
|
+
jest.advanceTimersByTime(400);
|
|
78
|
+
flashList.instance.props.onBlankArea!({
|
|
79
|
+
blankArea: 100,
|
|
80
|
+
offsetEnd: 100,
|
|
81
|
+
offsetStart: 0,
|
|
82
|
+
});
|
|
83
|
+
expect(onCumulativeBlankAreaChange).toHaveBeenCalledTimes(0);
|
|
84
|
+
jest.advanceTimersByTime(100);
|
|
85
|
+
flashList.instance.props.onBlankArea!({
|
|
86
|
+
blankArea: 100,
|
|
87
|
+
offsetEnd: 100,
|
|
88
|
+
offsetStart: 0,
|
|
89
|
+
});
|
|
90
|
+
expect(onCumulativeBlankAreaChange).toHaveBeenCalledTimes(1);
|
|
91
|
+
onCumulativeBlankAreaChange.mockClear();
|
|
92
|
+
|
|
93
|
+
flashList.root.setProps({ data: ["1"] });
|
|
94
|
+
flashList.instance.props.onBlankArea!({
|
|
95
|
+
blankArea: 100,
|
|
96
|
+
offsetEnd: 100,
|
|
97
|
+
offsetStart: 0,
|
|
98
|
+
});
|
|
99
|
+
expect(onCumulativeBlankAreaChange).toHaveBeenCalledTimes(0);
|
|
100
|
+
flashList.root.unmount();
|
|
101
|
+
});
|
|
102
|
+
it("keeps result object updated with correct values on unmount", () => {
|
|
103
|
+
let resultObjectCopy: BlankAreaTrackerResult | undefined;
|
|
104
|
+
const onCumulativeBlankAreaChange = jest.fn(
|
|
105
|
+
(result: BlankAreaTrackerResult) => {
|
|
106
|
+
if (!resultObjectCopy) {
|
|
107
|
+
resultObjectCopy = result;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
const onCumulativeBlankAreaResult = jest.fn();
|
|
112
|
+
const flashList = mountBlankTrackingFlashList({
|
|
113
|
+
onCumulativeBlankAreaResult,
|
|
114
|
+
onCumulativeBlankAreaChange,
|
|
115
|
+
blankAreaTrackerConfig: { startDelayInMs: 300 },
|
|
116
|
+
});
|
|
117
|
+
flashList.instance.props.onBlankArea!({
|
|
118
|
+
blankArea: 100,
|
|
119
|
+
offsetEnd: 100,
|
|
120
|
+
offsetStart: 0,
|
|
121
|
+
});
|
|
122
|
+
jest.advanceTimersByTime(300);
|
|
123
|
+
flashList.instance.props.onBlankArea!({
|
|
124
|
+
blankArea: 100,
|
|
125
|
+
offsetEnd: 100,
|
|
126
|
+
offsetStart: 0,
|
|
127
|
+
});
|
|
128
|
+
expect(resultObjectCopy!.cumulativeBlankArea).toBe(100);
|
|
129
|
+
expect(resultObjectCopy!.maxBlankArea).toBe(100);
|
|
130
|
+
|
|
131
|
+
flashList.instance.props.onBlankArea!({
|
|
132
|
+
blankArea: 200,
|
|
133
|
+
offsetEnd: 200,
|
|
134
|
+
offsetStart: 0,
|
|
135
|
+
});
|
|
136
|
+
flashList.instance.props.onBlankArea!({
|
|
137
|
+
blankArea: -200,
|
|
138
|
+
offsetEnd: -200,
|
|
139
|
+
offsetStart: 0,
|
|
140
|
+
});
|
|
141
|
+
expect(resultObjectCopy!.cumulativeBlankArea).toBe(300);
|
|
142
|
+
expect(resultObjectCopy!.maxBlankArea).toBe(200);
|
|
143
|
+
|
|
144
|
+
flashList.root.unmount();
|
|
145
|
+
expect(onCumulativeBlankAreaResult).toHaveBeenCalledWith(resultObjectCopy!);
|
|
146
|
+
});
|
|
147
|
+
it("can track negative values on demand", () => {
|
|
148
|
+
const onCumulativeBlankAreaResult = jest.fn();
|
|
149
|
+
const flashList = mountBlankTrackingFlashList({
|
|
150
|
+
onCumulativeBlankAreaResult,
|
|
151
|
+
blankAreaTrackerConfig: { sumNegativeValues: true },
|
|
152
|
+
});
|
|
153
|
+
flashList.instance.props.onBlankArea!({
|
|
154
|
+
blankArea: -200,
|
|
155
|
+
offsetEnd: -200,
|
|
156
|
+
offsetStart: 0,
|
|
157
|
+
});
|
|
158
|
+
jest.advanceTimersByTime(500);
|
|
159
|
+
flashList.instance.props.onBlankArea!({
|
|
160
|
+
blankArea: -200,
|
|
161
|
+
offsetEnd: -200,
|
|
162
|
+
offsetStart: 0,
|
|
163
|
+
});
|
|
164
|
+
flashList.instance.props.onBlankArea!({
|
|
165
|
+
blankArea: -200,
|
|
166
|
+
offsetEnd: -200,
|
|
167
|
+
offsetStart: 0,
|
|
168
|
+
});
|
|
169
|
+
flashList.root.unmount();
|
|
170
|
+
expect(onCumulativeBlankAreaResult).toHaveBeenCalledWith({
|
|
171
|
+
cumulativeBlankArea: -400,
|
|
172
|
+
maxBlankArea: 0,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
it("only calls onCumulativeBlankAreaChange when values have a change", () => {
|
|
176
|
+
const onCumulativeBlankAreaChange = jest.fn();
|
|
177
|
+
const flashList = mountBlankTrackingFlashList({
|
|
178
|
+
onCumulativeBlankAreaChange,
|
|
179
|
+
});
|
|
180
|
+
flashList.instance.props.onBlankArea!({
|
|
181
|
+
blankArea: -200,
|
|
182
|
+
offsetEnd: -200,
|
|
183
|
+
offsetStart: 0,
|
|
184
|
+
});
|
|
185
|
+
jest.advanceTimersByTime(500);
|
|
186
|
+
flashList.instance.props.onBlankArea!({
|
|
187
|
+
blankArea: -200,
|
|
188
|
+
offsetEnd: -200,
|
|
189
|
+
offsetStart: 0,
|
|
190
|
+
});
|
|
191
|
+
expect(onCumulativeBlankAreaChange).toHaveBeenCalledTimes(0);
|
|
192
|
+
flashList.instance.props.onBlankArea!({
|
|
193
|
+
blankArea: -100,
|
|
194
|
+
offsetEnd: -100,
|
|
195
|
+
offsetStart: 0,
|
|
196
|
+
});
|
|
197
|
+
expect(onCumulativeBlankAreaChange).toHaveBeenCalledTimes(0);
|
|
198
|
+
flashList.instance.props.onBlankArea!({
|
|
199
|
+
blankArea: 100,
|
|
200
|
+
offsetEnd: 100,
|
|
201
|
+
offsetStart: 0,
|
|
202
|
+
});
|
|
203
|
+
expect(onCumulativeBlankAreaChange).toHaveBeenCalledTimes(1);
|
|
204
|
+
flashList.root.unmount();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This method can be used to trigger scroll events that can be forwarded to an element. Anything that implements scrollable can leverage this.
|
|
3
|
+
* @param scroll The scrollable element
|
|
4
|
+
* @param fromX The x offset to start from
|
|
5
|
+
* @param fromY The y offset to start from
|
|
6
|
+
* @param toX the x offset to end scroll at
|
|
7
|
+
* @param toY the y offset to end scroll at
|
|
8
|
+
* @param speedMultiplier the speed multiplier to use
|
|
9
|
+
* @param cancellable can be used to cancel the scroll
|
|
10
|
+
* @returns Promise that resolves when the scroll is complete
|
|
11
|
+
*/
|
|
12
|
+
export function autoScroll(
|
|
13
|
+
scroll: (x: number, y: number, animated: boolean) => void,
|
|
14
|
+
fromX: number,
|
|
15
|
+
fromY: number,
|
|
16
|
+
toX: number,
|
|
17
|
+
toY: number,
|
|
18
|
+
speedMultiplier = 1,
|
|
19
|
+
cancellable: Cancellable = new Cancellable()
|
|
20
|
+
): Promise<boolean> {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
scroll(fromX, fromY, false);
|
|
23
|
+
// Very fast scrolls on Android/iOS typically move content 7px every millisecond.
|
|
24
|
+
const incrementPerMs = 7 * speedMultiplier;
|
|
25
|
+
const directionMultiplierX = toX > fromX ? 1 : -1;
|
|
26
|
+
const directionMultiplierY = toY > fromY ? 1 : -1;
|
|
27
|
+
const comparatorX = toX > fromX ? Math.min : Math.max;
|
|
28
|
+
const comparatorY = toY > fromY ? Math.min : Math.max;
|
|
29
|
+
let startTime = Date.now();
|
|
30
|
+
let startX = fromX;
|
|
31
|
+
let startY = fromY;
|
|
32
|
+
// Computes the number of pixels to scroll in the given time
|
|
33
|
+
// Also invokes the scrollable to update the scroll position
|
|
34
|
+
const animationLoop = () => {
|
|
35
|
+
requestAnimationFrame(() => {
|
|
36
|
+
if (cancellable.isCancelled()) {
|
|
37
|
+
resolve(false);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const currentTime = Date.now();
|
|
41
|
+
const timeElapsed = currentTime - startTime;
|
|
42
|
+
const distanceToCover = incrementPerMs * timeElapsed;
|
|
43
|
+
startX += distanceToCover * directionMultiplierX;
|
|
44
|
+
startY += distanceToCover * directionMultiplierY;
|
|
45
|
+
scroll(comparatorX(toX, startX), comparatorY(toY, startY), false);
|
|
46
|
+
startTime = currentTime;
|
|
47
|
+
if (
|
|
48
|
+
comparatorX(toX, startX) !== toX ||
|
|
49
|
+
comparatorY(toY, startY) !== toY
|
|
50
|
+
) {
|
|
51
|
+
return animationLoop();
|
|
52
|
+
}
|
|
53
|
+
resolve(true);
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
animationLoop();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class Cancellable {
|
|
61
|
+
public cancel() {
|
|
62
|
+
this._isCancelled = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public isCancelled(): boolean {
|
|
66
|
+
return this._isCancelled;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public _isCancelled: boolean = false;
|
|
70
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { roundToDecimalPlaces } from "./roundToDecimalPlaces";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Can be used to monitor JS thread performance
|
|
5
|
+
* Use startTracking() and stopAndGetData() to start and stop tracking
|
|
6
|
+
*/
|
|
7
|
+
export class JSFPSMonitor {
|
|
8
|
+
private startTime = 0;
|
|
9
|
+
private frameCount = 0;
|
|
10
|
+
private timeWindow = {
|
|
11
|
+
frameCount: 0,
|
|
12
|
+
startTime: 0,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
private minFPS = Number.MAX_SAFE_INTEGER;
|
|
16
|
+
private maxFPS = 0;
|
|
17
|
+
private averageFPS = 0;
|
|
18
|
+
|
|
19
|
+
private clearAnimationNumber = 0;
|
|
20
|
+
|
|
21
|
+
private measureLoop() {
|
|
22
|
+
// This looks weird but I'm avoiding a new closure
|
|
23
|
+
this.clearAnimationNumber = requestAnimationFrame(this.updateLoopCompute);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private updateLoopCompute = () => {
|
|
27
|
+
this.frameCount++;
|
|
28
|
+
const elapsedTime = (Date.now() - this.startTime) / 1000;
|
|
29
|
+
this.averageFPS = elapsedTime > 0 ? this.frameCount / elapsedTime : 0;
|
|
30
|
+
|
|
31
|
+
this.timeWindow.frameCount++;
|
|
32
|
+
const timeWindowElapsedTime =
|
|
33
|
+
(Date.now() - this.timeWindow.startTime) / 1000;
|
|
34
|
+
if (timeWindowElapsedTime >= 1) {
|
|
35
|
+
const timeWindowAverageFPS =
|
|
36
|
+
this.timeWindow.frameCount / timeWindowElapsedTime;
|
|
37
|
+
this.minFPS = Math.min(this.minFPS, timeWindowAverageFPS);
|
|
38
|
+
this.maxFPS = Math.max(this.maxFPS, timeWindowAverageFPS);
|
|
39
|
+
this.timeWindow.frameCount = 0;
|
|
40
|
+
this.timeWindow.startTime = Date.now();
|
|
41
|
+
}
|
|
42
|
+
this.measureLoop();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
public startTracking() {
|
|
46
|
+
if (this.startTime !== 0) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"This FPS Monitor has already been run, please create a new instance"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
this.startTime = Date.now();
|
|
52
|
+
this.timeWindow.startTime = Date.now();
|
|
53
|
+
this.measureLoop();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public stopAndGetData(): JSFPSResult {
|
|
57
|
+
cancelAnimationFrame(this.clearAnimationNumber);
|
|
58
|
+
if (this.minFPS === Number.MAX_SAFE_INTEGER) {
|
|
59
|
+
this.minFPS = this.averageFPS;
|
|
60
|
+
this.maxFPS = this.averageFPS;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
minFPS: roundToDecimalPlaces(this.minFPS, 1),
|
|
64
|
+
maxFPS: roundToDecimalPlaces(this.maxFPS, 1),
|
|
65
|
+
averageFPS: roundToDecimalPlaces(this.averageFPS, 1),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface JSFPSResult {
|
|
71
|
+
minFPS: number;
|
|
72
|
+
maxFPS: number;
|
|
73
|
+
averageFPS: number;
|
|
74
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
import FlashList from "../FlashList";
|
|
4
|
+
|
|
5
|
+
import { autoScroll, Cancellable } from "./AutoScrollHelper";
|
|
6
|
+
import { JSFPSMonitor, JSFPSResult } from "./JSFPSMonitor";
|
|
7
|
+
import { roundToDecimalPlaces } from "./roundToDecimalPlaces";
|
|
8
|
+
import {
|
|
9
|
+
BlankAreaTrackerResult,
|
|
10
|
+
useBlankAreaTracker,
|
|
11
|
+
} from "./useBlankAreaTracker";
|
|
12
|
+
|
|
13
|
+
export interface BenchmarkParams {
|
|
14
|
+
startDelayInMs?: number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Can be used to increase or decrease speed of scrolling
|
|
18
|
+
*/
|
|
19
|
+
speedMultiplier?: number;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Specify the number of times benchmark should repeat itself
|
|
23
|
+
*/
|
|
24
|
+
repeatCount?: number;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* When set to true, cumulative blank area will include sum of negative blank area values
|
|
28
|
+
* Blank area is negative when list is able to draw faster than the scroll speed.
|
|
29
|
+
*/
|
|
30
|
+
sumNegativeBlankAreaValues?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BenchmarkResult {
|
|
34
|
+
js?: JSFPSResult;
|
|
35
|
+
interrupted: boolean;
|
|
36
|
+
suggestions: string[];
|
|
37
|
+
blankArea?: BlankAreaTrackerResult;
|
|
38
|
+
formattedString?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Runs the benchmark on FlashList.
|
|
43
|
+
* Response object has a formatted string that can be printed to the console or shown as an alert.
|
|
44
|
+
* Result is posted to the callback method passed to the hook.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
export function useBenchmark(
|
|
48
|
+
flashListRef: React.RefObject<FlashList<any>>,
|
|
49
|
+
callback: (benchmarkResult: BenchmarkResult) => void,
|
|
50
|
+
params: BenchmarkParams = {}
|
|
51
|
+
) {
|
|
52
|
+
const [blankAreaResult, blankAreaTracker] = useBlankAreaTracker(
|
|
53
|
+
flashListRef,
|
|
54
|
+
undefined,
|
|
55
|
+
{ sumNegativeValues: params.sumNegativeBlankAreaValues, startDelayInMs: 0 }
|
|
56
|
+
);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const cancellable = new Cancellable();
|
|
59
|
+
const suggestions: string[] = [];
|
|
60
|
+
if (flashListRef.current) {
|
|
61
|
+
if (!(Number(flashListRef.current.props.data?.length) > 0)) {
|
|
62
|
+
throw new Error("Data is empty, cannot run benchmark");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const cancelTimeout = setTimeout(async () => {
|
|
66
|
+
const jsFPSMonitor = new JSFPSMonitor();
|
|
67
|
+
jsFPSMonitor.startTracking();
|
|
68
|
+
for (let i = 0; i < (params.repeatCount || 1); i++) {
|
|
69
|
+
await runScrollBenchmark(
|
|
70
|
+
flashListRef,
|
|
71
|
+
cancellable,
|
|
72
|
+
params.speedMultiplier || 1
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const jsProfilerResponse = jsFPSMonitor.stopAndGetData();
|
|
76
|
+
if (jsProfilerResponse.averageFPS < 35) {
|
|
77
|
+
suggestions.push(
|
|
78
|
+
`Your average JS FPS is low. This can indicate that your components are doing too much work. Try to optimize your components and reduce re-renders if any`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
computeSuggestions(flashListRef, suggestions);
|
|
82
|
+
const result: BenchmarkResult = generateResult(
|
|
83
|
+
jsProfilerResponse,
|
|
84
|
+
blankAreaResult,
|
|
85
|
+
suggestions,
|
|
86
|
+
cancellable
|
|
87
|
+
);
|
|
88
|
+
if (!cancellable.isCancelled()) {
|
|
89
|
+
result.formattedString = getFormattedString(result);
|
|
90
|
+
}
|
|
91
|
+
callback(result);
|
|
92
|
+
}, params.startDelayInMs || 3000);
|
|
93
|
+
return () => {
|
|
94
|
+
clearTimeout(cancelTimeout);
|
|
95
|
+
cancellable.cancel();
|
|
96
|
+
};
|
|
97
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
98
|
+
}, []);
|
|
99
|
+
return [blankAreaTracker];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getFormattedString(res: BenchmarkResult) {
|
|
103
|
+
return (
|
|
104
|
+
`Results:\n\n` +
|
|
105
|
+
`JS FPS: Avg: ${res.js?.averageFPS} | Min: ${res.js?.minFPS} | Max: ${res.js?.maxFPS}\n\n` +
|
|
106
|
+
`${
|
|
107
|
+
res.blankArea
|
|
108
|
+
? `Blank Area: Max: ${res.blankArea?.maxBlankArea} Cumulative: ${res.blankArea?.cumulativeBlankArea}\n\n`
|
|
109
|
+
: ``
|
|
110
|
+
}` +
|
|
111
|
+
`${
|
|
112
|
+
res.suggestions.length > 0
|
|
113
|
+
? `Suggestions:\n\n${res.suggestions
|
|
114
|
+
.map((value, index) => `${index + 1}. ${value}`)
|
|
115
|
+
.join("\n")}`
|
|
116
|
+
: ``
|
|
117
|
+
}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function generateResult(
|
|
122
|
+
jsProfilerResponse: JSFPSResult,
|
|
123
|
+
blankAreaResult: BlankAreaTrackerResult,
|
|
124
|
+
suggestions: string[],
|
|
125
|
+
cancellable: Cancellable
|
|
126
|
+
) {
|
|
127
|
+
return {
|
|
128
|
+
js: jsProfilerResponse,
|
|
129
|
+
blankArea:
|
|
130
|
+
blankAreaResult.maxBlankArea >= 0
|
|
131
|
+
? {
|
|
132
|
+
maxBlankArea: roundToDecimalPlaces(blankAreaResult.maxBlankArea, 0),
|
|
133
|
+
cumulativeBlankArea: roundToDecimalPlaces(
|
|
134
|
+
blankAreaResult.cumulativeBlankArea,
|
|
135
|
+
0
|
|
136
|
+
),
|
|
137
|
+
}
|
|
138
|
+
: undefined,
|
|
139
|
+
suggestions,
|
|
140
|
+
interrupted: cancellable.isCancelled(),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Scrolls to the end of the list and then back to the top
|
|
146
|
+
*/
|
|
147
|
+
async function runScrollBenchmark(
|
|
148
|
+
flashListRef: React.RefObject<FlashList<any> | null | undefined>,
|
|
149
|
+
cancellable: Cancellable,
|
|
150
|
+
scrollSpeedMultiplier: number
|
|
151
|
+
): Promise<void> {
|
|
152
|
+
if (flashListRef.current) {
|
|
153
|
+
const horizontal = flashListRef.current.props.horizontal;
|
|
154
|
+
const rlv = flashListRef.current.recyclerlistview_unsafe;
|
|
155
|
+
if (rlv) {
|
|
156
|
+
const rlvSize = rlv.getRenderedSize();
|
|
157
|
+
const rlvContentSize = rlv.getContentDimension();
|
|
158
|
+
|
|
159
|
+
const fromX = 0;
|
|
160
|
+
const fromY = 0;
|
|
161
|
+
const toX = rlvContentSize.width - rlvSize.width;
|
|
162
|
+
const toY = rlvContentSize.height - rlvSize.height;
|
|
163
|
+
|
|
164
|
+
const scrollNow = (x: number, y: number) => {
|
|
165
|
+
flashListRef.current?.scrollToOffset({
|
|
166
|
+
offset: horizontal ? x : y,
|
|
167
|
+
animated: false,
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
await autoScroll(
|
|
172
|
+
scrollNow,
|
|
173
|
+
fromX,
|
|
174
|
+
fromY,
|
|
175
|
+
toX,
|
|
176
|
+
toY,
|
|
177
|
+
scrollSpeedMultiplier,
|
|
178
|
+
cancellable
|
|
179
|
+
);
|
|
180
|
+
await autoScroll(
|
|
181
|
+
scrollNow,
|
|
182
|
+
toX,
|
|
183
|
+
toY,
|
|
184
|
+
fromX,
|
|
185
|
+
fromY,
|
|
186
|
+
scrollSpeedMultiplier,
|
|
187
|
+
cancellable
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function computeSuggestions(
|
|
193
|
+
flashListRef: React.RefObject<FlashList<any> | null | undefined>,
|
|
194
|
+
suggestions: string[]
|
|
195
|
+
) {
|
|
196
|
+
if (flashListRef.current) {
|
|
197
|
+
if (flashListRef.current.props.data!!.length < 200) {
|
|
198
|
+
suggestions.push(
|
|
199
|
+
`Data count is low. Try to increase it to a large number (e.g 200) using the 'useDataMultiplier' hook.`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const distanceFromWindow = roundToDecimalPlaces(
|
|
203
|
+
flashListRef.current.firstItemOffset,
|
|
204
|
+
0
|
|
205
|
+
);
|
|
206
|
+
if (
|
|
207
|
+
(flashListRef.current.props.estimatedFirstItemOffset || 0) !==
|
|
208
|
+
distanceFromWindow
|
|
209
|
+
) {
|
|
210
|
+
suggestions.push(
|
|
211
|
+
`estimatedFirstItemOffset can be set to ${distanceFromWindow}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const rlv = flashListRef.current.recyclerlistview_unsafe;
|
|
215
|
+
const horizontal = flashListRef.current.props.horizontal;
|
|
216
|
+
if (rlv) {
|
|
217
|
+
const sizeArray = rlv.props.dataProvider
|
|
218
|
+
.getAllData()
|
|
219
|
+
.map((_, index) =>
|
|
220
|
+
horizontal
|
|
221
|
+
? rlv.getLayout?.(index)?.width || 0
|
|
222
|
+
: rlv.getLayout?.(index)?.height || 0
|
|
223
|
+
);
|
|
224
|
+
const averageSize = Math.round(
|
|
225
|
+
sizeArray.reduce((prev, current) => prev + current, 0) /
|
|
226
|
+
sizeArray.length
|
|
227
|
+
);
|
|
228
|
+
if (
|
|
229
|
+
Math.abs(
|
|
230
|
+
averageSize -
|
|
231
|
+
(flashListRef.current.props.estimatedItemSize ??
|
|
232
|
+
flashListRef.current.state.layoutProvider
|
|
233
|
+
.defaultEstimatedItemSize)
|
|
234
|
+
) > 5
|
|
235
|
+
) {
|
|
236
|
+
suggestions.push(`estimatedItemSize can be set to ${averageSize}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|