@shopify/flash-list 2.0.0-alpha.9 → 2.0.0-rc.10

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 (209) hide show
  1. package/README.md +37 -97
  2. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/BlankAreaEvent.kt +2 -2
  3. package/dist/AnimatedFlashList.d.ts.map +1 -1
  4. package/dist/AnimatedFlashList.js +3 -3
  5. package/dist/AnimatedFlashList.js.map +1 -1
  6. package/dist/FlashList.d.ts +9 -0
  7. package/dist/FlashList.d.ts.map +1 -1
  8. package/dist/FlashList.js +20 -0
  9. package/dist/FlashList.js.map +1 -1
  10. package/dist/FlashListProps.d.ts +30 -10
  11. package/dist/FlashListProps.d.ts.map +1 -1
  12. package/dist/FlashListProps.js.map +1 -1
  13. package/dist/FlashListRef.d.ts +305 -0
  14. package/dist/FlashListRef.d.ts.map +1 -0
  15. package/dist/FlashListRef.js +3 -0
  16. package/dist/FlashListRef.js.map +1 -0
  17. package/dist/MasonryFlashList.js.map +1 -1
  18. package/dist/__tests__/RecyclerView.test.js +72 -28
  19. package/dist/__tests__/RecyclerView.test.js.map +1 -1
  20. package/dist/__tests__/RenderStackManager.test.d.ts +2 -0
  21. package/dist/__tests__/RenderStackManager.test.d.ts.map +1 -0
  22. package/dist/__tests__/RenderStackManager.test.js +485 -0
  23. package/dist/__tests__/RenderStackManager.test.js.map +1 -0
  24. package/dist/__tests__/helpers/createLayoutManager.d.ts.map +1 -1
  25. package/dist/__tests__/helpers/createLayoutManager.js +3 -4
  26. package/dist/__tests__/helpers/createLayoutManager.js.map +1 -1
  27. package/dist/__tests__/useUnmountAwareCallbacks.test.js +1 -1
  28. package/dist/__tests__/useUnmountAwareCallbacks.test.js.map +1 -1
  29. package/dist/benchmark/useBenchmark.js +0 -25
  30. package/dist/benchmark/useBenchmark.js.map +1 -1
  31. package/dist/benchmark/useFlatListBenchmark.js +8 -7
  32. package/dist/benchmark/useFlatListBenchmark.js.map +1 -1
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/native/config/PlatformHelper.android.d.ts +1 -0
  38. package/dist/native/config/PlatformHelper.android.d.ts.map +1 -1
  39. package/dist/native/config/PlatformHelper.android.js +1 -0
  40. package/dist/native/config/PlatformHelper.android.js.map +1 -1
  41. package/dist/native/config/PlatformHelper.d.ts +1 -0
  42. package/dist/native/config/PlatformHelper.d.ts.map +1 -1
  43. package/dist/native/config/PlatformHelper.ios.d.ts +1 -0
  44. package/dist/native/config/PlatformHelper.ios.d.ts.map +1 -1
  45. package/dist/native/config/PlatformHelper.ios.js +1 -0
  46. package/dist/native/config/PlatformHelper.ios.js.map +1 -1
  47. package/dist/native/config/PlatformHelper.js +1 -0
  48. package/dist/native/config/PlatformHelper.js.map +1 -1
  49. package/dist/native/config/PlatformHelper.web.d.ts +1 -0
  50. package/dist/native/config/PlatformHelper.web.d.ts.map +1 -1
  51. package/dist/native/config/PlatformHelper.web.js +1 -0
  52. package/dist/native/config/PlatformHelper.web.js.map +1 -1
  53. package/dist/recyclerview/RecyclerView.d.ts +2 -1
  54. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  55. package/dist/recyclerview/RecyclerView.js +104 -57
  56. package/dist/recyclerview/RecyclerView.js.map +1 -1
  57. package/dist/recyclerview/RecyclerViewContextProvider.d.ts +41 -6
  58. package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -1
  59. package/dist/recyclerview/RecyclerViewContextProvider.js +4 -0
  60. package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
  61. package/dist/recyclerview/RecyclerViewManager.d.ts +24 -7
  62. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  63. package/dist/recyclerview/RecyclerViewManager.js +119 -113
  64. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  65. package/dist/recyclerview/RenderStackManager.d.ts +86 -0
  66. package/dist/recyclerview/RenderStackManager.d.ts.map +1 -0
  67. package/dist/recyclerview/RenderStackManager.js +343 -0
  68. package/dist/recyclerview/RenderStackManager.js.map +1 -0
  69. package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
  70. package/dist/recyclerview/ViewHolder.js +5 -3
  71. package/dist/recyclerview/ViewHolder.js.map +1 -1
  72. package/dist/recyclerview/ViewHolderCollection.d.ts +9 -3
  73. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
  74. package/dist/recyclerview/ViewHolderCollection.js +26 -9
  75. package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
  76. package/dist/recyclerview/components/ScrollAnchor.d.ts +2 -2
  77. package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
  78. package/dist/recyclerview/components/ScrollAnchor.js +9 -5
  79. package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
  80. package/dist/recyclerview/components/StickyHeaders.d.ts +1 -1
  81. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
  82. package/dist/recyclerview/components/StickyHeaders.js +40 -33
  83. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  84. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +45 -1
  85. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -1
  86. package/dist/recyclerview/helpers/EngagedIndicesTracker.js +77 -20
  87. package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -1
  88. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +11 -0
  89. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -0
  90. package/dist/recyclerview/helpers/RenderTimeTracker.js +42 -0
  91. package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -0
  92. package/dist/recyclerview/helpers/VelocityTracker.d.ts +29 -0
  93. package/dist/recyclerview/helpers/VelocityTracker.d.ts.map +1 -0
  94. package/dist/recyclerview/helpers/VelocityTracker.js +70 -0
  95. package/dist/recyclerview/helpers/VelocityTracker.js.map +1 -0
  96. package/dist/recyclerview/hooks/useBoundDetection.d.ts +1 -2
  97. package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
  98. package/dist/recyclerview/hooks/useBoundDetection.js +56 -22
  99. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
  100. package/dist/recyclerview/hooks/useLayoutState.d.ts +3 -1
  101. package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -1
  102. package/dist/recyclerview/hooks/useLayoutState.js +5 -3
  103. package/dist/recyclerview/hooks/useLayoutState.js.map +1 -1
  104. package/dist/recyclerview/hooks/useMappingHelper.d.ts +1 -1
  105. package/dist/recyclerview/hooks/useMappingHelper.d.ts.map +1 -1
  106. package/dist/recyclerview/hooks/useMappingHelper.js +1 -1
  107. package/dist/recyclerview/hooks/useMappingHelper.js.map +1 -1
  108. package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -1
  109. package/dist/recyclerview/hooks/useOnLoad.js +4 -6
  110. package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
  111. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +5 -49
  112. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  113. package/dist/recyclerview/hooks/useRecyclerViewController.js +315 -204
  114. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  115. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +2 -0
  116. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
  117. package/dist/recyclerview/hooks/useRecyclerViewManager.js +11 -1
  118. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
  119. package/dist/recyclerview/hooks/useRecyclingState.d.ts +4 -2
  120. package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -1
  121. package/dist/recyclerview/hooks/useRecyclingState.js +2 -2
  122. package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -1
  123. package/dist/recyclerview/hooks/useSecondaryProps.js +1 -1
  124. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts +10 -3
  125. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts.map +1 -1
  126. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +33 -4
  127. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
  128. package/dist/recyclerview/hooks/useUnmountFlag.d.ts.map +1 -1
  129. package/dist/recyclerview/hooks/useUnmountFlag.js +1 -0
  130. package/dist/recyclerview/hooks/useUnmountFlag.js.map +1 -1
  131. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +18 -4
  132. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  133. package/dist/recyclerview/layout-managers/GridLayoutManager.js +60 -21
  134. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  135. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +35 -21
  136. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
  137. package/dist/recyclerview/layout-managers/LayoutManager.js +92 -28
  138. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  139. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +9 -1
  140. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -1
  141. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +28 -12
  142. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
  143. package/dist/recyclerview/utils/measureLayout.web.d.ts.map +1 -1
  144. package/dist/recyclerview/utils/measureLayout.web.js +1 -3
  145. package/dist/recyclerview/utils/measureLayout.web.js.map +1 -1
  146. package/dist/tsconfig.tsbuildinfo +1 -1
  147. package/dist/viewability/ViewToken.d.ts +2 -2
  148. package/dist/viewability/ViewToken.d.ts.map +1 -1
  149. package/dist/viewability/ViewabilityHelper.js +1 -1
  150. package/dist/viewability/ViewabilityHelper.js.map +1 -1
  151. package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
  152. package/dist/viewability/ViewabilityManager.js +11 -5
  153. package/dist/viewability/ViewabilityManager.js.map +1 -1
  154. package/jestSetup.js +30 -11
  155. package/package.json +2 -1
  156. package/src/AnimatedFlashList.ts +3 -2
  157. package/src/FlashList.tsx +24 -0
  158. package/src/FlashListProps.ts +41 -10
  159. package/src/FlashListRef.ts +320 -0
  160. package/src/MasonryFlashList.tsx +2 -2
  161. package/src/__tests__/RecyclerView.test.tsx +106 -31
  162. package/src/__tests__/RenderStackManager.test.ts +574 -0
  163. package/src/__tests__/helpers/createLayoutManager.ts +2 -3
  164. package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
  165. package/src/benchmark/useBenchmark.ts +0 -37
  166. package/src/benchmark/useFlatListBenchmark.ts +2 -2
  167. package/src/index.ts +2 -1
  168. package/src/native/config/PlatformHelper.android.ts +1 -0
  169. package/src/native/config/PlatformHelper.ios.ts +1 -0
  170. package/src/native/config/PlatformHelper.ts +1 -0
  171. package/src/native/config/PlatformHelper.web.ts +1 -0
  172. package/src/recyclerview/RecyclerView.tsx +139 -75
  173. package/src/recyclerview/RecyclerViewContextProvider.ts +52 -7
  174. package/src/recyclerview/RecyclerViewManager.ts +135 -98
  175. package/src/recyclerview/RenderStackManager.ts +317 -0
  176. package/src/recyclerview/ViewHolder.tsx +5 -3
  177. package/src/recyclerview/ViewHolderCollection.tsx +42 -14
  178. package/src/recyclerview/components/ScrollAnchor.tsx +21 -9
  179. package/src/recyclerview/components/StickyHeaders.tsx +63 -45
  180. package/src/recyclerview/helpers/EngagedIndicesTracker.ts +118 -23
  181. package/src/recyclerview/helpers/RenderTimeTracker.ts +42 -0
  182. package/src/recyclerview/helpers/VelocityTracker.ts +77 -0
  183. package/src/recyclerview/hooks/useBoundDetection.ts +72 -23
  184. package/src/recyclerview/hooks/useLayoutState.ts +15 -6
  185. package/src/recyclerview/hooks/useMappingHelper.ts +1 -1
  186. package/src/recyclerview/hooks/useOnLoad.ts +4 -6
  187. package/src/recyclerview/hooks/useRecyclerViewController.tsx +364 -254
  188. package/src/recyclerview/hooks/useRecyclerViewManager.ts +13 -1
  189. package/src/recyclerview/hooks/useRecyclingState.ts +11 -7
  190. package/src/recyclerview/hooks/useSecondaryProps.tsx +1 -1
  191. package/src/recyclerview/hooks/useUnmountAwareCallbacks.ts +39 -3
  192. package/src/recyclerview/hooks/useUnmountFlag.ts +1 -0
  193. package/src/recyclerview/layout-managers/GridLayoutManager.ts +67 -23
  194. package/src/recyclerview/layout-managers/LayoutManager.ts +110 -41
  195. package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +30 -8
  196. package/src/recyclerview/utils/measureLayout.web.ts +1 -3
  197. package/src/viewability/ViewToken.ts +2 -2
  198. package/src/viewability/ViewabilityHelper.ts +1 -1
  199. package/src/viewability/ViewabilityManager.ts +16 -9
  200. package/dist/__tests__/RecycleKeyManager.test.d.ts +0 -2
  201. package/dist/__tests__/RecycleKeyManager.test.d.ts.map +0 -1
  202. package/dist/__tests__/RecycleKeyManager.test.js +0 -210
  203. package/dist/__tests__/RecycleKeyManager.test.js.map +0 -1
  204. package/dist/recyclerview/RecycleKeyManager.d.ts +0 -82
  205. package/dist/recyclerview/RecycleKeyManager.d.ts.map +0 -1
  206. package/dist/recyclerview/RecycleKeyManager.js +0 -135
  207. package/dist/recyclerview/RecycleKeyManager.js.map +0 -1
  208. package/src/__tests__/RecycleKeyManager.test.ts +0 -254
  209. package/src/recyclerview/RecycleKeyManager.ts +0 -185
