@shopify/flash-list 2.0.0-alpha.8 → 2.0.0-rc.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 (197) hide show
  1. package/README.md +38 -98
  2. package/dist/AnimatedFlashList.d.ts.map +1 -1
  3. package/dist/AnimatedFlashList.js +3 -3
  4. package/dist/AnimatedFlashList.js.map +1 -1
  5. package/dist/FlashList.d.ts +9 -0
  6. package/dist/FlashList.d.ts.map +1 -1
  7. package/dist/FlashList.js +20 -0
  8. package/dist/FlashList.js.map +1 -1
  9. package/dist/FlashListProps.d.ts +15 -8
  10. package/dist/FlashListProps.d.ts.map +1 -1
  11. package/dist/FlashListProps.js.map +1 -1
  12. package/dist/FlashListRef.d.ts +305 -0
  13. package/dist/FlashListRef.d.ts.map +1 -0
  14. package/dist/FlashListRef.js +3 -0
  15. package/dist/FlashListRef.js.map +1 -0
  16. package/dist/MasonryFlashList.js.map +1 -1
  17. package/dist/__tests__/RecyclerView.test.js +63 -28
  18. package/dist/__tests__/RecyclerView.test.js.map +1 -1
  19. package/dist/__tests__/RenderStackManager.test.d.ts +2 -0
  20. package/dist/__tests__/RenderStackManager.test.d.ts.map +1 -0
  21. package/dist/__tests__/RenderStackManager.test.js +486 -0
  22. package/dist/__tests__/RenderStackManager.test.js.map +1 -0
  23. package/dist/__tests__/helpers/createLayoutManager.d.ts.map +1 -1
  24. package/dist/__tests__/helpers/createLayoutManager.js +3 -4
  25. package/dist/__tests__/helpers/createLayoutManager.js.map +1 -1
  26. package/dist/__tests__/useUnmountAwareCallbacks.test.js +1 -1
  27. package/dist/__tests__/useUnmountAwareCallbacks.test.js.map +1 -1
  28. package/dist/benchmark/useFlatListBenchmark.js +8 -7
  29. package/dist/benchmark/useFlatListBenchmark.js.map +1 -1
  30. package/dist/enableNewCore.d.ts.map +1 -1
  31. package/dist/enableNewCore.js +2 -1
  32. package/dist/enableNewCore.js.map +1 -1
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/native/config/PlatformHelper.android.d.ts +2 -0
  37. package/dist/native/config/PlatformHelper.android.d.ts.map +1 -1
  38. package/dist/native/config/PlatformHelper.android.js +2 -0
  39. package/dist/native/config/PlatformHelper.android.js.map +1 -1
  40. package/dist/native/config/PlatformHelper.d.ts +2 -0
  41. package/dist/native/config/PlatformHelper.d.ts.map +1 -1
  42. package/dist/native/config/PlatformHelper.ios.d.ts +2 -0
  43. package/dist/native/config/PlatformHelper.ios.d.ts.map +1 -1
  44. package/dist/native/config/PlatformHelper.ios.js +2 -0
  45. package/dist/native/config/PlatformHelper.ios.js.map +1 -1
  46. package/dist/native/config/PlatformHelper.js +2 -0
  47. package/dist/native/config/PlatformHelper.js.map +1 -1
  48. package/dist/native/config/PlatformHelper.web.d.ts +2 -0
  49. package/dist/native/config/PlatformHelper.web.d.ts.map +1 -1
  50. package/dist/native/config/PlatformHelper.web.js +3 -1
  51. package/dist/native/config/PlatformHelper.web.js.map +1 -1
  52. package/dist/recyclerview/RecyclerView.d.ts +2 -1
  53. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  54. package/dist/recyclerview/RecyclerView.js +64 -37
  55. package/dist/recyclerview/RecyclerView.js.map +1 -1
  56. package/dist/recyclerview/RecyclerViewContextProvider.d.ts +6 -5
  57. package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -1
  58. package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
  59. package/dist/recyclerview/RecyclerViewManager.d.ts +21 -7
  60. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  61. package/dist/recyclerview/RecyclerViewManager.js +105 -113
  62. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  63. package/dist/recyclerview/RenderStackManager.d.ts +85 -0
  64. package/dist/recyclerview/RenderStackManager.d.ts.map +1 -0
  65. package/dist/recyclerview/RenderStackManager.js +324 -0
  66. package/dist/recyclerview/RenderStackManager.js.map +1 -0
  67. package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
  68. package/dist/recyclerview/ViewHolder.js +5 -3
  69. package/dist/recyclerview/ViewHolder.js.map +1 -1
  70. package/dist/recyclerview/ViewHolderCollection.d.ts +3 -1
  71. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
  72. package/dist/recyclerview/ViewHolderCollection.js +23 -8
  73. package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
  74. package/dist/recyclerview/components/ScrollAnchor.d.ts +2 -1
  75. package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
  76. package/dist/recyclerview/components/ScrollAnchor.js +9 -4
  77. package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
  78. package/dist/recyclerview/components/StickyHeaders.d.ts +1 -1
  79. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
  80. package/dist/recyclerview/components/StickyHeaders.js +40 -32
  81. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  82. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +45 -1
  83. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -1
  84. package/dist/recyclerview/helpers/EngagedIndicesTracker.js +79 -20
  85. package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -1
  86. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +10 -0
  87. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -0
  88. package/dist/recyclerview/helpers/RenderTimeTracker.js +39 -0
  89. package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -0
  90. package/dist/recyclerview/helpers/VelocityTracker.d.ts +29 -0
  91. package/dist/recyclerview/helpers/VelocityTracker.d.ts.map +1 -0
  92. package/dist/recyclerview/helpers/VelocityTracker.js +70 -0
  93. package/dist/recyclerview/helpers/VelocityTracker.js.map +1 -0
  94. package/dist/recyclerview/hooks/useBoundDetection.d.ts +1 -2
  95. package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
  96. package/dist/recyclerview/hooks/useBoundDetection.js +19 -16
  97. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
  98. package/dist/recyclerview/hooks/useMappingHelper.d.ts +1 -1
  99. package/dist/recyclerview/hooks/useMappingHelper.d.ts.map +1 -1
  100. package/dist/recyclerview/hooks/useMappingHelper.js +1 -1
  101. package/dist/recyclerview/hooks/useMappingHelper.js.map +1 -1
  102. package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -1
  103. package/dist/recyclerview/hooks/useOnLoad.js +4 -6
  104. package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
  105. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +3 -48
  106. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  107. package/dist/recyclerview/hooks/useRecyclerViewController.js +179 -119
  108. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  109. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +2 -0
  110. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
  111. package/dist/recyclerview/hooks/useRecyclerViewManager.js +10 -1
  112. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
  113. package/dist/recyclerview/hooks/useSecondaryProps.js +1 -1
  114. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts +10 -3
  115. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts.map +1 -1
  116. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +33 -4
  117. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
  118. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +6 -0
  119. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  120. package/dist/recyclerview/layout-managers/GridLayoutManager.js +27 -5
  121. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  122. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +10 -16
  123. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
  124. package/dist/recyclerview/layout-managers/LayoutManager.js +4 -14
  125. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  126. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +2 -2
  127. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
  128. package/dist/recyclerview/utils/measureLayout.d.ts +1 -29
  129. package/dist/recyclerview/utils/measureLayout.d.ts.map +1 -1
  130. package/dist/recyclerview/utils/measureLayout.js +2 -4
  131. package/dist/recyclerview/utils/measureLayout.js.map +1 -1
  132. package/dist/recyclerview/utils/measureLayout.web.d.ts +29 -0
  133. package/dist/recyclerview/utils/measureLayout.web.d.ts.map +1 -0
  134. package/dist/recyclerview/utils/measureLayout.web.js +89 -0
  135. package/dist/recyclerview/utils/measureLayout.web.js.map +1 -0
  136. package/dist/tsconfig.tsbuildinfo +1 -1
  137. package/dist/viewability/ViewToken.d.ts +2 -2
  138. package/dist/viewability/ViewToken.d.ts.map +1 -1
  139. package/dist/viewability/ViewabilityHelper.js +1 -1
  140. package/dist/viewability/ViewabilityHelper.js.map +1 -1
  141. package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
  142. package/dist/viewability/ViewabilityManager.js +1 -2
  143. package/dist/viewability/ViewabilityManager.js.map +1 -1
  144. package/jestSetup.js +30 -11
  145. package/package.json +2 -1
  146. package/src/AnimatedFlashList.ts +3 -2
  147. package/src/FlashList.tsx +24 -0
  148. package/src/FlashListProps.ts +20 -8
  149. package/src/FlashListRef.ts +320 -0
  150. package/src/MasonryFlashList.tsx +2 -2
  151. package/src/__tests__/RecyclerView.test.tsx +84 -30
  152. package/src/__tests__/RenderStackManager.test.ts +575 -0
  153. package/src/__tests__/helpers/createLayoutManager.ts +2 -3
  154. package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
  155. package/src/benchmark/useFlatListBenchmark.ts +2 -2
  156. package/src/enableNewCore.ts +3 -1
  157. package/src/index.ts +1 -0
  158. package/src/native/config/PlatformHelper.android.ts +2 -0
  159. package/src/native/config/PlatformHelper.ios.ts +2 -0
  160. package/src/native/config/PlatformHelper.ts +2 -0
  161. package/src/native/config/PlatformHelper.web.ts +3 -1
  162. package/src/recyclerview/RecyclerView.tsx +85 -43
  163. package/src/recyclerview/RecyclerViewContextProvider.ts +12 -6
  164. package/src/recyclerview/RecyclerViewManager.ts +123 -98
  165. package/src/recyclerview/RenderStackManager.ts +291 -0
  166. package/src/recyclerview/ViewHolder.tsx +5 -3
  167. package/src/recyclerview/ViewHolderCollection.tsx +33 -12
  168. package/src/recyclerview/components/ScrollAnchor.tsx +21 -8
  169. package/src/recyclerview/components/StickyHeaders.tsx +63 -44
  170. package/src/recyclerview/helpers/EngagedIndicesTracker.ts +120 -23
  171. package/src/recyclerview/helpers/RenderTimeTracker.ts +38 -0
  172. package/src/recyclerview/helpers/VelocityTracker.ts +77 -0
  173. package/src/recyclerview/hooks/useBoundDetection.ts +25 -18
  174. package/src/recyclerview/hooks/useMappingHelper.ts +1 -1
  175. package/src/recyclerview/hooks/useOnLoad.ts +4 -6
  176. package/src/recyclerview/hooks/useRecyclerViewController.tsx +204 -173
  177. package/src/recyclerview/hooks/useRecyclerViewManager.ts +11 -1
  178. package/src/recyclerview/hooks/useSecondaryProps.tsx +1 -1
  179. package/src/recyclerview/hooks/useUnmountAwareCallbacks.ts +39 -3
  180. package/src/recyclerview/layout-managers/GridLayoutManager.ts +30 -7
  181. package/src/recyclerview/layout-managers/LayoutManager.ts +12 -21
  182. package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +1 -1
  183. package/src/recyclerview/utils/measureLayout.ts +3 -3
  184. package/src/recyclerview/utils/measureLayout.web.ts +104 -0
  185. package/src/viewability/ViewToken.ts +2 -2
  186. package/src/viewability/ViewabilityHelper.ts +1 -1
  187. package/src/viewability/ViewabilityManager.ts +6 -3
  188. package/dist/__tests__/RecycleKeyManager.test.d.ts +0 -2
  189. package/dist/__tests__/RecycleKeyManager.test.d.ts.map +0 -1
  190. package/dist/__tests__/RecycleKeyManager.test.js +0 -210
  191. package/dist/__tests__/RecycleKeyManager.test.js.map +0 -1
  192. package/dist/recyclerview/RecycleKeyManager.d.ts +0 -82
  193. package/dist/recyclerview/RecycleKeyManager.d.ts.map +0 -1
  194. package/dist/recyclerview/RecycleKeyManager.js +0 -135
  195. package/dist/recyclerview/RecycleKeyManager.js.map +0 -1
  196. package/src/__tests__/RecycleKeyManager.test.ts +0 -254
  197. package/src/recyclerview/RecycleKeyManager.ts +0 -185
