@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.
Files changed (206) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/LICENSE.md +7 -0
  3. package/README.md +65 -0
  4. package/RNFlashList.podspec +26 -0
  5. package/android/build.gradle +59 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutShadow.kt +94 -0
  8. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutView.kt +79 -0
  9. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutViewManager.kt +69 -0
  10. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/CellContainer.java +16 -0
  11. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/CellContainerImpl.kt +16 -0
  12. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/CellContainerManager.kt +27 -0
  13. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/FlashListPackage.kt +19 -0
  14. package/android/src/test/java/com/shopify/reactnative/flash_list/AutoLayoutShadowTest.kt +146 -0
  15. package/android/src/test/java/com/shopify/reactnative/flash_list/models/Rect.kt +59 -0
  16. package/android/src/test/java/com/shopify/reactnative/flash_list/models/TestCollection.kt +6 -0
  17. package/android/src/test/java/com/shopify/reactnative/flash_list/models/TestDataModel.kt +8 -0
  18. package/android/src/test/resources/LayoutTestData.json +708 -0
  19. package/dist/AnimatedFlashList.d.ts +6 -0
  20. package/dist/AnimatedFlashList.d.ts.map +1 -0
  21. package/dist/AnimatedFlashList.js +8 -0
  22. package/dist/AnimatedFlashList.js.map +1 -0
  23. package/dist/FlashList.d.ts +121 -0
  24. package/dist/FlashList.d.ts.map +1 -0
  25. package/dist/FlashList.js +502 -0
  26. package/dist/FlashList.js.map +1 -0
  27. package/dist/FlashListProps.d.ts +251 -0
  28. package/dist/FlashListProps.d.ts.map +1 -0
  29. package/dist/FlashListProps.js +3 -0
  30. package/dist/FlashListProps.js.map +1 -0
  31. package/dist/GridLayoutProviderWithProps.d.ts +30 -0
  32. package/dist/GridLayoutProviderWithProps.d.ts.map +1 -0
  33. package/dist/GridLayoutProviderWithProps.js +80 -0
  34. package/dist/GridLayoutProviderWithProps.js.map +1 -0
  35. package/dist/PureComponentWrapper.d.ts +22 -0
  36. package/dist/PureComponentWrapper.d.ts.map +1 -0
  37. package/dist/PureComponentWrapper.js +37 -0
  38. package/dist/PureComponentWrapper.js.map +1 -0
  39. package/dist/__tests__/AverageWindow.test.d.ts +2 -0
  40. package/dist/__tests__/AverageWindow.test.d.ts.map +1 -0
  41. package/dist/__tests__/AverageWindow.test.js +69 -0
  42. package/dist/__tests__/AverageWindow.test.js.map +1 -0
  43. package/dist/__tests__/FlashList.test.d.ts +2 -0
  44. package/dist/__tests__/FlashList.test.d.ts.map +1 -0
  45. package/dist/__tests__/FlashList.test.js +656 -0
  46. package/dist/__tests__/FlashList.test.js.map +1 -0
  47. package/dist/__tests__/GridLayoutProviderWithProps.test.d.ts +2 -0
  48. package/dist/__tests__/GridLayoutProviderWithProps.test.d.ts.map +1 -0
  49. package/dist/__tests__/GridLayoutProviderWithProps.test.js +133 -0
  50. package/dist/__tests__/GridLayoutProviderWithProps.test.js.map +1 -0
  51. package/dist/__tests__/PlatformHelper.web.test.d.ts +2 -0
  52. package/dist/__tests__/PlatformHelper.web.test.d.ts.map +1 -0
  53. package/dist/__tests__/PlatformHelper.web.test.js +25 -0
  54. package/dist/__tests__/PlatformHelper.web.test.js.map +1 -0
  55. package/dist/__tests__/ViewabilityHelper.test.d.ts +2 -0
  56. package/dist/__tests__/ViewabilityHelper.test.d.ts.map +1 -0
  57. package/dist/__tests__/ViewabilityHelper.test.js +187 -0
  58. package/dist/__tests__/ViewabilityHelper.test.js.map +1 -0
  59. package/dist/__tests__/helpers/mountFlashList.d.ts +20 -0
  60. package/dist/__tests__/helpers/mountFlashList.d.ts.map +1 -0
  61. package/dist/__tests__/helpers/mountFlashList.js +44 -0
  62. package/dist/__tests__/helpers/mountFlashList.js.map +1 -0
  63. package/dist/__tests__/useBlankAreaTracker.test.d.ts +2 -0
  64. package/dist/__tests__/useBlankAreaTracker.test.d.ts.map +1 -0
  65. package/dist/__tests__/useBlankAreaTracker.test.js +179 -0
  66. package/dist/__tests__/useBlankAreaTracker.test.js.map +1 -0
  67. package/dist/benchmark/AutoScrollHelper.d.ts +18 -0
  68. package/dist/benchmark/AutoScrollHelper.d.ts.map +1 -0
  69. package/dist/benchmark/AutoScrollHelper.js +68 -0
  70. package/dist/benchmark/AutoScrollHelper.js.map +1 -0
  71. package/dist/benchmark/JSFPSMonitor.d.ts +23 -0
  72. package/dist/benchmark/JSFPSMonitor.d.ts.map +1 -0
  73. package/dist/benchmark/JSFPSMonitor.js +65 -0
  74. package/dist/benchmark/JSFPSMonitor.js.map +1 -0
  75. package/dist/benchmark/roundToDecimalPlaces.d.ts +2 -0
  76. package/dist/benchmark/roundToDecimalPlaces.d.ts.map +1 -0
  77. package/dist/benchmark/roundToDecimalPlaces.js +9 -0
  78. package/dist/benchmark/roundToDecimalPlaces.js.map +1 -0
  79. package/dist/benchmark/useBenchmark.d.ts +35 -0
  80. package/dist/benchmark/useBenchmark.d.ts.map +1 -0
  81. package/dist/benchmark/useBenchmark.js +167 -0
  82. package/dist/benchmark/useBenchmark.js.map +1 -0
  83. package/dist/benchmark/useBlankAreaTracker.d.ts +34 -0
  84. package/dist/benchmark/useBlankAreaTracker.d.ts.map +1 -0
  85. package/dist/benchmark/useBlankAreaTracker.js +67 -0
  86. package/dist/benchmark/useBlankAreaTracker.js.map +1 -0
  87. package/dist/benchmark/useDataMultiplier.d.ts +9 -0
  88. package/dist/benchmark/useDataMultiplier.d.ts.map +1 -0
  89. package/dist/benchmark/useDataMultiplier.js +25 -0
  90. package/dist/benchmark/useDataMultiplier.js.map +1 -0
  91. package/dist/benchmark/useFlatListBenchmark.d.ts +13 -0
  92. package/dist/benchmark/useFlatListBenchmark.d.ts.map +1 -0
  93. package/dist/benchmark/useFlatListBenchmark.js +100 -0
  94. package/dist/benchmark/useFlatListBenchmark.js.map +1 -0
  95. package/dist/errors/CustomError.d.ts +8 -0
  96. package/dist/errors/CustomError.d.ts.map +1 -0
  97. package/dist/errors/CustomError.js +14 -0
  98. package/dist/errors/CustomError.js.map +1 -0
  99. package/dist/errors/ExceptionList.d.ts +20 -0
  100. package/dist/errors/ExceptionList.d.ts.map +1 -0
  101. package/dist/errors/ExceptionList.js +22 -0
  102. package/dist/errors/ExceptionList.js.map +1 -0
  103. package/dist/errors/Warnings.d.ts +10 -0
  104. package/dist/errors/Warnings.d.ts.map +1 -0
  105. package/dist/errors/Warnings.js +15 -0
  106. package/dist/errors/Warnings.js.map +1 -0
  107. package/dist/index.d.ts +13 -0
  108. package/dist/index.d.ts.map +1 -0
  109. package/dist/index.js +28 -0
  110. package/dist/index.js.map +1 -0
  111. package/dist/native/auto-layout/AutoLayoutView.d.ts +21 -0
  112. package/dist/native/auto-layout/AutoLayoutView.d.ts.map +1 -0
  113. package/dist/native/auto-layout/AutoLayoutView.js +48 -0
  114. package/dist/native/auto-layout/AutoLayoutView.js.map +1 -0
  115. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.d.ts +4 -0
  116. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.d.ts.map +1 -0
  117. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.js +6 -0
  118. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.js.map +1 -0
  119. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.web.d.ts +5 -0
  120. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.web.d.ts.map +1 -0
  121. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.web.js +6 -0
  122. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.web.js.map +1 -0
  123. package/dist/native/auto-layout/AutoLayoutViewNativeComponentProps.d.ts +14 -0
  124. package/dist/native/auto-layout/AutoLayoutViewNativeComponentProps.d.ts.map +1 -0
  125. package/dist/native/auto-layout/AutoLayoutViewNativeComponentProps.js +3 -0
  126. package/dist/native/auto-layout/AutoLayoutViewNativeComponentProps.js.map +1 -0
  127. package/dist/native/cell-container/CellContainer.d.ts +6 -0
  128. package/dist/native/cell-container/CellContainer.d.ts.map +1 -0
  129. package/dist/native/cell-container/CellContainer.js +9 -0
  130. package/dist/native/cell-container/CellContainer.js.map +1 -0
  131. package/dist/native/cell-container/CellContainer.web.d.ts +7 -0
  132. package/dist/native/cell-container/CellContainer.web.d.ts.map +1 -0
  133. package/dist/native/cell-container/CellContainer.web.js +13 -0
  134. package/dist/native/cell-container/CellContainer.web.js.map +1 -0
  135. package/dist/tsconfig.tsbuildinfo +1 -0
  136. package/dist/utils/AverageWindow.d.ts +21 -0
  137. package/dist/utils/AverageWindow.d.ts.map +1 -0
  138. package/dist/utils/AverageWindow.js +49 -0
  139. package/dist/utils/AverageWindow.js.map +1 -0
  140. package/dist/utils/PlatformHelper.d.ts +14 -0
  141. package/dist/utils/PlatformHelper.d.ts.map +1 -0
  142. package/dist/utils/PlatformHelper.js +16 -0
  143. package/dist/utils/PlatformHelper.js.map +1 -0
  144. package/dist/utils/PlatformHelper.web.d.ts +14 -0
  145. package/dist/utils/PlatformHelper.web.d.ts.map +1 -0
  146. package/dist/utils/PlatformHelper.web.js +18 -0
  147. package/dist/utils/PlatformHelper.web.js.map +1 -0
  148. package/dist/viewability/ViewToken.d.ts +8 -0
  149. package/dist/viewability/ViewToken.d.ts.map +1 -0
  150. package/dist/viewability/ViewToken.js +3 -0
  151. package/dist/viewability/ViewToken.js.map +1 -0
  152. package/dist/viewability/ViewabilityHelper.d.ts +25 -0
  153. package/dist/viewability/ViewabilityHelper.d.ts.map +1 -0
  154. package/dist/viewability/ViewabilityHelper.js +104 -0
  155. package/dist/viewability/ViewabilityHelper.js.map +1 -0
  156. package/dist/viewability/ViewabilityManager.d.ts +24 -0
  157. package/dist/viewability/ViewabilityManager.d.ts.map +1 -0
  158. package/dist/viewability/ViewabilityManager.js +94 -0
  159. package/dist/viewability/ViewabilityManager.js.map +1 -0
  160. package/ios/RNFlashList.xcodeproj/project.pbxproj +3 -0
  161. package/ios/RNFlashList.xcodeproj/project.xcworkspace/contents.xcworkspacedata +4 -0
  162. package/ios/Sources/AutoLayoutView.swift +218 -0
  163. package/ios/Sources/AutoLayoutViewManager.m +14 -0
  164. package/ios/Sources/AutoLayoutViewManager.swift +12 -0
  165. package/ios/Sources/CellContainer.swift +9 -0
  166. package/ios/Sources/CellContainerManager.m +8 -0
  167. package/ios/Sources/CellContainerManager.swift +12 -0
  168. package/ios/Sources/FlatListPro-Bridging-Header.h +8 -0
  169. package/ios/Tests/AutoLayoutViewTests.swift +113 -0
  170. package/jestSetup.js +15 -0
  171. package/package.json +75 -0
  172. package/src/AnimatedFlashList.ts +11 -0
  173. package/src/FlashList.tsx +801 -0
  174. package/src/FlashListProps.ts +312 -0
  175. package/src/GridLayoutProviderWithProps.ts +137 -0
  176. package/src/PureComponentWrapper.tsx +42 -0
  177. package/src/__tests__/AverageWindow.test.ts +80 -0
  178. package/src/__tests__/FlashList.test.tsx +738 -0
  179. package/src/__tests__/GridLayoutProviderWithProps.test.ts +150 -0
  180. package/src/__tests__/PlatformHelper.web.test.ts +29 -0
  181. package/src/__tests__/ViewabilityHelper.test.ts +283 -0
  182. package/src/__tests__/helpers/mountFlashList.tsx +62 -0
  183. package/src/__tests__/useBlankAreaTracker.test.tsx +206 -0
  184. package/src/benchmark/AutoScrollHelper.ts +70 -0
  185. package/src/benchmark/JSFPSMonitor.ts +74 -0
  186. package/src/benchmark/roundToDecimalPlaces.ts +4 -0
  187. package/src/benchmark/useBenchmark.ts +240 -0
  188. package/src/benchmark/useBlankAreaTracker.ts +117 -0
  189. package/src/benchmark/useDataMultiplier.ts +19 -0
  190. package/src/benchmark/useFlatListBenchmark.ts +107 -0
  191. package/src/errors/CustomError.ts +10 -0
  192. package/src/errors/ExceptionList.ts +23 -0
  193. package/src/errors/Warnings.ts +18 -0
  194. package/src/index.ts +32 -0
  195. package/src/native/auto-layout/AutoLayoutView.tsx +72 -0
  196. package/src/native/auto-layout/AutoLayoutViewNativeComponent.ts +7 -0
  197. package/src/native/auto-layout/AutoLayoutViewNativeComponent.web.ts +8 -0
  198. package/src/native/auto-layout/AutoLayoutViewNativeComponentProps.ts +14 -0
  199. package/src/native/cell-container/CellContainer.ts +7 -0
  200. package/src/native/cell-container/CellContainer.web.tsx +9 -0
  201. package/src/utils/AverageWindow.ts +49 -0
  202. package/src/utils/PlatformHelper.ts +16 -0
  203. package/src/utils/PlatformHelper.web.ts +20 -0
  204. package/src/viewability/ViewToken.ts +7 -0
  205. package/src/viewability/ViewabilityHelper.ts +162 -0
  206. package/src/viewability/ViewabilityManager.ts +133 -0