@@ -8,6 +8,10 @@ export interface RVEngagedIndicesTracker {
8
8
  scrollOffset: number;
9
9
  // Total distance (in pixels) to pre-render items before and after the visible viewport
10
10
  drawDistance: number;
11
+ // Whether to use offset projection to predict the next scroll offset
12
+ enableOffsetProjection: boolean;
13
+ // Average render time of the list
14
+ averageRenderTime: number;
11
15
 
12
16
  /**
13
17
  * Updates the scroll offset and calculates which items should be rendered (engaged indices).
@@ -21,9 +25,31 @@ export interface RVEngagedIndicesTracker {
21
25
  velocity: Velocity | null | undefined,
22
26
  layoutManager: RVLayoutManager
23
27
  ) => ConsecutiveNumbers | undefined;
28
+
29
+ /**
30
+ * Returns the currently engaged (rendered) indices.
31
+ * This includes both visible items and buffer items.
32
+ * @returns The last computed set of engaged indices
33
+ */
24
34
  getEngagedIndices: () => ConsecutiveNumbers;
35
+
36
+ /**
37
+ * Computes the visible indices in the viewport.
38
+ * @param layoutManager - Layout manager to fetch item positions and dimensions
39
+ * @returns Indices of items currently visible in the viewport
40
+ */
25
41
  computeVisibleIndices: (layoutManager: RVLayoutManager) => ConsecutiveNumbers;
42
+
43
+ /**
44
+ * Sets the scroll direction for velocity history tracking.
45
+ * @param scrollDirection - The direction of scrolling ("forward" or "backward")
46
+ */
26
47
  setScrollDirection: (scrollDirection: "forward" | "backward") => void;
48
+
49
+ /**
50
+ * Resets the velocity history based on the current scroll direction.
51
+ */
52
+ resetVelocityHistory: () => void;
27
53
  }