@@ -88,7 +88,7 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
88
88
  );
89
89
 
90
90
  const separator = useMemo(() => {
91
- return ItemSeparatorComponent ? (
91
+ return ItemSeparatorComponent && trailingItem !== undefined ? (
92
92
  <ItemSeparatorComponent leadingItem={item} trailingItem={trailingItem} />
93
93
  ) : null;
94
94
  }, [ItemSeparatorComponent, item, trailingItem]);
@@ -97,7 +97,10 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
97
97
 
98
98
  const children = useMemo(() => {
99
99
  return renderItem?.({ item, index, extraData, target }) ?? null;
100
- }, [item, index, extraData, target, renderItem]);
100
+ // TODO: Test more thoroughly
101
+ // We don't really to re-render the children when the index changes
102
+ // eslint-disable-next-line react-hooks/exhaustive-deps
103
+ }, [item, extraData, target, renderItem]);
101
104
 
102
105
  const style = {
103
106
  flexDirection: horizontal ? "row" : "column",
@@ -110,7 +113,6 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
110
113
  maxWidth: layout.maxWidth,
111
114
  left: layout.x,
112
115
  top: layout.y,
113
- zIndex: 0,
114
116
  } as const;
115
117
 
116
118
  // TODO: Fix this type issue
@@ -21,7 +21,7 @@ export interface ViewHolderCollectionProps<TItem> {
21
21
  /** The data array to be rendered */
22
22
  data: FlashListProps<TItem>["data"];
23
23
  /** Map of indices to React keys for each rendered item */
24
- renderStack: Map<number, string>;
24
+ renderStack: Map<string, { index: number }>;
25
25
  /** Function to get layout information for a specific index */
26
26
  getLayout: (index: number) => RVLayout;
27
27
  /** Ref to control layout updates from parent components */
@@ -99,43 +99,64 @@ export const ViewHolderCollection = <TItem,>(
99
99
  // );
100
100
  recyclerViewContext?.layout();
101
101
  }
102
+ // we need to run this callback on when fixedContainerSize changes
103
+ // eslint-disable-next-line react-hooks/exhaustive-deps
102
104
  }, [fixedContainerSize]);
