@shopify/flash-list 2.0.4-alpha.1 → 2.2.0

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 (203) hide show
  1. package/dist/AnimatedFlashList.js +4 -6
  2. package/dist/AnimatedFlashList.js.map +1 -1
  3. package/dist/FlashList.js +1 -5
  4. package/dist/FlashList.js.map +1 -1
  5. package/dist/FlashListProps.d.ts +41 -1
  6. package/dist/FlashListProps.d.ts.map +1 -1
  7. package/dist/FlashListProps.js +1 -4
  8. package/dist/FlashListProps.js.map +1 -1
  9. package/dist/FlashListRef.js +1 -2
  10. package/dist/benchmark/AutoScrollHelper.js +22 -30
  11. package/dist/benchmark/AutoScrollHelper.js.map +1 -1
  12. package/dist/benchmark/JSFPSMonitor.js +27 -33
  13. package/dist/benchmark/JSFPSMonitor.js.map +1 -1
  14. package/dist/benchmark/roundToDecimalPlaces.js +2 -5
  15. package/dist/benchmark/roundToDecimalPlaces.js.map +1 -1
  16. package/dist/benchmark/useBenchmark.d.ts +9 -1
  17. package/dist/benchmark/useBenchmark.d.ts.map +1 -1
  18. package/dist/benchmark/useBenchmark.js +86 -95
  19. package/dist/benchmark/useBenchmark.js.map +1 -1
  20. package/dist/benchmark/useDataMultiplier.js +6 -10
  21. package/dist/benchmark/useDataMultiplier.js.map +1 -1
  22. package/dist/benchmark/useFlatListBenchmark.d.ts +4 -1
  23. package/dist/benchmark/useFlatListBenchmark.d.ts.map +1 -1
  24. package/dist/benchmark/useFlatListBenchmark.js +73 -81
  25. package/dist/benchmark/useFlatListBenchmark.js.map +1 -1
  26. package/dist/errors/ErrorMessages.js +1 -4
  27. package/dist/errors/ErrorMessages.js.map +1 -1
  28. package/dist/errors/WarningMessages.js +1 -4
  29. package/dist/errors/WarningMessages.js.map +1 -1
  30. package/dist/index.js +17 -35
  31. package/dist/index.js.map +1 -1
  32. package/dist/isNewArch.js +6 -9
  33. package/dist/isNewArch.js.map +1 -1
  34. package/dist/native/config/PlatformHelper.android.js +2 -5
  35. package/dist/native/config/PlatformHelper.android.js.map +1 -1
  36. package/dist/native/config/PlatformHelper.ios.js +2 -5
  37. package/dist/native/config/PlatformHelper.ios.js.map +1 -1
  38. package/dist/native/config/PlatformHelper.js +2 -5
  39. package/dist/native/config/PlatformHelper.js.map +1 -1
  40. package/dist/native/config/PlatformHelper.web.js +2 -5
  41. package/dist/native/config/PlatformHelper.web.js.map +1 -1
  42. package/dist/recyclerview/LayoutCommitObserver.js +20 -24
  43. package/dist/recyclerview/LayoutCommitObserver.js.map +1 -1
  44. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  45. package/dist/recyclerview/RecyclerView.js +134 -111
  46. package/dist/recyclerview/RecyclerView.js.map +1 -1
  47. package/dist/recyclerview/RecyclerViewContextProvider.js +7 -12
  48. package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
  49. package/dist/recyclerview/RecyclerViewManager.js +138 -167
  50. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  51. package/dist/recyclerview/RecyclerViewProps.js +1 -2
  52. package/dist/recyclerview/RenderStackManager.js +97 -188
  53. package/dist/recyclerview/RenderStackManager.js.map +1 -1
  54. package/dist/recyclerview/ViewHolder.d.ts +2 -0
  55. package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
  56. package/dist/recyclerview/ViewHolder.js +19 -21
  57. package/dist/recyclerview/ViewHolder.js.map +1 -1
  58. package/dist/recyclerview/ViewHolderCollection.d.ts +4 -0
  59. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
  60. package/dist/recyclerview/ViewHolderCollection.js +26 -30
  61. package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
  62. package/dist/recyclerview/components/CompatScroller.js +6 -7
  63. package/dist/recyclerview/components/CompatScroller.js.map +1 -1
  64. package/dist/recyclerview/components/CompatView.js +6 -7
  65. package/dist/recyclerview/components/CompatView.js.map +1 -1
  66. package/dist/recyclerview/components/ScrollAnchor.js +10 -15
  67. package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
  68. package/dist/recyclerview/components/StickyHeaders.d.ts +5 -1
  69. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
  70. package/dist/recyclerview/components/StickyHeaders.js +77 -51
  71. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  72. package/dist/recyclerview/helpers/ConsecutiveNumbers.js +39 -66
  73. package/dist/recyclerview/helpers/ConsecutiveNumbers.js.map +1 -1
  74. package/dist/recyclerview/helpers/EngagedIndicesTracker.js +57 -63
  75. package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -1
  76. package/dist/recyclerview/helpers/RenderTimeTracker.js +19 -24
  77. package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -1
  78. package/dist/recyclerview/helpers/VelocityTracker.js +16 -22
  79. package/dist/recyclerview/helpers/VelocityTracker.js.map +1 -1
  80. package/dist/recyclerview/hooks/useBoundDetection.js +37 -40
  81. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
  82. package/dist/recyclerview/hooks/useLayoutState.js +9 -15
  83. package/dist/recyclerview/hooks/useLayoutState.js.map +1 -1
  84. package/dist/recyclerview/hooks/useMappingHelper.js +6 -10
  85. package/dist/recyclerview/hooks/useMappingHelper.js.map +1 -1
  86. package/dist/recyclerview/hooks/useOnLoad.js +16 -22
  87. package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
  88. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  89. package/dist/recyclerview/hooks/useRecyclerViewController.js +169 -188
  90. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  91. package/dist/recyclerview/hooks/useRecyclerViewManager.js +12 -17
  92. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
  93. package/dist/recyclerview/hooks/useRecyclingState.js +10 -14
  94. package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -1
  95. package/dist/recyclerview/hooks/useSecondaryProps.d.ts +2 -0
  96. package/dist/recyclerview/hooks/useSecondaryProps.d.ts.map +1 -1
  97. package/dist/recyclerview/hooks/useSecondaryProps.js +39 -30
  98. package/dist/recyclerview/hooks/useSecondaryProps.js.map +1 -1
  99. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +17 -22
  100. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
  101. package/dist/recyclerview/hooks/useUnmountFlag.js +5 -9
  102. package/dist/recyclerview/hooks/useUnmountFlag.js.map +1 -1
  103. package/dist/recyclerview/layout-managers/GridLayoutManager.js +61 -80
  104. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  105. package/dist/recyclerview/layout-managers/LayoutManager.js +83 -123
  106. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  107. package/dist/recyclerview/layout-managers/LinearLayoutManager.js +51 -91
  108. package/dist/recyclerview/layout-managers/LinearLayoutManager.js.map +1 -1
  109. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +77 -96
  110. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
  111. package/dist/recyclerview/utils/adjustOffsetForRTL.js +1 -4
  112. package/dist/recyclerview/utils/adjustOffsetForRTL.js.map +1 -1
  113. package/dist/recyclerview/utils/componentUtils.js +4 -9
  114. package/dist/recyclerview/utils/componentUtils.js.map +1 -1
  115. package/dist/recyclerview/utils/findVisibleIndex.js +9 -13
  116. package/dist/recyclerview/utils/findVisibleIndex.js.map +1 -1
  117. package/dist/recyclerview/utils/measureLayout.js +12 -20
  118. package/dist/recyclerview/utils/measureLayout.js.map +1 -1
  119. package/dist/recyclerview/utils/measureLayout.web.js +15 -23
  120. package/dist/recyclerview/utils/measureLayout.web.js.map +1 -1
  121. package/dist/recyclerview/viewability/ViewToken.js +1 -2
  122. package/dist/recyclerview/viewability/ViewabilityHelper.js +34 -41
  123. package/dist/recyclerview/viewability/ViewabilityHelper.js.map +1 -1
  124. package/dist/recyclerview/viewability/ViewabilityManager.js +48 -61
  125. package/dist/recyclerview/viewability/ViewabilityManager.js.map +1 -1
  126. package/dist/tsconfig.tsbuildinfo +1 -1
  127. package/dist/utils/AverageWindow.js +28 -39
  128. package/dist/utils/AverageWindow.js.map +1 -1
  129. package/package.json +4 -6
  130. package/src/FlashListProps.ts +51 -1
  131. package/src/benchmark/useBenchmark.ts +47 -4
  132. package/src/benchmark/useFlatListBenchmark.ts +38 -5
  133. package/src/recyclerview/RecyclerView.tsx +42 -8
  134. package/src/recyclerview/ViewHolder.tsx +6 -1
  135. package/src/recyclerview/ViewHolderCollection.tsx +10 -0
  136. package/src/recyclerview/components/StickyHeaders.tsx +54 -13
  137. package/src/recyclerview/hooks/useRecyclerViewController.tsx +7 -4
  138. package/src/recyclerview/hooks/useSecondaryProps.tsx +23 -0
  139. package/dist/__tests__/AverageWindow.test.d.ts +0 -2
  140. package/dist/__tests__/AverageWindow.test.d.ts.map +0 -1
  141. package/dist/__tests__/AverageWindow.test.js +0 -104
  142. package/dist/__tests__/AverageWindow.test.js.map +0 -1
  143. package/dist/__tests__/ConsecutiveNumbers.test.d.ts +0 -2
  144. package/dist/__tests__/ConsecutiveNumbers.test.d.ts.map +0 -1
  145. package/dist/__tests__/ConsecutiveNumbers.test.js +0 -224
  146. package/dist/__tests__/ConsecutiveNumbers.test.js.map +0 -1
  147. package/dist/__tests__/GridLayoutManager.test.d.ts +0 -2
  148. package/dist/__tests__/GridLayoutManager.test.d.ts.map +0 -1
  149. package/dist/__tests__/GridLayoutManager.test.js +0 -69
  150. package/dist/__tests__/GridLayoutManager.test.js.map +0 -1
  151. package/dist/__tests__/LayoutCommitObserver.test.d.ts +0 -2
  152. package/dist/__tests__/LayoutCommitObserver.test.d.ts.map +0 -1
  153. package/dist/__tests__/LayoutCommitObserver.test.js +0 -37
  154. package/dist/__tests__/LayoutCommitObserver.test.js.map +0 -1
  155. package/dist/__tests__/LinearLayoutManager.test.d.ts +0 -2
  156. package/dist/__tests__/LinearLayoutManager.test.d.ts.map +0 -1
  157. package/dist/__tests__/LinearLayoutManager.test.js +0 -140
  158. package/dist/__tests__/LinearLayoutManager.test.js.map +0 -1
  159. package/dist/__tests__/MasonryLayoutManager.test.d.ts +0 -2
  160. package/dist/__tests__/MasonryLayoutManager.test.d.ts.map +0 -1
  161. package/dist/__tests__/MasonryLayoutManager.test.js +0 -148
  162. package/dist/__tests__/MasonryLayoutManager.test.js.map +0 -1
  163. package/dist/__tests__/RecyclerView.test.d.ts +0 -2
  164. package/dist/__tests__/RecyclerView.test.d.ts.map +0 -1
  165. package/dist/__tests__/RecyclerView.test.js +0 -103
  166. package/dist/__tests__/RecyclerView.test.js.map +0 -1
  167. package/dist/__tests__/RecyclerViewManager.test.d.ts +0 -2
  168. package/dist/__tests__/RecyclerViewManager.test.d.ts.map +0 -1
  169. package/dist/__tests__/RecyclerViewManager.test.js +0 -56
  170. package/dist/__tests__/RecyclerViewManager.test.js.map +0 -1
  171. package/dist/__tests__/RenderStackManager.test.d.ts +0 -2
  172. package/dist/__tests__/RenderStackManager.test.d.ts.map +0 -1
  173. package/dist/__tests__/RenderStackManager.test.js +0 -485
  174. package/dist/__tests__/RenderStackManager.test.js.map +0 -1
  175. package/dist/__tests__/ViewabilityHelper.test.d.ts +0 -2
  176. package/dist/__tests__/ViewabilityHelper.test.d.ts.map +0 -1
  177. package/dist/__tests__/ViewabilityHelper.test.js +0 -186
  178. package/dist/__tests__/ViewabilityHelper.test.js.map +0 -1
  179. package/dist/__tests__/findVisibleIndex.test.d.ts +0 -2
  180. package/dist/__tests__/findVisibleIndex.test.d.ts.map +0 -1
  181. package/dist/__tests__/findVisibleIndex.test.js +0 -259
  182. package/dist/__tests__/findVisibleIndex.test.js.map +0 -1
  183. package/dist/__tests__/helpers/createLayoutManager.d.ts +0 -34
  184. package/dist/__tests__/helpers/createLayoutManager.d.ts.map +0 -1
  185. package/dist/__tests__/helpers/createLayoutManager.js +0 -110
  186. package/dist/__tests__/helpers/createLayoutManager.js.map +0 -1
  187. package/dist/__tests__/useUnmountAwareCallbacks.test.d.ts +0 -2
  188. package/dist/__tests__/useUnmountAwareCallbacks.test.d.ts.map +0 -1
  189. package/dist/__tests__/useUnmountAwareCallbacks.test.js +0 -185
  190. package/dist/__tests__/useUnmountAwareCallbacks.test.js.map +0 -1
  191. package/src/__tests__/AverageWindow.test.ts +0 -128
  192. package/src/__tests__/ConsecutiveNumbers.test.ts +0 -232
  193. package/src/__tests__/GridLayoutManager.test.ts +0 -113
  194. package/src/__tests__/LayoutCommitObserver.test.tsx +0 -63
  195. package/src/__tests__/LinearLayoutManager.test.ts +0 -227
  196. package/src/__tests__/MasonryLayoutManager.test.ts +0 -202
  197. package/src/__tests__/RecyclerView.test.tsx +0 -144
  198. package/src/__tests__/RecyclerViewManager.test.ts +0 -74
  199. package/src/__tests__/RenderStackManager.test.ts +0 -574
  200. package/src/__tests__/ViewabilityHelper.test.ts +0 -282
  201. package/src/__tests__/findVisibleIndex.test.ts +0 -369
  202. package/src/__tests__/helpers/createLayoutManager.ts +0 -141
  203. package/src/__tests__/useUnmountAwareCallbacks.test.tsx +0 -285
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from "react";
1
+ import React, { useEffect, useState, useCallback, useRef } from "react";
2
2
 