@@ -0,0 +1,801 @@
1
+ import React from "react";
2
+ import {
3
+ View,
4
+ RefreshControl,
5
+ LayoutChangeEvent,
6
+ ViewStyle,
7
+ NativeSyntheticEvent,
8
+ NativeScrollEvent,
9
+ } from "react-native";
10
+ import {
11
+ BaseItemAnimator,
12
+ DataProvider,
13
+ ProgressiveListView,
14
+ RecyclerListView,
15
+ RecyclerListViewProps,
16
+ WindowCorrectionConfig,
17
+ } from "recyclerlistview";
18
+ import StickyContainer, { StickyContainerProps } from "recyclerlistview/sticky";
19
+
20
+ import AutoLayoutView from "./native/auto-layout/AutoLayoutView";
21
+ import CellContainer from "./native/cell-container/CellContainer";
22
+ import { PureComponentWrapper } from "./PureComponentWrapper";
23
+ import GridLayoutProviderWithProps from "./GridLayoutProviderWithProps";
24
+ import CustomError from "./errors/CustomError";
25
+ import ExceptionList from "./errors/ExceptionList";
26
+ import WarningList from "./errors/Warnings";
27
+ import ViewabilityManager from "./viewability/ViewabilityManager";
28
+ import { FlashListProps, ContentStyle } from "./FlashListProps";
29
+ import {
30
+ getCellContainerPlatformStyles,
31
+ getItemAnimator,
32
+ PlatformConfig,
33
+ } from "./utils/PlatformHelper";
34
+
35
+ interface StickyProps extends StickyContainerProps {
36
+ children: any;
37
+ }
38
+ const StickyHeaderContainer =
39
+ StickyContainer as React.ComponentClass<StickyProps>;
40
+
41
+ export interface FlashListState<T> {
42
+ dataProvider: DataProvider;
43
+ numColumns: number;
44
+ layoutProvider: GridLayoutProviderWithProps<T>;
45
+ data?: ReadonlyArray<T> | null;
46
+ extraData?: ExtraData<unknown>;
47
+ renderItem?: FlashListProps<T>["renderItem"];
48
+ }
49
+
50
+ interface ExtraData<T> {
51
+ value?: T;
52
+ }
53
+
54
+ class FlashList<T> extends React.PureComponent<
55
+ FlashListProps<T>,
56
+ FlashListState<T>
57
+ > {
58
+ private rlvRef?: RecyclerListView<RecyclerListViewProps, any>;
59
+ private stickyContentContainerRef?: PureComponentWrapper;
60
+ private listFixedDimensionSize = 0;
61
+ private transformStyle = { transform: [{ scaleY: -1 }] };
62
+ private distanceFromWindow = 0;
63
+ private contentStyle: ContentStyle = {};
64
+ private loadStartTime = 0;
65
+ private isListLoaded = false;
66
+ private windowCorrectionConfig: WindowCorrectionConfig = {
67
+ value: {
68
+ windowShift: 0,
69
+ startCorrection: 0,
70
+ endCorrection: 0,
71
+ },
72
+ applyToItemScroll: true,
73
+ applyToInitialOffset: true,
74
+ };
75
+
76
+ private emptyObject = {};
77
+ private postLoadTimeoutId?: ReturnType<typeof setTimeout>;
78
+ private sizeWarningTimeoutId?: ReturnType<typeof setTimeout>;
79
+
80
+ private isEmptyList = false;
81
+ private viewabilityManager: ViewabilityManager<T>;
82
+
83
+ private itemAnimator?: BaseItemAnimator;
84
+
85
+ static defaultProps = {
86
+ data: [],
87
+ numColumns: 1,
88
+ };
89
+
90
+ constructor(props: FlashListProps<T>) {
91
+ super(props);
92
+ this.loadStartTime = Date.now();
93
+ this.validateProps();
94
+ if (props.estimatedListSize) {
95
+ if (props.horizontal) {
96
+ this.listFixedDimensionSize = props.estimatedListSize.height;
97
+ } else {
98
+ this.listFixedDimensionSize = props.estimatedListSize.width;
99
+ }
100
+ }
101
+ this.distanceFromWindow =
102
+ props.estimatedFirstItemOffset ?? ((props.ListHeaderComponent && 1) || 0);
103
+ // eslint-disable-next-line react/state-in-constructor
104
+ this.state = FlashList.getInitialMutableState(this);
105
+ this.viewabilityManager = new ViewabilityManager(this);
106
+ this.itemAnimator = getItemAnimator();
107
+ }
108
+
109
+ private validateProps() {
110
+ if (this.props.onRefresh && typeof this.props.refreshing !== "boolean") {
111
+ throw new CustomError(ExceptionList.refreshBooleanMissing);
112
+ }
113
+ if (
114
+ Number(this.props.stickyHeaderIndices?.length) > 0 &&
115
+ this.props.horizontal
116
+ ) {
117
+ throw new CustomError(ExceptionList.stickyWhileHorizontalNotSupported);
118
+ }
119
+ if (Number(this.props.numColumns) > 1 && this.props.horizontal) {
120
+ throw new CustomError(ExceptionList.columnsWhileHorizontalNotSupported);
121
+ }
122
+
123
+ // `createAnimatedComponent` always passes a blank style object. To avoid warning while using AnimatedFlashList we've modified the check
124
+ if (Object.keys(this.props.style || {}).length > 0) {
125
+ console.warn(WarningList.styleUnsupported);
126
+ }
127
+ const contentStyleInfo = this.getContentContainerInfo();
128
+ if (contentStyleInfo.unsupportedKeys) {
129
+ console.warn(WarningList.styleContentContainerUnsupported);
130
+ }
131
+ if (contentStyleInfo.paddingIgnored) {
132
+ console.warn(WarningList.styleUnsupportedPaddingType);
133
+ }
134
+ }
135
+
136
+ // Some of the state variables need to update when props change
137
+ static getDerivedStateFromProps<T>(
138
+ nextProps: FlashListProps<T>,
139
+ prevState: FlashListState<T>
140
+ ): FlashListState<T> {
141
+ const newState = { ...prevState };
142
+ if (prevState.numColumns !== nextProps.numColumns) {
143
+ newState.numColumns = nextProps.numColumns || 1;
144
+ newState.layoutProvider = FlashList.getLayoutProvider<T>(
145
+ newState.numColumns,
146
+ nextProps
147
+ );
148
+ }
149
+ if (nextProps.data !== prevState.data) {
150
+ newState.data = nextProps.data;
151
+ newState.dataProvider = prevState.dataProvider.cloneWithRows(
152
+ nextProps.data as any[]
153
+ );
154
+ if (nextProps.renderItem !== prevState.renderItem) {
155
+ newState.extraData = { ...prevState.extraData };
156
+ }
157
+ }
158
+ if (nextProps.extraData !== prevState.extraData?.value) {
159
+ newState.extraData = { value: nextProps.extraData };
160
+ }
161
+ newState.renderItem = nextProps.renderItem;
162
+ newState.layoutProvider.updateProps(nextProps);
163
+ return newState;
164
+ }
165
+
166
+ private static getInitialMutableState<T>(
167
+ flashList: FlashList<T>
168
+ ): FlashListState<T> {
169
+ let getStableId: ((index: number) => string) | undefined;
170
+ if (
171
+ flashList.props.keyExtractor !== null &&
172
+ flashList.props.keyExtractor !== undefined
173
+ ) {
174
+ getStableId = (index) =>
175
+ // We assume `keyExtractor` function will never change from being `null | undefined` to defined and vice versa.
176
+ // Similarly, data should never be `null | undefined` when `getStableId` is called.
177
+ flashList.props.keyExtractor!(
178
+ flashList.props.data![index],
179
+ index
180
+ ).toString();
181
+ }
182
+ return {
183
+ data: null,
184
+ layoutProvider: null!!,
185
+ dataProvider: new DataProvider((r1, r2) => {
186
+ return r1 !== r2;
187
+ }, getStableId),
188
+ numColumns: 0,
189
+ };
190
+ }
191
+
192
+ // Using only grid layout provider as it can also act as a listview, sizeProvider is a function to support future overrides
193
+ private static getLayoutProvider<T>(
194
+ numColumns: number,
195
+ props: FlashListProps<T>
196
+ ) {
197
+ return new GridLayoutProviderWithProps<T>(
198
+ // max span or, total columns
199
+ numColumns,
200
+ (index, props) => {
201
+ // type of the item for given index
202
+ const type = props.getItemType?.(
203
+ props.data!![index],
204
+ index,
205
+ props.extraData
206
+ );
207
+ return type || 0;
208
+ },
209
+ (index, props, mutableLayout) => {
210
+ // span of the item at given index, item can choose to span more than one column
211
+ props.overrideItemLayout?.(
212
+ mutableLayout,
213
+ props.data!![index],
214
+ index,
215
+ numColumns,
216
+ props.extraData
217
+ );
218
+ return mutableLayout?.span ?? 1;
219
+ },
220
+ (index, props, mutableLayout) => {
221
+ // estimated size of the item an given index
222
+ props.overrideItemLayout?.(
223
+ mutableLayout,
224
+ props.data!![index],
225
+ index,
226
+ numColumns,
227
+ props.extraData
228
+ );
229
+ return mutableLayout?.size;
230
+ },
231
+ props
232
+ );
233
+ }
234
+
235
+ private onEndReached = () => {
236
+ this.props.onEndReached?.();
237
+ };
238
+
239
+ private getRefreshControl = () => {
240
+ if (this.props.onRefresh) {
241
+ return (
242
+ <RefreshControl
243
+ refreshing={Boolean(this.props.refreshing)}
244
+ progressViewOffset={this.props.progressViewOffset}
245
+ onRefresh={this.props.onRefresh}
246
+ />
247
+ );
248
+ }
249
+ };
250
+
251
+ componentDidMount() {
252
+ if (this.props.data?.length === 0) {
253
+ this.raiseOnLoadEventIfNeeded();
254
+ }
255
+ }
256
+
257
+ componentWillUnmount() {
258
+ this.viewabilityManager.dispose();
259
+ this.clearPostLoadTimeout();
260
+ if (this.sizeWarningTimeoutId !== undefined) {
261
+ clearTimeout(this.sizeWarningTimeoutId);
262
+ }
263
+ }
264
+
265
+ render() {
266
+ this.isEmptyList = this.state.dataProvider.getSize() === 0;
267
+ this.contentStyle = this.getContentContainerInfo().style;
268
+
269
+ const {
270
+ drawDistance,
271
+ removeClippedSubviews,
272
+ stickyHeaderIndices,
273
+ horizontal,
274
+ onEndReachedThreshold,
275
+ estimatedListSize,
276
+ initialScrollIndex,
277
+ style,
278
+ contentContainerStyle,
279
+ ...restProps
280
+ } = this.props;
281
+
282
+ // RecyclerListView simply ignores if initialScrollIndex is set to 0 because it doesn't understand headers
283
+ // Using initialOffset to force RLV to scroll to the right place
284
+ const initialOffset =
285
+ (this.isInitialScrollIndexInFirstRow() && this.distanceFromWindow) ||
286
+ undefined;
287
+ const finalDrawDistance =
288
+ drawDistance === undefined
289
+ ? PlatformConfig.defaultDrawDistance
290
+ : drawDistance;
291
+
292
+ return (
293
+ <StickyHeaderContainer
294
+ overrideRowRenderer={this.stickyRowRenderer}
295
+ applyWindowCorrection={this.applyWindowCorrection}
296
+ stickyHeaderIndices={stickyHeaderIndices}
297
+ style={
298
+ this.props.horizontal
299
+ ? this.emptyObject
300
+ : { flex: 1, ...this.getTransform() }
301
+ }
302
+ >
303
+ <ProgressiveListView
304
+ {...restProps}
305
+ ref={this.recyclerRef}
306
+ layoutProvider={this.state.layoutProvider}
307
+ dataProvider={this.state.dataProvider}
308
+ rowRenderer={this.emptyRowRenderer}
309
+ canChangeSize
310
+ isHorizontal={Boolean(horizontal)}
311
+ scrollViewProps={{
312
+ onScrollBeginDrag: this.onScrollBeginDrag,
313
+ onLayout: this.handleSizeChange,
314
+ refreshControl:
315
+ this.props.refreshControl || this.getRefreshControl(),
316
+
317
+ // Min values are being used to suppress RLV's bounded exception
318
+ style: { minHeight: 1, minWidth: 1 },
319
+ contentContainerStyle: {
320
+ backgroundColor: this.contentStyle.backgroundColor,
321
+
322
+ // Required to handle a scrollview bug. Check: https://github.com/Shopify/flash-list/pull/187
323
+ minHeight: 1,
324
+ minWidth: 1,
325
+ },
326
+ ...this.props.overrideProps,
327
+ }}
328
+ forceNonDeterministicRendering
329
+ renderItemContainer={this.itemContainer}
330
+ renderContentContainer={this.container}
331
+ onEndReached={this.onEndReached}
332
+ onEndReachedThresholdRelative={onEndReachedThreshold || undefined}
333
+ extendedState={this.state.extraData}
334
+ layoutSize={estimatedListSize}
335
+ maxRenderAhead={3 * finalDrawDistance}
336
+ finalRenderAheadOffset={finalDrawDistance}
337
+ renderAheadStep={finalDrawDistance}
338
+ initialRenderIndex={
339
+ (!this.isInitialScrollIndexInFirstRow() && initialScrollIndex) ||
340
+ undefined
341
+ }
342
+ initialOffset={initialOffset}
343
+ onItemLayout={this.onItemLayout}
344
+ onScroll={this.onScroll}
345
+ onVisibleIndicesChanged={
346
+ this.viewabilityManager.shouldListenToVisibleIndices
347
+ ? this.viewabilityManager.onVisibleIndicesChanged
348
+ : undefined
349
+ }
350
+ windowCorrectionConfig={this.getUpdatedWindowCorrectionConfig()}
351
+ itemAnimator={this.itemAnimator}
352
+ suppressBoundedSizeException
353
+ />
354
+ </StickyHeaderContainer>
355
+ );
356
+ }
357
+
358
+ private onScrollBeginDrag = (
359
+ event: NativeSyntheticEvent<NativeScrollEvent>
360
+ ) => {
361
+ this.recordInteraction();
362
+ this.props.onScrollBeginDrag?.(event);
363
+ };
364
+
365
+ private onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
366
+ this.recordInteraction();
367
+ this.viewabilityManager.updateViewableItems();
368
+ this.props.onScroll?.(event);
369
+ };
370
+
371
+ private getUpdatedWindowCorrectionConfig() {
372
+ // If the initial scroll index is in the first row then we're forcing RLV to use initialOffset and thus we need to disable window correction
373
+ // This isn't clean but it's the only way to get RLV to scroll to the right place
374
+ // TODO: Remove this when RLV fixes this. Current implementation will also fail if column span is overridden in the first row.
375
+ if (this.isInitialScrollIndexInFirstRow()) {
376
+ this.windowCorrectionConfig.applyToInitialOffset = false;
377
+ } else {
378
+ this.windowCorrectionConfig.applyToInitialOffset = true;
379
+ }
380
+ this.windowCorrectionConfig.value.windowShift = -this.distanceFromWindow;
381
+ return this.windowCorrectionConfig;
382
+ }
383
+
384
+ private isInitialScrollIndexInFirstRow() {
385
+ return (
386
+ (this.props.initialScrollIndex ?? this.state.numColumns) <
387
+ this.state.numColumns
388
+ );
389
+ }
390
+
391
+ private validateListSize(event: LayoutChangeEvent) {
392
+ const { height, width } = event.nativeEvent.layout;
393
+ if (Math.floor(height) <= 1 || Math.floor(width) <= 1) {
394
+ console.warn(WarningList.unusableRenderedSize);
395
+ }
396
+ }
397
+
398
+ private handleSizeChange = (event: LayoutChangeEvent) => {
399
+ this.validateListSize(event);
400
+ const newSize = this.props.horizontal
401
+ ? event.nativeEvent.layout.height
402
+ : event.nativeEvent.layout.width;
403
+ const oldSize = this.listFixedDimensionSize;
404
+ this.listFixedDimensionSize = newSize;
405
+
406
+ // >0 check is to avoid rerender on mount where it would be redundant
407
+ if (oldSize > 0 && oldSize !== newSize) {
408
+ this.rlvRef?.forceRerender();
409
+ }
410
+ if (this.props.onLayout) {
411
+ this.props.onLayout(event);
412
+ }
413
+ };
414
+
415
+ private container = (props: object, children: React.ReactNode[]) => {
416
+ this.clearPostLoadTimeout();
417
+ return (
418
+ <>
419
+ <PureComponentWrapper
420
+ enabled={children.length > 0 || this.isEmptyList}
421
+ contentStyle={this.props.contentContainerStyle}
422
+ horizontal={this.props.horizontal}
423
+ header={this.props.ListHeaderComponent}
424
+ extraData={this.state.extraData}
425
+ headerStyle={this.props.ListHeaderComponentStyle}
426
+ inverted={this.props.inverted}
427
+ renderer={this.header}
428
+ />
429
+ <AutoLayoutView
430
+ {...props}
431
+ onBlankAreaEvent={this.props.onBlankArea}
432
+ onLayout={this.updateDistanceFromWindow}
433
+ disableAutoLayout={this.props.disableAutoLayout}
434
+ >
435
+ {children}
436
+ </AutoLayoutView>
437
+ {this.isEmptyList
438
+ ? this.getValidComponent(this.props.ListEmptyComponent)
439
+ : null}
440
+ <PureComponentWrapper
441
+ enabled={children.length > 0 || this.isEmptyList}
442
+ contentStyle={this.props.contentContainerStyle}
443
+ horizontal={this.props.horizontal}
444
+ header={this.props.ListFooterComponent}
445
+ extraData={this.state.extraData}
446
+ headerStyle={this.props.ListFooterComponentStyle}
447
+ inverted={this.props.inverted}
448
+ renderer={this.footer}
449
+ />
450
+ {this.getComponentForHeightMeasurement()}
451
+ </>
452
+ );
453
+ };
454
+
455
+ private itemContainer = (props: any, parentProps: any) => {
456
+ const CellRendererComponent =
457
+ this.props.CellRendererComponent ?? CellContainer;
458
+ return (
459
+ <CellRendererComponent
460
+ {...props}
461
+ style={{
462
+ ...props.style,
463
+ flexDirection: this.props.horizontal ? "row" : "column",
464
+ alignItems: "stretch",
465
+ ...this.getTransform(),
466
+ ...getCellContainerPlatformStyles(this.props.inverted!!, parentProps),
467
+ }}
468
+ index={parentProps.index}
469
+ >
470
+ <PureComponentWrapper
471
+ extendedState={parentProps.extendedState}
472
+ internalSnapshot={parentProps.internalSnapshot}
473
+ data={parentProps.data}
474
+ arg={parentProps.index}
475
+ renderer={this.getCellContainerChild}
476
+ />
477
+ </CellRendererComponent>
478
+ );
479
+ };
480
+
481
+ private updateDistanceFromWindow = (event: LayoutChangeEvent) => {
482
+ this.distanceFromWindow = this.props.horizontal
483
+ ? event.nativeEvent.layout.x
484
+ : event.nativeEvent.layout.y;
485
+ this.windowCorrectionConfig.value.windowShift = -this.distanceFromWindow;
486
+ };
487
+
488
+ private getTransform() {
489
+ return (this.props.inverted && this.transformStyle) || undefined;
490
+ }
491
+
492
+ private getContentContainerInfo() {
493
+ const {
494
+ paddingTop,
495
+ paddingRight,
496
+ paddingBottom,
497
+ paddingLeft,
498
+ padding,
499
+ paddingVertical,
500
+ paddingHorizontal,
501
+ backgroundColor,
502
+ ...rest
503
+ } = (this.props.contentContainerStyle || {}) as ViewStyle;
504
+ const unsupportedKeys = Object.keys(rest).length > 0;
505
+ if (this.props.horizontal) {
506
+ const paddingIgnored =
507
+ padding || paddingVertical || paddingTop || paddingBottom;
508
+ return {
509
+ style: {
510
+ paddingLeft: paddingLeft || paddingHorizontal || padding || 0,
511
+ paddingRight: paddingRight || paddingHorizontal || padding || 0,
512
+ backgroundColor,
513
+ },
514
+ unsupportedKeys,
515
+ paddingIgnored,
516
+ };
517
+ } else {
518
+ const paddingIgnored =
519
+ padding || paddingHorizontal || paddingLeft || paddingRight;
520
+ return {
521
+ style: {
522
+ paddingTop: paddingTop || paddingVertical || padding || 0,
523
+ paddingBottom: paddingBottom || paddingVertical || padding || 0,
524
+ backgroundColor,
525
+ },
526
+ unsupportedKeys,
527
+ paddingIgnored,
528
+ };
529
+ }
530
+ }
531
+
532
+ private separator = (index: number) => {
533
+ const leadingItem = this.props.data?.[index];
534
+ const trailingItem = this.props.data?.[index + 1];
535
+ if (trailingItem === undefined) {
536
+ return null;
537
+ }
538
+ const props = {
539
+ leadingItem,
540
+ trailingItem,
541
+ // TODO: Missing sections as we don't have this feature implemented yet. Implement section, leadingSection and trailingSection.
542
+ // https://github.com/facebook/react-native/blob/8bd3edec88148d0ab1f225d2119435681fbbba33/Libraries/Lists/VirtualizedSectionList.js#L285-L294
543
+ };
544
+ const Separator = this.props.ItemSeparatorComponent;
545
+ return Separator && <Separator {...props} />;
546
+ };
547
+
548
+ private header = () => {
549
+ return (
550
+ <>
551
+ <View
552
+ style={{
553
+ paddingTop: this.contentStyle.paddingTop,
554
+ paddingLeft: this.contentStyle.paddingLeft,
555
+ }}
556
+ />
557
+
558
+ <View
559
+ style={[this.props.ListHeaderComponentStyle, this.getTransform()]}
560
+ >
561
+ {this.getValidComponent(this.props.ListHeaderComponent)}
562
+ </View>
563
+ </>
564
+ );
565
+ };
566
+
567
+ private footer = () => {
568
+ return (
569
+ <>
570
+ <View
571
+ style={[this.props.ListFooterComponentStyle, this.getTransform()]}
572
+ >
573
+ {this.getValidComponent(this.props.ListFooterComponent)}
574
+ </View>
575
+ <View
576
+ style={{
577
+ paddingBottom: this.contentStyle.paddingBottom,
578
+ paddingRight: this.contentStyle.paddingRight,
579
+ }}
580
+ />
581
+ </>
582
+ );
583
+ };
584
+
585
+ private getComponentForHeightMeasurement = () => {
586
+ return this.props.horizontal &&
587
+ !this.props.disableHorizontalListHeightMeasurement &&
588
+ !this.isListLoaded &&
589
+ this.state.dataProvider.getSize() > 0 ? (
590
+ <View style={{ opacity: 0 }} pointerEvents="none">
591
+ {this.rowRendererWithIndex(
592
+ Math.min(this.state.dataProvider.getSize() - 1, 1)
593
+ )}
594
+ </View>
595
+ ) : null;
596
+ };
597
+
598
+ private getValidComponent(
599
+ component: React.ComponentType | React.ReactElement | null | undefined
600
+ ) {
601
+ const PassedComponent = component;
602
+ return (
603
+ (React.isValidElement(PassedComponent) && PassedComponent) ||
604
+ (PassedComponent && <PassedComponent />) ||
605
+ null
606
+ );
607
+ }
608
+
609
+ private applyWindowCorrection = (
610
+ _: any,
611
+ __: any,
612
+ correctionObject: { windowShift: number }
613
+ ) => {
614
+ correctionObject.windowShift = -this.distanceFromWindow;
615
+ this.stickyContentContainerRef?.setEnabled(this.isStickyEnabled);
616
+ };
617
+
618
+ private rowRendererWithIndex = (index: number) => {
619
+ // known issue: expected to pass separators which isn't available in RLV
620
+ return this.props.renderItem?.({
621
+ item: this.props.data?.[index],
622
+ index,
623
+ extraData: this.state.extraData?.value,
624
+ } as any) as JSX.Element;
625
+ };
626
+
627
+ /**
628
+ * This will prevent render item calls unless data changes.
629
+ * Output of this method is received as children object so returning null here is no issue as long as we handle it inside our child container.
630
+ * @module getCellContainerChild acts as the new rowRenderer and is called directly from our child container.
631
+ */
632
+ private emptyRowRenderer = () => {
633
+ return null;
634
+ };
635
+
636
+ private getCellContainerChild = (index: number) => {
637
+ return (
638
+ <>
639
+ <View
640
+ style={{
641
+ flexDirection:
642
+ this.props.horizontal || this.props.numColumns === 1
643
+ ? "column"
644
+ : "row",
645
+ }}
646
+ >
647
+ {this.rowRendererWithIndex(index)}
648
+ </View>
649
+ {this.separator(index)}
650
+ </>
651
+ );
652
+ };
653
+
654
+ private recyclerRef = (ref: any) => {
655
+ this.rlvRef = ref;
656
+ };
657
+
658
+ private stickyContentRef = (ref: any) => {
659
+ this.stickyContentContainerRef = ref;
660
+ };
661
+
662
+ private stickyRowRenderer = (_: any, __: any, index: number, ___: any) => {
663
+ return (
664
+ <PureComponentWrapper
665
+ ref={this.stickyContentRef}
666
+ enabled={this.isStickyEnabled}
667
+ arg={index}
668
+ renderer={this.rowRendererWithIndex}
669
+ />
670
+ );
671
+ };
672
+
673
+ private get isStickyEnabled() {
674
+ const currentOffset = this.rlvRef?.getCurrentScrollOffset() || 0;
675
+ return currentOffset >= this.distanceFromWindow;
676
+ }
677
+
678
+ private onItemLayout = (index: number) => {
679
+ // Informing the layout provider about change to an item's layout. It already knows the dimensions so there's not need to pass them.
680
+ this.state.layoutProvider.reportItemLayout(index);
681
+ this.raiseOnLoadEventIfNeeded();
682
+ };
683
+
684
+ private raiseOnLoadEventIfNeeded = () => {
685
+ if (!this.isListLoaded) {
686
+ this.isListLoaded = true;
687
+ this.props.onLoad?.({
688
+ elapsedTimeInMs: Date.now() - this.loadStartTime,
689
+ });
690
+ this.runAfterOnLoad();
691
+ }
692
+ };
693
+
694
+ private runAfterOnLoad = () => {
695
+ if (this.props.estimatedItemSize === undefined) {
696
+ this.sizeWarningTimeoutId = setTimeout(() => {
697
+ const averageItemSize = Math.floor(
698
+ this.state.layoutProvider.averageItemSize
699
+ );
700
+ console.warn(
701
+ WarningList.estimatedItemSizeMissingWarning.replace(
702
+ "@size",
703
+ averageItemSize.toString()
704
+ )
705
+ );
706
+ }, 1000);
707
+ }
708
+ this.postLoadTimeoutId = setTimeout(() => {
709
+ // This force update is required to remove dummy element rendered to measure horizontal list height when the list doesn't update on its own.
710
+ // In most cases this timeout will never be triggered because list usually updates atleast once and this timeout is cleared on update.
711
+ if (this.props.horizontal) {
712
+ this.forceUpdate();
713
+ }
714
+ }, 500);
715
+ };
716
+
717
+ private clearPostLoadTimeout = () => {
718
+ if (this.postLoadTimeoutId !== undefined) {
719
+ clearTimeout(this.postLoadTimeoutId);
720
+ this.postLoadTimeoutId = undefined;
721
+ }
722
+ };
723
+
724
+ /**
725
+ * Disables recycling for the next frame so that layout animations run well.
726
+ * Warning: Avoid this when making large changes to the data as the list might draw too much to run animations. Single item insertions/deletions
727
+ * should be good. With recycling paused the list cannot do much optimization.
728
+ * The next render will run as normal and reuse items.
729
+ */
730
+ public prepareForLayoutAnimationRender(): void {
731
+ if (
732
+ this.props.keyExtractor === null ||
733
+ this.props.keyExtractor === undefined
734
+ ) {
735
+ console.warn(WarningList.missingKeyExtractor);
736
+ } else {
737
+ this.rlvRef?.prepareForLayoutAnimationRender();
738
+ }
739
+ }
740
+
741
+ public scrollToEnd(params?: { animated?: boolean | null | undefined }) {
742
+ this.rlvRef?.scrollToEnd(Boolean(params?.animated));
743
+ }
744
+
745
+ public scrollToIndex(params: {
746
+ animated?: boolean | null | undefined;
747
+ index: number;
748
+ viewOffset?: number | undefined;
749
+ viewPosition?: number | undefined;
750
+ }) {
751
+ // known issue: no support for view offset/position
752
+ this.rlvRef?.scrollToIndex(params.index, Boolean(params.animated));
753
+ }
754
+
755
+ public scrollToItem(params: {
756
+ animated?: boolean | null | undefined;
757
+ item: any;
758
+ viewPosition?: number | undefined;
759
+ }) {
760
+ this.rlvRef?.scrollToItem(params.item, Boolean(params.animated));
761
+ }
762
+
763
+ public scrollToOffset(params: {
764
+ animated?: boolean | null | undefined;
765
+ offset: number;
766
+ }) {
767
+ const x = this.props.horizontal ? params.offset : 0;
768
+ const y = this.props.horizontal ? 0 : params.offset;
769
+ this.rlvRef?.scrollToOffset(x, y, Boolean(params.animated));
770
+ }
771
+
772
+ public getScrollableNode(): number | null {
773
+ return this.rlvRef?.getScrollableNode?.() || null;
774
+ }
775
+
776
+ /**
777
+ * Allows access to internal recyclerlistview. This is useful for enabling access to its public APIs.
778
+ * Warning: We may swap recyclerlistview for something else in the future. Use with caution.
779
+ */
780
+ /* eslint-disable @typescript-eslint/naming-convention */
781
+ public get recyclerlistview_unsafe() {
782
+ return this.rlvRef;
783
+ }
784
+
785
+ /**
786
+ * Specifies how far the first item is from top of the list. This would normally be a sum of header size and top/left padding applied to the list.
787
+ */
788
+ public get firstItemOffset() {
789
+ return this.distanceFromWindow;
790
+ }
791
+
792
+ /**
793
+ * Tells the list an interaction has occurred, which should trigger viewability calculations, e.g. if waitForInteractions is true and the user has not scrolled.
794
+ * This is typically called by taps on items or by navigation actions.
795
+ */
796
+ public recordInteraction = () => {
797
+ this.viewabilityManager.recordInteraction();
798
+ };
799
+ }
800
+
801
+ export default FlashList;