103
105
 
104
106
  useLayoutEffect(() => {
105
107
  if (renderId > 0) {
106
108
  onCommitLayoutEffect?.();
107
109
  }
110
+ // we need to run this callback on when renderId changes
111
+ // eslint-disable-next-line react-hooks/exhaustive-deps
108
112
  }, [renderId]);
109
113
 
110
114
  useEffect(() => {
111
115
  if (renderId > 0) {
112
116
  onCommitEffect?.();
113
117
  }
118
+ // we need to run this callback on when renderId changes
119
+ // eslint-disable-next-line react-hooks/exhaustive-deps
114
120
  }, [renderId]);
115
121
 
116
122
  // Expose forceUpdate through ref
117
- useImperativeHandle(viewHolderCollectionRef, () => ({
118
- commitLayout: () => {
119
- // This will trigger a re-render of the component
120
- setRenderId((prev) => prev + 1);
121
- },
122
- }));
123
+ useImperativeHandle(
124
+ viewHolderCollectionRef,
125
+ () => ({
126
+ commitLayout: () => {
127
+ // This will trigger a re-render of the component
128
+ setRenderId((prev) => prev + 1);
129
+ },
130
+ }),
131
+ [setRenderId]
132
+ );
123
133
 
124
134
  const hasData = data && data.length > 0;