3
3
  import { FlashListRef } from "../FlashListRef";
4
4
  import { ErrorMessages } from "../errors/ErrorMessages";
@@ -24,6 +24,12 @@ export interface BenchmarkParams {
24
24
  * Blank area is negative when list is able to draw faster than the scroll speed.
25
25
  */
26
26
  sumNegativeBlankAreaValues?: boolean;
27
+
28
+ /**
29
+ * When set to true, the benchmark will not start automatically.
30
+ * Use the returned startBenchmark function to trigger it manually.
31
+ */
32
+ startManually?: boolean;
27
33
  }
28
34
 
29
35
  export interface BenchmarkResult {
@@ -44,15 +50,27 @@ export function useBenchmark(
44
50
  callback: (benchmarkResult: BenchmarkResult) => void,
45
51
  params: BenchmarkParams = {}
46
52
  ) {
47
- useEffect(() => {
53
+ const [isBenchmarkRunning, setIsBenchmarkRunning] = useState(false);
54
+ const cancellableRef = useRef<Cancellable | null>(null);
55
+
56
+ const startBenchmark = useCallback(() => {
57
+ if (isBenchmarkRunning) {
58
+ return;
59
+ }
60
+
48
61
  const cancellable = new Cancellable();
62
+ cancellableRef.current = cancellable;
49
63
  const suggestions: string[] = [];
64
+
50
65
  if (flashListRef.current) {
51
66
  if (!(Number(flashListRef.current.props.data?.length) > 0)) {
52
67
  throw new Error(ErrorMessages.dataEmptyCannotRunBenchmark);
53
68
  }
54
69
  }
55
- const cancelTimeout = setTimeout(async () => {
70
+
71
+ setIsBenchmarkRunning(true);
72
+
73
+ const runBenchmark = async () => {
56
74
  const jsFPSMonitor = new JSFPSMonitor();
57
75
  jsFPSMonitor.startTracking();
58
76
  for (let i = 0; i < (params.repeatCount || 1); i++) {
@@ -78,13 +96,37 @@ export function useBenchmark(
78
96
  result.formattedString = getFormattedString(result);
79
97
  }
80
98
  callback(result);
99
+ setIsBenchmarkRunning(false);
100
+ };
101
+
102
+ runBenchmark();
103
+ }, [
104
+ callback,
105
+ flashListRef,
106
+ isBenchmarkRunning,
107
+ params.repeatCount,
108
+ params.speedMultiplier,
109
+ ]);
110
+
111
+ useEffect(() => {
112
+ if (params.startManually) {
113
+ return;
114
+ }
115
+
116
+ const cancelTimeout = setTimeout(() => {
117
+ startBenchmark();
81
118
  }, params.startDelayInMs || 3000);
119
+
82
120
  return () => {
83
121
  clearTimeout(cancelTimeout);
84
- cancellable.cancel();
122
+ if (cancellableRef.current) {
123
+ cancellableRef.current.cancel();
124
+ }
85
125
  };
86
126
  // eslint-disable-next-line react-hooks/exhaustive-deps
87
127
  }, []);
128
+
129
+ return { startBenchmark, isBenchmarkRunning } as const;
88
130
  }
89
131
 
90
132
  export function getFormattedString(res: BenchmarkResult) {
@@ -161,6 +203,7 @@ async function runScrollBenchmark(
161
203
  }
162
204
  }
163
205
  }
206
+
164
207
  function computeSuggestions(
165
208
  flashListRef: React.RefObject<FlashListRef<any> | null | undefined>,
166
209
  suggestions: string[]
@@ -1,4 +1,4 @@
1
- import { useEffect } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { FlatList } from "react-native";
3
3
 
4
4
  import { ErrorMessages } from "../errors/ErrorMessages";
@@ -25,14 +25,24 @@ export function useFlatListBenchmark(
25
25
  callback: (benchmarkResult: BenchmarkResult) => void,
26
26
  params: FlatListBenchmarkParams
27
27
  ) {
28
- useEffect(() => {
28
+ const [isBenchmarkRunning, setIsBenchmarkRunning] = useState(false);
29
+ const cancellableRef = useRef<Cancellable | null>(null);
30
+
31
+ const startBenchmark = useCallback(() => {
32
+ if (isBenchmarkRunning) {
33
+ return;
34
+ }
29
35
  const cancellable = new Cancellable();
36
+ cancellableRef.current = cancellable;
30
37
  if (flatListRef.current && flatListRef.current.props) {
31
38
  if (!(Number(flatListRef.current.props.data?.length) > 0)) {
32
39
  throw new Error(ErrorMessages.dataEmptyCannotRunBenchmark);
33
40
  }
34
41
  }
35
- const cancelTimeout = setTimeout(async () => {
42
+
43
+ setIsBenchmarkRunning(true);
44
+
45
+ const runBenchmark = async () => {
36
46
  const jsFPSMonitor = new JSFPSMonitor();
37
47
  jsFPSMonitor.startTracking();
38
48
  for (let i = 0; i < (params.repeatCount || 1); i++) {
@@ -53,14 +63,37 @@ export function useFlatListBenchmark(
53
63
  result.formattedString = getFormattedString(result);
54
64
  }
55
65
  callback(result);
66
+ setIsBenchmarkRunning(false);
67
+ };
68
+
69
+ runBenchmark();
70
+ }, [
71
+ callback,
72
+ flatListRef,
73
+ isBenchmarkRunning,
74
+ params.repeatCount,
75
+ params.speedMultiplier,
76
+ params.targetOffset,
77
+ ]);
78
+
79
+ useEffect(() => {
80
+ if (params.startManually) {
81
+ return;
82
+ }
83
+
84
+ const cancelTimeout = setTimeout(() => {
85
+ startBenchmark();
56
86
  }, params.startDelayInMs || 3000);
87
+
57
88
  return () => {
58
89
  clearTimeout(cancelTimeout);
59
- cancellable.cancel();
90
+ if (cancellableRef.current) {
91
+ cancellableRef.current.cancel();
92
+ }
60
93
  };
61
94
  // eslint-disable-next-line react-hooks/exhaustive-deps
62
95
  }, []);
63
- return [];
96
+ return { startBenchmark, isBenchmarkRunning };
64
97
  }
65
98
 
66
99
  /**
@@ -84,6 +84,8 @@ const RecyclerViewComponent = <T,>(
84
84
  stickyHeaderIndices,
85
85
  maintainVisibleContentPosition,
86
86
  onCommitLayoutEffect,
87
+ onChangeStickyIndex,
88
+ stickyHeaderConfig,
87
89
  ...rest
88
90
  } = props;
89
91
 
@@ -91,6 +93,13 @@ const RecyclerViewComponent = <T,>(
91
93
 
92
94
  renderTimeTracker.startTracking();
93
95
 
96
+ // Sticky header config
97
+ const stickyHeaderOffset = stickyHeaderConfig?.offset ?? 0;
98
+ const stickyHeaderUseNativeDriver =
99
+ stickyHeaderConfig?.useNativeDriver ?? true;
100
+ const stickyHeaderHideRelatedCell =
101
+ stickyHeaderConfig?.hideRelatedCell ?? false;
102
+
94
103
  // Core refs for managing scroll view, internal view, and child container
95
104
  const scrollViewRef = useRef<CompatScroller>(null);
96
105
  const internalViewRef = useRef<CompatView>(null);
@@ -108,6 +117,7 @@ const RecyclerViewComponent = <T,>(
108
117
  // State for managing layout and render updates
109
118
  const [_, setLayoutTreeId] = useLayoutState(0);
110
119
  const [__, setRenderId] = useState(0);
120
+ const [currentStickyIndex, setCurrentStickyIndex] = useState(-1);
111
121
 
112
122
  // Map to store refs for each item in the list
113
123
  const refHolder = useMemo(
@@ -388,6 +398,7 @@ const RecyclerViewComponent = <T,>(
388
398
  renderFooter,
389
399
  renderEmpty,
390
400
  CompatScrollView,
401
+ renderStickyHeaderBackdrop,
391
402
  } = useSecondaryProps(props);
392
403
 
393
404
  if (
@@ -408,15 +419,23 @@ const RecyclerViewComponent = <T,>(
408
419
  if (horizontal) {
409
420
  throw new Error(ErrorMessages.stickyHeadersNotSupportedForHorizontal);
410
421
  }
422
+
411
423
  return (
412
424
  <StickyHeaders
413
425
  stickyHeaderIndices={stickyHeaderIndices}
426
+ stickyHeaderOffset={stickyHeaderOffset}
414
427
  data={data}
415
428
  renderItem={renderItem}
416
429
  scrollY={scrollY}
417
430
  stickyHeaderRef={stickyHeaderRef}
418
431
  recyclerViewManager={recyclerViewManager}
419
432
  extraData={extraData}
433
+ onChangeStickyIndex={(newStickyHeaderIndex) => {
434
+ if (stickyHeaderHideRelatedCell) {
435
+ setCurrentStickyIndex(newStickyHeaderIndex);
436
+ }
437
+ onChangeStickyIndex?.(newStickyHeaderIndex, currentStickyIndex);
438
+ }}
420
439
  />
421
440
  );
422
441
  }
@@ -424,11 +443,15 @@ const RecyclerViewComponent = <T,>(
424
443
  }, [
425
444
  data,
426
445
  stickyHeaderIndices,
446
+ stickyHeaderOffset,
427
447
  renderItem,
428
448
  scrollY,
429
449
  horizontal,
430
450
  recyclerViewManager,
431
451
  extraData,
452
+ currentStickyIndex,
453
+ onChangeStickyIndex,
454
+ stickyHeaderHideRelatedCell,
432
455
  ]);
433
456
 
434
457
  // Set up scroll event handling with animation support for sticky headers
@@ -436,11 +459,14 @@ const RecyclerViewComponent = <T,>(
436
459
  if (stickyHeaders) {
437
460
  return Animated.event(
438
461
  [{ nativeEvent: { contentOffset: { y: scrollY } } }],
439
- { useNativeDriver: true, listener: onScrollHandler }
462
+ {
463
+ useNativeDriver: stickyHeaderUseNativeDriver,
464
+ listener: onScrollHandler,
465
+ }
440
466
  );
441
467
  }
442
468
  return onScrollHandler;
443
- }, [onScrollHandler, scrollY, stickyHeaders]);
469
+ }, [onScrollHandler, scrollY, stickyHeaders, stickyHeaderUseNativeDriver]);
444
470
 
445
471
  const shouldMaintainVisibleContentPosition =
446
472
  recyclerViewManager.shouldMaintainVisibleContentPosition();
@@ -464,13 +490,14 @@ const RecyclerViewComponent = <T,>(
464
490
  return (
465
491
  <CompatView
466
492
  style={{
493
+ marginTop: horizontal ? undefined : stickyHeaderOffset,
467
494
  height: horizontal ? undefined : 0,
468
495
  width: horizontal ? 0 : undefined,
469
496
  }}
470
497
  ref={firstChildViewRef}
471
498
  />
472
499
  );
473
- }, [horizontal]);
500
+ }, [horizontal, stickyHeaderOffset]);
474
501
 
475
502
  const scrollAnchor = useMemo(() => {
476
503
  if (shouldMaintainVisibleContentPosition) {
@@ -490,11 +517,13 @@ const RecyclerViewComponent = <T,>(
490
517
  return (
491
518
  <RecyclerViewContextProvider value={recyclerViewContext}>
492
519
  <CompatView
493
- style={{
494
- flex: horizontal ? undefined : 1,
495
- overflow: "hidden",
496
- ...style,
497
- }}
520
+ style={[
521
+ {
522
+ flex: horizontal ? undefined : 1,
523
+ overflow: "hidden",
524
+ },
525
+ style,
526
+ ]}
498
527
  ref={internalViewRef}
499
528
  collapsable={false}
500
529
  onLayout={(event) => {
@@ -588,10 +617,15 @@ const RecyclerViewComponent = <T,>(
588
617
  ? recyclerViewManager.getChildContainerDimensions()
589
618
  : undefined
590
619
  }
620
+ currentStickyIndex={currentStickyIndex}
621
+ hideStickyHeaderRelatedCell={stickyHeaderHideRelatedCell}
591
622
  />
592
623
  {renderEmpty}
593
624
  {renderFooter}
594
625
  </CompatScrollView>
626
+ {stickyHeaderIndices && stickyHeaderIndices.length > 0
627
+ ? renderStickyHeaderBackdrop
628
+ : null}
595
629
  {stickyHeaders}
596
630
  </CompatView>
597
631
  </RecyclerViewContextProvider>
@@ -47,6 +47,8 @@ export interface ViewHolderProps<TItem> {
47
47
  horizontal?: FlashListProps<TItem>["horizontal"];
48
48
  /** Callback when the item's size changes */
49
49
  onSizeChanged?: (index: number, size: RVDimension) => void;
50
+ /** Whether this item should be hidden (likely because it is associated with the active sticky header) */
51
+ hidden: boolean;
50
52
  }
51
53
 
52
54
  /**
@@ -69,6 +71,7 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
69
71
  ItemSeparatorComponent,
70
72
  trailingItem,
71
73
  horizontal,
74
+ hidden,
72
75
  } = props;
73
76
 
74
77
  useLayoutEffect(() => {
@@ -113,6 +116,7 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
113
116
  maxWidth: layout.maxWidth,
114
117
  left: layout.x,
115
118
  top: layout.y,
119
+ opacity: hidden ? 0 : 1,
116
120
  } as const;
117
121
 
118
122
  // TODO: Fix this type issue
@@ -152,7 +156,8 @@ export const ViewHolder = React.memo(
152
156
  prevProps.CellRendererComponent === nextProps.CellRendererComponent &&
153
157
  prevProps.ItemSeparatorComponent === nextProps.ItemSeparatorComponent &&
154
158
  prevProps.trailingItem === nextProps.trailingItem &&
155
- prevProps.horizontal === nextProps.horizontal
159
+ prevProps.horizontal === nextProps.horizontal &&
160
+ prevProps.hidden === nextProps.hidden
156
161
  );
157
162
  }
158
163
  );
@@ -50,6 +50,10 @@ export interface ViewHolderCollectionProps<TItem> {
50
50
  * For startRenderingFromBottom, we need to adjust the height of the container
51
51
  */
52
52
  getAdjustmentMargin: () => number;
53
+ /** Current sticky index */
54
+ currentStickyIndex: number;
55
+ /** Whether the cell associated with an active sticky header is hidden */
56
+ hideStickyHeaderRelatedCell: boolean;
53
57
  }
54
58
 
55
59
  /**
@@ -84,6 +88,8 @@ export const ViewHolderCollection = <TItem,>(
84
88
  onCommitEffect,
85
89
  horizontal,
86
90
  getAdjustmentMargin,
91
+ currentStickyIndex,
92
+ hideStickyHeaderRelatedCell,
87
93
  } = props;
88
94
 
89
95
  const [renderId, setRenderId] = React.useState(0);
@@ -168,6 +174,7 @@ export const ViewHolderCollection = <TItem,>(
168
174
  const trailingItem = ItemSeparatorComponent
169
175
  ? data[index + 1]
170
176
  : undefined;
177
+
171
178
  return (
172
179
  <ViewHolder
173
180
  key={reactKey}
@@ -185,6 +192,9 @@ export const ViewHolderCollection = <TItem,>(
185
192
  CellRendererComponent={CellRendererComponent}
186
193
  ItemSeparatorComponent={ItemSeparatorComponent}
187
194
  horizontal={horizontal}
195
+ hidden={
196
+ hideStickyHeaderRelatedCell && currentStickyIndex === index
197
+ }
188
198
  />
189
199
  );
190
200
  })}
@@ -27,6 +27,10 @@ import { CompatAnimatedView } from "./CompatView";
27
27
  export interface StickyHeaderProps<TItem> {
28
28
  /** Array of indices that should have sticky headers */
29
29
  stickyHeaderIndices: number[];
30
+ /** Offset from the top where sticky headers should stick (in pixels) */
31
+ stickyHeaderOffset: number;
32
+ /** Sticky header change handler */
33
+ onChangeStickyIndex: (index: number) => void;
30
34
  /** The data array being rendered */
31
35
  data: ReadonlyArray<TItem>;
32
36
  /** Animated value tracking scroll position */
@@ -56,12 +60,14 @@ interface StickyHeaderState {
56
60
 
57
61
  export const StickyHeaders = <TItem,>({
58
62
  stickyHeaderIndices,
63
+ stickyHeaderOffset,
59
64
  renderItem,
60
65
  stickyHeaderRef,
61
66
  recyclerViewManager,
62
67
  scrollY,
63
68
  data,
64
69
  extraData,
70
+ onChangeStickyIndex,
65
71
  }: StickyHeaderProps<TItem>) => {
66
72
  const [stickyHeaderState, setStickyHeaderState] = useState<StickyHeaderState>(
67
73
  {
@@ -91,7 +97,7 @@ export const StickyHeaders = <TItem,>({
91
97
  // Binary search for current sticky index
92
98
  const currentIndexInArray = findCurrentStickyIndex(
93
99
  sortedIndices,
94
- adjustedScrollOffset,
100
+ adjustedScrollOffset + stickyHeaderOffset,
95
101
  (index) => recyclerViewManager.getLayout(index).y
96
102
  );
97
103
 
@@ -102,7 +108,8 @@ export const StickyHeaders = <TItem,>({
102
108
  newNextStickyIndex = -1;
103
109
  }
104
110
 
105
- // To make sure header offset is 0 in the interpolate compute
111
+ // Calculate when the next sticky header should start pushing the current one
112
+ // The next header starts pushing when it reaches the bottom of the current sticky header
106
113
  const newNextStickyY =
107
114
  newNextStickyIndex === -1
108
115
  ? Number.MAX_SAFE_INTEGER
@@ -111,6 +118,7 @@ export const StickyHeaders = <TItem,>({
111
118
  const newCurrentStickyHeight =
112
119
  recyclerViewManager.tryGetLayout(newStickyIndex)?.height ?? 0;
113
120
 
121
+ // Push should start when the next header reaches the bottom of the current sticky header
114
122
  const newPushStartsAt = newNextStickyY - newCurrentStickyHeight;
115
123
 
116
124
  if (
@@ -119,15 +127,21 @@ export const StickyHeaders = <TItem,>({
119
127
  ) {
120
128
  setStickyHeaderState({
121
129
  currentStickyIndex: newStickyIndex,
122
- pushStartsAt: newPushStartsAt,
130
+ pushStartsAt: newPushStartsAt - stickyHeaderOffset,
123
131
  });
124
132
  }
133
+
134
+ if (newStickyIndex !== currentStickyIndex) {
135
+ onChangeStickyIndex?.(newStickyIndex);
136
+ }
125
137
  }, [
126
138
  legthInvalid,
127
139
  recyclerViewManager,
128
140
  sortedIndices,
129
141
  currentStickyIndex,
130
142
  pushStartsAt,
143
+ onChangeStickyIndex,
144
+ stickyHeaderOffset,
131
145
  ]);
132
146
 
133
147
  useEffect(() => {
@@ -147,16 +161,32 @@ export const StickyHeaders = <TItem,>({
147
161
 
148
162
  const refHolder = useRef(new Map()).current;
149
163
 
150
- const translateY = useMemo(() => {
164
+ const { translateY, opacity } = useMemo(() => {
151
165
  const currentStickyHeight =
152
166
  recyclerViewManager.tryGetLayout(currentStickyIndex)?.height ?? 0;
153
167
 
154
- return scrollY.interpolate({
155
- inputRange: [pushStartsAt, pushStartsAt + currentStickyHeight],
156
- outputRange: [0, -currentStickyHeight],
157
- extrapolate: "clamp",
158
- });
159
- }, [recyclerViewManager, currentStickyIndex, scrollY, pushStartsAt]);
168
+ return {
169
+ translateY: scrollY.interpolate({
170
+ inputRange: [pushStartsAt, pushStartsAt + currentStickyHeight],
171
+ outputRange: [0, -currentStickyHeight],
172
+ extrapolate: "clamp",
173
+ }),
174
+ opacity:
175
+ stickyHeaderOffset > 0
176
+ ? scrollY.interpolate({
177
+ inputRange: [pushStartsAt, pushStartsAt + currentStickyHeight],
178
+ outputRange: [1, 0],
179
+ extrapolate: "clamp",
180
+ })
181
+ : undefined,
182
+ };
183
+ }, [
184
+ recyclerViewManager,
185
+ currentStickyIndex,
186
+ scrollY,
187
+ pushStartsAt,
188
+ stickyHeaderOffset,
189
+ ]);
160
190
 
161
191
  // Memoize header content
162
192
  const headerContent = useMemo(() => {
@@ -164,11 +194,12 @@ export const StickyHeaders = <TItem,>({
164
194
  <CompatAnimatedView
165
195
  style={{
166
196
  position: "absolute",
167
- top: 0,
197
+ top: stickyHeaderOffset,
168
198
  left: 0,
169
199
  right: 0,
170
- zIndex: 1,
200
+ zIndex: 2,
171
201
  transform: [{ translateY }],
202
+ opacity,
172
203
  }}
173
204
  >
174
205
  {currentStickyIndex !== -1 && currentStickyIndex < data.length ? (
@@ -181,11 +212,21 @@ export const StickyHeaders = <TItem,>({
181
212
  extraData={extraData}
182
213
  trailingItem={null}
183
214
  target="StickyHeader"
215
+ hidden={false}
184
216
  />
185
217
  ) : null}
186
218
  </CompatAnimatedView>
187
219
  );
188
- }, [translateY, currentStickyIndex, data, renderItem, refHolder, extraData]);
220
+ }, [
221
+ translateY,
222
+ opacity,
223
+ currentStickyIndex,
224
+ data,
225
+ renderItem,
226
+ refHolder,
227
+ extraData,
228
+ stickyHeaderOffset,
229
+ ]);
189
230
 
190
231
  return headerContent;
191
232
  };
@@ -289,7 +289,7 @@ export function useRecyclerViewController<T>(
289
289
  }
290
290
  }
291
291
  setTimeout(() => {
292
- scrollViewRef.current!.scrollToEnd({ animated });
292
+ scrollViewRef.current?.scrollToEnd({ animated });
293
293
  }, 0);
294
294
  },
295
295
 
@@ -564,7 +564,8 @@ export function useRecyclerViewController<T>(
564
564
  ]);
565
565
 
566
566
  const applyInitialScrollIndex = useCallback(() => {
567
- const { horizontal, data } = recyclerViewManager.props;
567
+ const { horizontal, data, initialScrollIndexParams } =
568
+ recyclerViewManager.props;
568
569
 
569
570
  const initialScrollIndex =
570
571
  recyclerViewManager.getInitialScrollIndex() ?? -1;
@@ -583,9 +584,11 @@ export function useRecyclerViewController<T>(
583
584
 
584
585
  pauseOffsetCorrection.current = true;
585
586
 
587
+ const additionalOffset = initialScrollIndexParams?.viewOffset ?? 0;
586
588
  const offset = horizontal
587
- ? recyclerViewManager.getLayout(initialScrollIndex).x
588
- : recyclerViewManager.getLayout(initialScrollIndex).y;
589
+ ? recyclerViewManager.getLayout(initialScrollIndex).x + additionalOffset
590
+ : recyclerViewManager.getLayout(initialScrollIndex).y +
591
+ additionalOffset;
589
592
  handlerMethods.scrollToOffset({
590
593
  offset,
591
594
  animated: false,
@@ -20,6 +20,7 @@ import { CompatAnimatedScroller } from "../components/CompatScroller";
20
20
  * - renderHeader: The header component renderer
21
21
  * - renderFooter: The footer component renderer
22
22
  * - renderEmpty: The empty state component renderer
23
+ * - renderStickyHeaderBackdrop: The sticky header backdrop component renderer
23
24
  * - CompatScrollView: The animated scroll component
24
25
  */
25
26
  export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
@@ -35,6 +36,7 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
35
36
  onRefresh,
36
37
  data,
37
38
  refreshControl: customRefreshControl,
39
+ stickyHeaderConfig,
38
40
  } = props;
39
41
 
40
42
  /**
@@ -94,6 +96,26 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
94
96
  return getValidComponent(ListEmptyComponent);
95
97
  }, [ListEmptyComponent, data]);
96
98
 
99
+ /**
100
+ * Creates the sticky header backdrop component.
101
+ */
102
+ const renderStickyHeaderBackdrop = useMemo(() => {
103
+ if (!stickyHeaderConfig?.backdropComponent) {
104
+ return null;
105
+ }
106
+ return (
107
+ <CompatView
108
+ style={{
109
+ position: "absolute",
110
+ inset: 0,
111
+ pointerEvents: "none",
112
+ }}
113
+ >
114
+ {getValidComponent(stickyHeaderConfig?.backdropComponent)}
115
+ </CompatView>
116
+ );
117
+ }, [stickyHeaderConfig?.backdropComponent]);
118
+
97
119
  /**
98
120
  * Creates an animated scroll component based on the provided renderScrollComponent.
99
121
  * If no custom component is provided, uses the default CompatAnimatedScroller.
@@ -120,5 +142,6 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
120
142
  renderFooter,
121
143
  renderEmpty,
122
144
  CompatScrollView,
145
+ renderStickyHeaderBackdrop,
123
146
  };
124
147
  }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=AverageWindow.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AverageWindow.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/AverageWindow.test.ts"],"names":[],"mappings":""}