28
54
 
29
55
  export interface Velocity {
@@ -35,17 +61,26 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
35
61
  // Current scroll position of the list
36
62
  public scrollOffset = 0;
37
63
  // Distance to pre-render items before and after the visible viewport (in pixels)
38
- // TODO: Increase this value for web
39
64
  public drawDistance = PlatformConfig.defaultDrawDistance;
65
+
66
+ // Whether to use offset projection to predict the next scroll offset
67
+ public enableOffsetProjection = true;
68
+
69
+ // Average render time of the list
70
+ public averageRenderTime = 16;
71
+
72
+ // Internal override to disable offset projection
73
+ private forceDisableOffsetProjection = false;
74
+
40
75
  // Currently rendered item indices (including buffer items)
41
76
  private engagedIndices = ConsecutiveNumbers.EMPTY;
42
77
 
43
78
  // Buffer distribution multipliers for scroll direction optimization
44
- private smallMultiplier = 0.1; // Used for buffer in the opposite direction of scroll
45
- private largeMultiplier = 0.9; // Used for buffer in the direction of scroll
79
+ private smallMultiplier = 0.3; // Used for buffer in the opposite direction of scroll
80
+ private largeMultiplier = 0.7; // Used for buffer in the direction of scroll
46
81
 
47
82
  // Circular buffer to track recent scroll velocities for direction detection
48
- private velocityHistory = [-1, -1, -1, -1, -1];
83
+ private velocityHistory = [0, 0, 0, -0.1, -0.1];
49
84
  private velocityIndex = 0;
50
85
 
51
86
  /**
@@ -67,7 +102,21 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
67
102
  // STEP 1: Determine the currently visible viewport
68
103
  const windowSize = layoutManager.getWindowsSize();
69
104
  const isHorizontal = layoutManager.isHorizontal();
70
- const viewportStart = offset;
105
+
106
+ // Update velocity history
107
+ if (velocity) {
108
+ this.updateVelocityHistory(isHorizontal ? velocity.x : velocity.y);
109
+ }
110
+
111
+ // Determine scroll direction to optimize buffer distribution
112
+ const isScrollingBackward = this.isScrollingBackward();
113
+ const viewportStart =
114
+ this.enableOffsetProjection && !this.forceDisableOffsetProjection
115
+ ? this.getProjectedScrollOffset(offset, this.averageRenderTime)
116
+ : offset;
117
+
118
+ // console.log("timeMs", this.averageRenderTime, offset, viewportStart);
119
+
71
120
  const viewportSize = isHorizontal ? windowSize.width : windowSize.height;
72
121
  const viewportEnd = viewportStart + viewportSize;
73
122
 
@@ -75,11 +124,6 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
75
124
  // The total extra space where items will be pre-rendered
76
125
  const totalBuffer = this.drawDistance * 2;
77
126
 
78
- // Determine scroll direction to optimize buffer distribution
79
- const isScrollingBackward = this.isScrollingBackward(
80
- isHorizontal ? velocity?.x : velocity?.y
81
- );
82
-
83
127
  // Distribute more buffer in the direction of scrolling
84
128
  // When scrolling forward: more buffer after viewport
85
129
  // When scrolling backward: more buffer before viewport
@@ -123,9 +167,12 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
123
167
  extendedStart,
124
168
  extendedEnd
125
169
  );
126
- if (!isHorizontal) {
127
- // console.log("newEngagedIndices", newEngagedIndices, this.scrollOffset);
128
- }
170
+ // console.log(
171
+ // "newEngagedIndices",
172
+ // newEngagedIndices,
173
+ // this.scrollOffset,
174
+ // viewportStart
175
+ // );
129
176
  // Only return new indices if they've changed
130
177
  const oldEngagedIndices = this.engagedIndices;
131
178
  this.engagedIndices = newEngagedIndices;
@@ -135,19 +182,21 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
135
182
  : newEngagedIndices;
136
183
  }
137
184
 
185
+ /**
186
+ * Updates the velocity history with a new velocity value.
187
+ * @param velocity - Current scroll velocity component (x or y)
188
+ */
189
+ private updateVelocityHistory(velocity: number) {
190
+ this.velocityHistory[this.velocityIndex] = velocity;
191
+ this.velocityIndex = (this.velocityIndex + 1) % this.velocityHistory.length;
192
+ }
193
+
138
194
  /**
139
195
  * Determines scroll direction by analyzing recent velocity history.
140
196
  * Uses a majority voting system on the last 5 velocity values.
141
- * @param velocity - Current scroll velocity component (x or y)
142
197
  * @returns true if scrolling backward (negative direction), false otherwise
143
198
  */
144
- private isScrollingBackward(velocity?: number): boolean {
145
- // update velocity history
146
- if (velocity) {
147
- this.velocityHistory[this.velocityIndex] = velocity;
148
- this.velocityIndex =
149
- (this.velocityIndex + 1) % this.velocityHistory.length;
150
- }
199
+ private isScrollingBackward(): boolean {
151
200
  // should decide based on whether we have more positive or negative values, use for loop
152
201
  let positiveCount = 0;
153
202
  let negativeCount = 0;
@@ -162,6 +211,40 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
162
211
  return positiveCount < negativeCount;
163
212
  }
164
213
 
214
+ /**
215
+ * Calculates the median velocity based on velocity history
216
+ * Medina works better agains outliers
217
+ * @returns Median velocity over the recent history
218
+ */
219
+ private getMedianVelocity(): number {
220
+ // Make a copy of velocity history and sort it
221
+ const sortedVelocities = [...this.velocityHistory].sort(
222
+ (valueA, valueB) => valueA - valueB
223
+ );
224
+ const length = sortedVelocities.length;
225
+
226
+ // If length is odd, return the middle element
227
+ if (length % 2 === 1) {
228
+ return sortedVelocities[Math.floor(length / 2)];
229
+ }
230
+
231
+ // If length is even, return the average of the two middle elements
232
+ const midIndex = length / 2;
233
+ return (sortedVelocities[midIndex - 1] + sortedVelocities[midIndex]) / 2;
234
+ }
235
+
236
+ /**
237
+ * Projects the next scroll offset based on median velocity
238
+ * @param timeMs Time in milliseconds to predict ahead
239
+ * @returns Projected scroll offset
240
+ */
241
+ private getProjectedScrollOffset(offset: number, timeMs: number): number {
242
+ const medianVelocity = this.getMedianVelocity();
243
+ // Convert time from ms to seconds for velocity calculation
244
+ // Predict next position: current position + (velocity * time)
245
+ return offset + medianVelocity * timeMs;
246
+ }
247
+
165
248
  /**
166
249
  * Calculates which items are currently visible in the viewport.
167
250
  * Unlike getEngagedIndices, this doesn't include buffer items.
@@ -196,11 +279,23 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
196
279
 
197
280
  setScrollDirection(scrollDirection: "forward" | "backward") {
198
281
  if (scrollDirection === "forward") {
199
- this.velocityHistory = [1, 1, 1, 1, 1];
282
+ this.velocityHistory = [0, 0, 0, 0.1, 0.1];
200
283
  this.velocityIndex = 0;
201
284
  } else {
202
- this.velocityHistory = [-1, -1, -1, -1, -1];
285
+ this.velocityHistory = [0, 0, 0, -0.1, -0.1];
203
286
  this.velocityIndex = 0;
204
287
  }
205
288
  }
289
+
290
+ /**
291
+ * Resets the velocity history based on the current scroll direction.
292
+ * This ensures that the velocity history is always in sync with the current scroll direction.
293
+ */
294
+ resetVelocityHistory() {
295
+ if (this.isScrollingBackward()) {
296
+ this.setScrollDirection("backward");
297
+ } else {
298
+ this.setScrollDirection("forward");
299
+ }
300
+ }
206
301
  }
@@ -0,0 +1,42 @@
1
+ import { PlatformConfig } from "../../native/config/PlatformHelper";
2
+ import { AverageWindow } from "../../utils/AverageWindow";
3
+
4
+ export class RenderTimeTracker {
5
+ private renderTimeAvgWindow = new AverageWindow(5);
6
+ private lastTimerStartedAt = -1;
7
+ private maxRenderTime = 32; // TODO: Improve this even more
8
+ private defaultRenderTime = 16;
9
+
10
+ startTracking() {
11
+ if (!PlatformConfig.trackAverageRenderTimeForOffsetProjection) {
12
+ return;
13
+ }
14
+ if (this.lastTimerStartedAt === -1) {
15
+ this.lastTimerStartedAt = Date.now();
16
+ }
17
+ }
18
+
19
+ markRenderComplete() {
20
+ if (!PlatformConfig.trackAverageRenderTimeForOffsetProjection) {
21
+ return;
22
+ }
23
+ if (this.lastTimerStartedAt !== -1) {
24
+ this.renderTimeAvgWindow.addValue(Date.now() - this.lastTimerStartedAt);
25
+ this.lastTimerStartedAt = -1;
26
+ }
27
+ }
28
+
29
+ getRawValue() {
30
+ return this.renderTimeAvgWindow.currentValue;
31
+ }
32
+
33
+ getAverageRenderTime() {
34
+ if (!PlatformConfig.trackAverageRenderTimeForOffsetProjection) {
35
+ return this.defaultRenderTime;
36
+ }
37
+ return Math.min(
38
+ this.maxRenderTime,
39
+ Math.max(Math.round(this.renderTimeAvgWindow.currentValue), 16)
40
+ );
41
+ }
42
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tracks and calculates velocity for scroll/drag movements
3
+ * Used to determine momentum scrolling behavior
4
+ */
5
+ export class VelocityTracker<T> {
6
+ /** Timestamp of the last velocity update */
7
+ private lastUpdateTime = Date.now();
8
+ /** Current velocity vector with x and y components */
9
+ private velocity = { x: 0, y: 0 };
10
+
11
+ /** Reference to the momentum end timeout */
12
+ private timeoutId: NodeJS.Timeout | null = null;
13
+
14
+ /**
15
+ * Calculates velocity based on position change over time
16
+ * @param newOffset Current position value
17
+ * @param oldOffset Previous position value
18
+ * @param isHorizontal Whether movement is horizontal (true) or vertical (false)
19
+ * @param isRTL Whether layout direction is right-to-left
20
+ * @param callback Function to call with velocity updates and momentum end signal
21
+ */
22
+ computeVelocity(
23
+ newOffset: number,
24
+ oldOffset: number,
25
+ isHorizontal: boolean,
26
+ callback: (
27
+ velocity: { x: number; y: number },
28
+ isMomentumEnd: boolean
29
+ ) => void
30
+ ) {
31
+ // Clear any pending momentum end timeout
32
+ this.cleanUp();
33
+ // Calculate time since last update
34
+ const currentTime = Date.now();
35
+ const timeSinceLastUpdate = Math.max(1, currentTime - this.lastUpdateTime);
36
+
37
+ // Calculate velocity as distance/time
38
+ const newVelocity = (newOffset - oldOffset) / timeSinceLastUpdate;
39
+
40
+ // console.log(
41
+ // "newVelocity",
42
+ // newOffset,
43
+ // oldOffset,
44
+ // currentTime,
45
+ // this.lastUpdateTime,
46
+ // timeSinceLastUpdate,
47
+ // newVelocity
48
+ // );
49
+ this.lastUpdateTime = currentTime;
50
+
51
+ // Apply velocity to the correct axis
52
+ this.velocity.x = isHorizontal ? newVelocity : 0;
53
+ this.velocity.y = isHorizontal ? 0 : newVelocity;
54
+
55
+ // Trigger callback with current velocity
56
+ callback(this.velocity, false);
57
+
58
+ // Set timeout to signal momentum end after 100ms of no updates
59
+ this.timeoutId = setTimeout(() => {
60
+ this.cleanUp();
61
+ this.lastUpdateTime = Date.now();
62
+ this.velocity.x = 0;
63
+ this.velocity.y = 0;
64
+ callback(this.velocity, true);
65
+ }, 100);
66
+ }
67
+
68
+ /**
69
+ * Cleans up resources by clearing any pending timeout
70
+ */
71
+ cleanUp() {
72
+ if (this.timeoutId !== null) {
73
+ clearTimeout(this.timeoutId);
74
+ this.timeoutId = null;
75
+ }
76
+ }
77
+ }
@@ -1,9 +1,10 @@
1
1
  import { useCallback, useEffect, useMemo, useRef } from "react";
2
2
 
3
3
  import { RecyclerViewManager } from "../RecyclerViewManager";
4
- import { RecyclerViewProps } from "../RecyclerViewProps";
5
4
  import { CompatScroller } from "../components/CompatScroller";
6
5
 
6
+ import { useUnmountAwareAnimationFrame } from "./useUnmountAwareCallbacks";
7
+
7
8
  /**
8
9
  * Hook to detect when the scroll position reaches near the start or end of the list
9
10
  * and trigger the appropriate callbacks. This hook is responsible for:
@@ -17,7 +18,6 @@ import { CompatScroller } from "../components/CompatScroller";
17
18
  */
18
19
  export function useBoundDetection<T>(
19
20
  recyclerViewManager: RecyclerViewManager<T>,
20
- props: RecyclerViewProps<T>,
21
21
  scrollViewRef: React.RefObject<CompatScroller>
22
22
  ) {
23
23
  // Track whether we've already triggered the end reached callback to prevent duplicate calls
@@ -26,22 +26,48 @@ export function useBoundDetection<T>(
26
26
  const pendingStartReached = useRef(false);
27
27
  // Track whether we should auto-scroll to bottom when new content is added
28
28
  const pendingAutoscrollToBottom = useRef(false);
29
- const { horizontal, data, maintainVisibleContentPosition } = props;
29
+
30
+ const lastCheckBoundsTime = useRef(Date.now());
31
+
32
+ const { data } = recyclerViewManager.props;
33
+ const { requestAnimationFrame } = useUnmountAwareAnimationFrame();
34
+
35
+ const windowHeight = recyclerViewManager.hasLayout()
36
+ ? recyclerViewManager.getWindowSize().height
37
+ : 0;
38
+
39
+ const contentHeight = recyclerViewManager.hasLayout()
40
+ ? recyclerViewManager.getChildContainerDimensions().height
41
+ : 0;
42
+
43
+ const windowWidth = recyclerViewManager.hasLayout()
44
+ ? recyclerViewManager.getWindowSize().width
45
+ : 0;
46
+
47
+ const contentWidth = recyclerViewManager.hasLayout()
48
+ ? recyclerViewManager.getChildContainerDimensions().width
49
+ : 0;
30
50
 
31
51
  /**
32
52
  * Checks if the scroll position is near the start or end of the list
33
53
  * and triggers appropriate callbacks if configured.
34
54
  */
35
55
  const checkBounds = useCallback(() => {
56
+ lastCheckBoundsTime.current = Date.now();
57
+
58
+ const {
59
+ onEndReached,
60
+ onStartReached,
61
+ maintainVisibleContentPosition,
62
+ horizontal,
63
+ onEndReachedThreshold: onEndReachedThresholdProp,
64
+ onStartReachedThreshold: onStartReachedThresholdProp,
65
+ } = recyclerViewManager.props;
36
66
  // Skip all calculations if neither callback is provided and autoscroll is disabled
37
67
  const autoscrollToBottomThreshold =
38
68
  maintainVisibleContentPosition?.autoscrollToBottomThreshold ?? -1;
39
69
 
40
- if (
41
- !props.onEndReached &&
42
- !props.onStartReached &&
43
- autoscrollToBottomThreshold < 0
44
- ) {
70
+ if (!onEndReached && !onStartReached && autoscrollToBottomThreshold < 0) {
45
71
  return;
46
72
  }
47
73
 
@@ -50,7 +76,7 @@ export function useBoundDetection<T>(
50
76
  recyclerViewManager.getAbsoluteLastScrollOffset();
51
77
  const contentSize = recyclerViewManager.getChildContainerDimensions();
52
78
  const windowSize = recyclerViewManager.getWindowSize();
53
- const isHorizontal = props.horizontal === true;
79
+ const isHorizontal = horizontal === true;
54
80
 
55
81
  // Calculate dimensions based on scroll direction
56
82
  const visibleLength = isHorizontal ? windowSize.width : windowSize.height;
@@ -59,8 +85,8 @@ export function useBoundDetection<T>(
59
85
  recyclerViewManager.firstItemOffset;
60
86
 
61
87
  // Check if we're near the end of the list
62
- if (props.onEndReached) {
63
- const onEndReachedThreshold = props.onEndReachedThreshold ?? 0.5;
88
+ if (onEndReached) {
89
+ const onEndReachedThreshold = onEndReachedThresholdProp ?? 0.5;
64
90
  const endThresholdDistance = onEndReachedThreshold * visibleLength;
65
91
 
66
92
  const isNearEnd =
@@ -69,27 +95,27 @@ export function useBoundDetection<T>(
69
95
 
70
96
  if (isNearEnd && !pendingEndReached.current) {
71
97
  pendingEndReached.current = true;
72
- props.onEndReached();
98
+ onEndReached();
73
99
  }
74
100
  pendingEndReached.current = isNearEnd;
75
101
  }
76
102
 
77
103
  // Check if we're near the start of the list
78
- if (props.onStartReached) {
79
- const onStartReachedThreshold = props.onStartReachedThreshold ?? 0.2;
104
+ if (onStartReached) {
105
+ const onStartReachedThreshold = onStartReachedThresholdProp ?? 0.2;
80
106
  const startThresholdDistance = onStartReachedThreshold * visibleLength;
81
107
 
82
108
  const isNearStart = lastScrollOffset <= startThresholdDistance;
83
109
 
84
110
  if (isNearStart && !pendingStartReached.current) {
85
111
  pendingStartReached.current = true;
86
- props.onStartReached();
112
+ onStartReached();
87
113
  }
88
114
  pendingStartReached.current = isNearStart;
89
115
  }
90
116
 
91
117
  // Handle auto-scrolling to bottom for vertical lists
92
- if (!horizontal) {
118
+ if (!isHorizontal && autoscrollToBottomThreshold >= 0) {
93
119
  const autoscrollToBottomThresholdDistance =
94
120
  autoscrollToBottomThreshold * visibleLength;
95
121
 
@@ -104,22 +130,45 @@ export function useBoundDetection<T>(
104
130
  }
105
131
  }
106
132
  }
107
- }, [recyclerViewManager, props]);
133
+ }, [recyclerViewManager]);
134
+
135
+ const runAutoScrollToBottomCheck = useCallback(() => {
136
+ if (pendingAutoscrollToBottom.current) {
137
+ pendingAutoscrollToBottom.current = false;
138
+ requestAnimationFrame(() => {
139
+ const shouldAnimate =
140
+ recyclerViewManager.props.maintainVisibleContentPosition
141
+ ?.animateAutoScrollToBottom ?? true;
142
+ scrollViewRef.current?.scrollToEnd({
143
+ animated: shouldAnimate,
144
+ });
145
+ });
146
+ }
147
+ }, [requestAnimationFrame, scrollViewRef, recyclerViewManager]);
108
148
 
109
149
  // Reset end reached state when data changes
110
150
  useMemo(() => {
111
151
  pendingEndReached.current = false;
152
+ // needs to run only when data changes
153
+ // eslint-disable-next-line react-hooks/exhaustive-deps
112
154
  }, [data]);
113
155
 
114
156
  // Auto-scroll to bottom when new content is added and we're near the bottom
115
157
  useEffect(() => {
116
- if (pendingAutoscrollToBottom.current) {
117
- requestAnimationFrame(() => {
118
- scrollViewRef.current?.scrollToEnd();
119
- pendingAutoscrollToBottom.current = false;
120
- });
158
+ runAutoScrollToBottomCheck();
159
+ }, [data, runAutoScrollToBottomCheck, windowHeight, windowWidth]);
160
+
161
+ // Since content changes frequently, we try and avoid doing the auto scroll during active scrolls
162
+ useEffect(() => {
163
+ if (Date.now() - lastCheckBoundsTime.current >= 100) {
164
+ runAutoScrollToBottomCheck();
121
165
  }
122
- }, [data]);
166
+ }, [
167
+ contentHeight,
168
+ contentWidth,
169
+ recyclerViewManager.firstItemOffset,
170
+ runAutoScrollToBottomCheck,
171
+ ]);
123
172
 
124
173
  return {
125
174
  checkBounds,
@@ -2,6 +2,13 @@ import { useState, useCallback } from "react";
2
2
 
3
3
  import { useRecyclerViewContext } from "../RecyclerViewContextProvider";
4
4
 
5
+ export type LayoutStateSetter<T> = (
6
+ newValue: T | ((prevValue: T) => T),
7
+ skipParentLayout?: boolean
8
+ ) => void;
9
+
10
+ export type LayoutStateInitialValue<T> = T | (() => T);
11
+
5
12
  /**
6
13
  * Custom hook that combines state management with RecyclerView layout updates.
7
14
  * This hook provides a way to manage state that affects the layout of the RecyclerView,
@@ -13,8 +20,8 @@ import { useRecyclerViewContext } from "../RecyclerViewContextProvider";
13
20
  * - A setter function that updates the state and triggers a layout recalculation
14
21
  */
15
22
  export function useLayoutState<T>(
16
- initialState: T | (() => T)
17
- ): [T, (newValue: T | ((prevValue: T) => T)) => void] {
23
+ initialState: LayoutStateInitialValue<T>
24
+ ): [T, LayoutStateSetter<T>] {
18
25
  // Initialize state with the provided initial value
19
26
  const [state, setState] = useState<T>(initialState);
20
27
  // Get the RecyclerView context for layout management
@@ -28,16 +35,18 @@ export function useLayoutState<T>(
28
35
  * @param newValue - Either a new state value or a function that receives the previous state
29
36
  * and returns the new state
30
37
  */
31
- const setLayoutState = useCallback(
32
- (newValue: T | ((prevValue: T) => T)) => {
38
+ const setLayoutState: LayoutStateSetter<T> = useCallback(
39
+ (newValue, skipParentLayout) => {
33
40
  // Update the state using either the new value or the result of the updater function
34
41
  setState((prevValue) =>
35
42
  typeof newValue === "function"
36
43
  ? (newValue as (prevValue: T) => T)(prevValue)
37
44
  : newValue
38
45
  );
39
- // Trigger a layout recalculation in the RecyclerView
40
- recyclerViewContext?.layout();
46
+ if (!skipParentLayout) {
47
+ // Trigger a layout recalculation in the RecyclerView
48
+ recyclerViewContext?.layout();
49
+ }
41
50
  },
42
51
  [recyclerViewContext]
43
52
  );
@@ -10,7 +10,7 @@ import { useRecyclerViewContext } from "../RecyclerViewContextProvider";
10
10
  export const useMappingHelper = () => {
11
11
  const recyclerViewContext = useRecyclerViewContext();
12
12
  const getMappingKey = useCallback(
13
- (index: number, itemKey: string | number | bigint) => {
13
+ (itemKey: string | number | bigint, index: number) => {
14
14
  return recyclerViewContext ? index : itemKey;
15
15
  },
16
16
  [recyclerViewContext]
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
2
2
 
3
3
  import { RecyclerViewManager } from "../RecyclerViewManager";
4
4
 
5
- import { useUnmountFlag } from "./useUnmountFlag";
5
+ import { useUnmountAwareAnimationFrame } from "./useUnmountAwareCallbacks";
6
6
  // import { ToastAndroid } from "react-native";
7
7
 
8
8
  /**
@@ -22,7 +22,7 @@ export const useOnListLoad = <T>(
22
22
  const [isLoaded, setIsLoaded] = useState<boolean>(false);
23
23
  const dataLength = recyclerViewManager.getDataLength();
24
24
  // const dataCollector = useRef<number[]>([]);
25
- const isUnmounted = useUnmountFlag();
25
+ const { requestAnimationFrame } = useUnmountAwareAnimationFrame();
26
26
  // Track render cycles by collecting elapsed time on each render
27
27
  // useEffect(() => {
28
28
  // const elapsedTimeInMs = Date.now() - loadStartTimeRef.current;
@@ -48,10 +48,8 @@ export const useOnListLoad = <T>(
48
48
  // console.log("----------> dataCollector", dataCollectorString);
49
49
  // console.log("----------> FlashList v2 load in", `${elapsedTimeInMs} ms`);
50
50
  requestAnimationFrame(() => {
51
- if (!isUnmounted.current) {
52
- onLoad?.({ elapsedTimeInMs });
53
- setIsLoaded(true);
54
- }
51
+ onLoad?.({ elapsedTimeInMs });
52
+ setIsLoaded(true);
55
53
  });
56
54
  });
57
55