125
135
 
126
136
  const containerStyle = {
127
137
  width: horizontal ? containerLayout?.width : undefined,
128
138
  height: containerLayout?.height,
139
+ // TODO: Temp workaround, useLayoutEffect doesn't block paint in some cases
140
+ // We need to investigate why this is happening
141
+ opacity: renderId > 0 ? 1 : 0,
129
142
  };
130
143
 
144
+ // sort by index and log
145
+ // const sortedRenderStack = Array.from(renderStack.entries()).sort(
146
+ // ([, a], [, b]) => a.index - b.index
147
+ // );
148
+ // console.log(
149
+ // "sortedRenderStack",
150
+ // sortedRenderStack.map(([reactKey, { index }]) => {
151
+ // return `${index} => ${reactKey}`;
152
+ // })
153
+ // );
154
+
131
155
  return (
132
- <CompatView
133
- // TODO: Take care of web scroll bar here
134
- style={hasData && containerStyle}
135
- >
156
+ <CompatView style={hasData && containerStyle}>
136
157
  {containerLayout &&
137
158
  hasData &&
138
- Array.from(renderStack, ([index, reactKey]) => {
159
+ Array.from(renderStack.entries(), ([reactKey, { index }]) => {
139
160
  const item = data[index];
140
161
  const trailingItem = ItemSeparatorComponent
141
162
  ? data[index + 1]
@@ -15,6 +15,7 @@ import { CompatView } from "./CompatView";
15
15
  export interface ScrollAnchorProps {
16
16
  /** Ref to access scroll anchor methods */
17
17
  scrollAnchorRef: React.Ref<ScrollAnchorRef>;
18
+ horizontal: boolean;
18
19
  }
19
20
 
20
21
  /**
@@ -30,24 +31,36 @@ export interface ScrollAnchorRef {
30
31
  * @param props - Component props
31
32
  * @returns An invisible anchor element used for scrolling
32
33
  */
33
- export function ScrollAnchor({ scrollAnchorRef }: ScrollAnchorProps) {
34
+ export function ScrollAnchor({
35
+ scrollAnchorRef,
36
+ horizontal,
37
+ }: ScrollAnchorProps) {
34
38
  const [scrollOffset, setScrollOffset] = useState(1000000); // TODO: Fix this value
35
39
 
36
40
  // Expose scrollBy method through ref
37
- useImperativeHandle(scrollAnchorRef, () => ({
38
- scrollBy: (offset: number) => {
39
- setScrollOffset((prev) => prev + offset);
40
- },
41
- }));
41
+ useImperativeHandle(
42
+ scrollAnchorRef,
43
+ () => ({
44
+ scrollBy: (offset: number) => {
45
+ setScrollOffset((prev) => prev + offset);
46
+ },
47
+ }),
48
+ []
49
+ );
42
50
 
43
51
  // Create an invisible anchor element that can be positioned
44
52
  const anchor = useMemo(() => {
45
53
  return (
46
54
  <CompatView
47
- style={{ position: "absolute", height: 0, top: scrollOffset }}
55
+ style={{
56
+ position: "absolute",
57
+ height: 0,
58
+ top: horizontal ? 0 : scrollOffset,
59
+ left: horizontal ? scrollOffset : 0,
60
+ }}
48
61
  />
49
62
  );
50
- }, [scrollOffset]);
63
+ }, [scrollOffset, horizontal]);
51
64
 
52
65
  return anchor;
53
66
  }
@@ -49,6 +49,11 @@ export interface StickyHeaderRef {
49
49
  reportScrollEvent: (event: NativeScrollEvent) => void;
50
50
  }
51
51
 
52
+ interface StickyHeaderState {
53
+ currentStickyIndex: number;
54
+ pushStartsAt: number;
55
+ }
56
+
52
57
  export const StickyHeaders = <TItem,>({
53
58
  stickyHeaderIndices,
54
59
  renderItem,
@@ -58,25 +63,35 @@ export const StickyHeaders = <TItem,>({
58
63
  data,
59
64
  extraData,
60
65
  }: StickyHeaderProps<TItem>) => {
61
- const [stickyIndices, setStickyIndices] = useState<{
62
- currentStickyIndex: number;
63
- nextStickyIndex: number;
64
- }>({ currentStickyIndex: -1, nextStickyIndex: -1 });
66
+ const [stickyHeaderState, setStickyHeaderState] = useState<StickyHeaderState>(
67
+ {
68
+ currentStickyIndex: -1,
69
+ pushStartsAt: Number.MAX_SAFE_INTEGER,
70
+ }
71
+ );
65
72
 
66
- const { currentStickyIndex, nextStickyIndex } = stickyIndices;
73
+ const { currentStickyIndex, pushStartsAt } = stickyHeaderState;
67
74
 
68
- // Memoize sorted indices based on their Y positions
75
+ // sort indices and memoize compute
69
76
  const sortedIndices = useMemo(() => {
70
77
  return stickyHeaderIndices.sort((first, second) => first - second);
71
78
  }, [stickyHeaderIndices]);
72
79
 
80
+ const legthInvalid =
81
+ sortedIndices.length === 0 ||
82
+ recyclerViewManager.getDataLength() <=
83
+ sortedIndices[sortedIndices.length - 1];
84
+
73
85
  const compute = useCallback(() => {
74
- const adjustedValue = recyclerViewManager.getLastScrollOffset();
86
+ if (legthInvalid) {
87
+ return;
88
+ }
89
+ const adjustedScrollOffset = recyclerViewManager.getLastScrollOffset();
75
90
 
76
91
  // Binary search for current sticky index
77
92
  const currentIndexInArray = findCurrentStickyIndex(
78
93
  sortedIndices,
79
- adjustedValue,
94
+ adjustedScrollOffset,
80
95
  (index) => recyclerViewManager.getLayout(index).y
81
96
  );
82
97
 
@@ -87,16 +102,33 @@ export const StickyHeaders = <TItem,>({
87
102
  newNextStickyIndex = -1;
88
103
  }
89
104
 
105
+ // To make sure header offset is 0 in the interpolate compute
106
+ const newNextStickyY =
107
+ newNextStickyIndex === -1
108
+ ? Number.MAX_SAFE_INTEGER
109
+ : (recyclerViewManager.tryGetLayout(newNextStickyIndex)?.y ?? 0) +
110
+ recyclerViewManager.firstItemOffset;
111
+ const newCurrentStickyHeight =
112
+ recyclerViewManager.tryGetLayout(newStickyIndex)?.height ?? 0;
113
+
114
+ const newPushStartsAt = newNextStickyY - newCurrentStickyHeight;
115
+
90
116
  if (
91
117
  newStickyIndex !== currentStickyIndex ||
92
- newNextStickyIndex !== nextStickyIndex
118
+ newPushStartsAt !== pushStartsAt
93
119
  ) {
94
- setStickyIndices({
120
+ setStickyHeaderState({
95
121
  currentStickyIndex: newStickyIndex,
96
- nextStickyIndex: newNextStickyIndex,
122
+ pushStartsAt: newPushStartsAt,
97
123
  });
98
124
  }
99
- }, [currentStickyIndex, nextStickyIndex, recyclerViewManager, sortedIndices]);
125
+ }, [
126
+ legthInvalid,
127
+ recyclerViewManager,
128
+ sortedIndices,
129
+ currentStickyIndex,
130
+ pushStartsAt,
131
+ ]);
100
132
 
101
133
  useEffect(() => {
102
134
  compute();
@@ -115,35 +147,19 @@ export const StickyHeaders = <TItem,>({
115
147
 
116
148
  const refHolder = useRef(new Map()).current;
117
149
 
118
- // Memoize translateY calculation
119
150
  const translateY = useMemo(() => {
120
- if (currentStickyIndex === -1 || nextStickyIndex === -1) {
121
- return scrollY.interpolate({
122
- inputRange: [0, Infinity],
123
- outputRange: [0, 0],
124
- extrapolate: "clamp",
125
- });
126
- }
127
-
128
- const currentLayout = recyclerViewManager.getLayout(currentStickyIndex);
129
- const nextLayout = recyclerViewManager.getLayout(nextStickyIndex);
130
-
131
- const pushStartsAt = nextLayout.y - currentLayout.height;
151
+ const currentStickyHeight =
152
+ recyclerViewManager.tryGetLayout(currentStickyIndex)?.height ?? 0;
132
153
 
133
154
  return scrollY.interpolate({
134
- inputRange: [
135
- pushStartsAt + recyclerViewManager.firstItemOffset,
136
- nextLayout.y + recyclerViewManager.firstItemOffset,
137
- ],
138
- outputRange: [0, -currentLayout.height],
155
+ inputRange: [pushStartsAt, pushStartsAt + currentStickyHeight],
156
+ outputRange: [0, -currentStickyHeight],
139
157
  extrapolate: "clamp",
140
158
  });
141
- }, [currentStickyIndex, nextStickyIndex, recyclerViewManager, scrollY]);
159
+ }, [recyclerViewManager, currentStickyIndex, scrollY, pushStartsAt]);
142
160
 
143
161
  // Memoize header content
144
162
  const headerContent = useMemo(() => {
145
- if (currentStickyIndex === -1) return null;
146
-
147
163
  return (
148
164
  <CompatAnimatedView
149
165
  style={{
@@ -151,22 +167,25 @@ export const StickyHeaders = <TItem,>({
151
167
  top: 0,
152
168
  left: 0,
153
169
  right: 0,
170
+ zIndex: 1,
154
171
  transform: [{ translateY }],
155
172
  }}
156
173
  >
157
- <ViewHolder
158
- index={currentStickyIndex}
159
- item={data[currentStickyIndex]}
160
- renderItem={renderItem}
161
- layout={{ x: 0, y: 0, width: 0, height: 0 }}
162
- refHolder={refHolder}
163
- extraData={extraData}
164
- trailingItem={null}
165
- target="StickyHeader"
166
- />
174
+ {currentStickyIndex !== -1 ? (
175
+ <ViewHolder
176
+ index={currentStickyIndex}
177
+ item={data[currentStickyIndex]}
178
+ renderItem={renderItem}
179
+ layout={{ x: 0, y: 0, width: 0, height: 0 }}
180
+ refHolder={refHolder}
181
+ extraData={extraData}
182
+ trailingItem={null}
183
+ target="StickyHeader"
184
+ />
185
+ ) : null}
167
186
  </CompatAnimatedView>
168
187
  );
169
- }, [currentStickyIndex, data, renderItem, extraData, refHolder, translateY]);
188
+ }, [translateY, currentStickyIndex, data, renderItem, refHolder, extraData]);
170
189
 
171
190
  return headerContent;
172
191
  };
@@ -1,3 +1,4 @@
1
+ import { PlatformConfig } from "../../native/config/PlatformHelper";
1
2
  import { RVLayoutManager } from "../layout-managers/LayoutManager";
2
3
 
3
4
  import { ConsecutiveNumbers } from "./ConsecutiveNumbers";
@@ -7,6 +8,10 @@ export interface RVEngagedIndicesTracker {
7
8
  scrollOffset: number;
8
9
  // Total distance (in pixels) to pre-render items before and after the visible viewport
9
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;
10
15
 
11
16
  /**
12
17
  * Updates the scroll offset and calculates which items should be rendered (engaged indices).
@@ -20,9 +25,31 @@ export interface RVEngagedIndicesTracker {
20
25
  velocity: Velocity | null | undefined,
21
26
  layoutManager: RVLayoutManager
22
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
+ */
23
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
+ */
24
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
+ */
25
47
  setScrollDirection: (scrollDirection: "forward" | "backward") => void;
48
+
49
+ /**
50
+ * Resets the velocity history based on the current scroll direction.
51
+ */
52
+ resetVelocityHistory: () => void;
26
53
  }
27
54
 
28
55
  export interface Velocity {
@@ -34,16 +61,26 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
34
61
  // Current scroll position of the list
35
62
  public scrollOffset = 0;
36
63
  // Distance to pre-render items before and after the visible viewport (in pixels)
37
- public drawDistance = 250;
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
+
38
75
  // Currently rendered item indices (including buffer items)
39
76
  private engagedIndices = ConsecutiveNumbers.EMPTY;
40
77
 
41
78
  // Buffer distribution multipliers for scroll direction optimization
42
- private smallMultiplier = 0.1; // Used for buffer in the opposite direction of scroll
43
- 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
44
81
 
45
82
  // Circular buffer to track recent scroll velocities for direction detection
46
- private velocityHistory = [-1, -1, -1, -1, -1];
83
+ private velocityHistory = [0, 0, 0, -0.1, -0.1];
47
84
  private velocityIndex = 0;
48
85
 
49
86
  /**
@@ -65,7 +102,21 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
65
102
  // STEP 1: Determine the currently visible viewport
66
103
  const windowSize = layoutManager.getWindowsSize();
67
104
  const isHorizontal = layoutManager.isHorizontal();
68
- 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
+
69
120
  const viewportSize = isHorizontal ? windowSize.width : windowSize.height;
70
121
  const viewportEnd = viewportStart + viewportSize;
71
122
 
@@ -73,11 +124,6 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
73
124
  // The total extra space where items will be pre-rendered
74
125
  const totalBuffer = this.drawDistance * 2;
75
126
 
76
- // Determine scroll direction to optimize buffer distribution
77
- const isScrollingBackward = this.isScrollingBackward(
78
- isHorizontal ? velocity?.x : velocity?.y
79
- );
80
-
81
127
  // Distribute more buffer in the direction of scrolling
82
128
  // When scrolling forward: more buffer after viewport
83
129
  // When scrolling backward: more buffer before viewport
@@ -121,9 +167,12 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
121
167
  extendedStart,
122
168
  extendedEnd
123
169
  );
124
- if (!isHorizontal) {
125
- // console.log("newEngagedIndices", newEngagedIndices, this.scrollOffset);
126
- }
170
+ // console.log(
171
+ // "newEngagedIndices",
172
+ // newEngagedIndices,
173
+ // this.scrollOffset,
174
+ // viewportStart
175
+ // );
127
176
  // Only return new indices if they've changed
128
177
  const oldEngagedIndices = this.engagedIndices;
129
178
  this.engagedIndices = newEngagedIndices;
@@ -133,19 +182,21 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
133
182
  : newEngagedIndices;
134
183
  }
135
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
+
136
194
  /**
137
195
  * Determines scroll direction by analyzing recent velocity history.
138
196
  * Uses a majority voting system on the last 5 velocity values.
139
- * @param velocity - Current scroll velocity component (x or y)
140
197
  * @returns true if scrolling backward (negative direction), false otherwise
141
198
  */
142
- private isScrollingBackward(velocity?: number): boolean {
143
- // update velocity history
144
- if (velocity) {
145
- this.velocityHistory[this.velocityIndex] = velocity;
146
- this.velocityIndex =
147
- (this.velocityIndex + 1) % this.velocityHistory.length;
148
- }
199
+ private isScrollingBackward(): boolean {
149
200
  // should decide based on whether we have more positive or negative values, use for loop
150
201
  let positiveCount = 0;
151
202
  let negativeCount = 0;
@@ -160,6 +211,40 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
160
211
  return positiveCount < negativeCount;
161
212
  }
162
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
+
163
248
  /**
164
249
  * Calculates which items are currently visible in the viewport.
165
250
  * Unlike getEngagedIndices, this doesn't include buffer items.
@@ -194,11 +279,23 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
194
279
 
195
280
  setScrollDirection(scrollDirection: "forward" | "backward") {
196
281
  if (scrollDirection === "forward") {
197
- this.velocityHistory = [1, 1, 1, 1, 1];
282
+ this.velocityHistory = [0, 0, 0, 0.1, 0.1];
198
283
  this.velocityIndex = 0;
199
284
  } else {
200
- this.velocityHistory = [-1, -1, -1, -1, -1];
285
+ this.velocityHistory = [0, 0, 0, -0.1, -0.1];
201
286
  this.velocityIndex = 0;
202
287
  }
203
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
+ }
204
301
  }
@@ -0,0 +1,38 @@
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
+ getAverageRenderTime() {
30
+ if (!PlatformConfig.trackAverageRenderTimeForOffsetProjection) {
31
+ return this.defaultRenderTime;
32
+ }
33
+ return Math.min(
34
+ this.maxRenderTime,
35
+ Math.max(Math.round(this.renderTimeAvgWindow.currentValue), 16)
36
+ );
37
+ }
38
+ }
@@ -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
+ }