@shopify/flash-list 1.8.0 → 2.0.0-alpha.2

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 (170) hide show
  1. package/README.md +147 -26
  2. package/dist/FlashListProps.d.ts +65 -2
  3. package/dist/FlashListProps.d.ts.map +1 -1
  4. package/dist/__tests__/AverageWindow.test.js +35 -0
  5. package/dist/__tests__/AverageWindow.test.js.map +1 -1
  6. package/dist/enableNewCore.d.ts +3 -0
  7. package/dist/enableNewCore.d.ts.map +1 -0
  8. package/dist/enableNewCore.js +25 -0
  9. package/dist/enableNewCore.js.map +1 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +28 -8
  13. package/dist/index.js.map +1 -1
  14. package/dist/recyclerview/RecycleKeyManager.d.ts +82 -0
  15. package/dist/recyclerview/RecycleKeyManager.d.ts.map +1 -0
  16. package/dist/recyclerview/RecycleKeyManager.js +135 -0
  17. package/dist/recyclerview/RecycleKeyManager.js.map +1 -0
  18. package/dist/recyclerview/RecyclerView.d.ts +12 -0
  19. package/dist/recyclerview/RecyclerView.d.ts.map +1 -0
  20. package/dist/recyclerview/RecyclerView.js +283 -0
  21. package/dist/recyclerview/RecyclerView.js.map +1 -0
  22. package/dist/recyclerview/RecyclerViewContextProvider.d.ts +12 -0
  23. package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -0
  24. package/dist/recyclerview/RecyclerViewContextProvider.js +11 -0
  25. package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -0
  26. package/dist/recyclerview/RecyclerViewManager.d.ts +52 -0
  27. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -0
  28. package/dist/recyclerview/RecyclerViewManager.js +323 -0
  29. package/dist/recyclerview/RecyclerViewManager.js.map +1 -0
  30. package/dist/recyclerview/RecyclerViewProps.d.ts +9 -0
  31. package/dist/recyclerview/RecyclerViewProps.d.ts.map +1 -0
  32. package/dist/recyclerview/RecyclerViewProps.js +3 -0
  33. package/dist/recyclerview/RecyclerViewProps.js.map +1 -0
  34. package/dist/recyclerview/ViewHolder.d.ts +45 -0
  35. package/dist/recyclerview/ViewHolder.d.ts.map +1 -0
  36. package/dist/recyclerview/ViewHolder.js +96 -0
  37. package/dist/recyclerview/ViewHolder.js.map +1 -0
  38. package/dist/recyclerview/ViewHolderCollection.d.ts +57 -0
  39. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -0
  40. package/dist/recyclerview/ViewHolderCollection.js +75 -0
  41. package/dist/recyclerview/ViewHolderCollection.js.map +1 -0
  42. package/dist/recyclerview/components/CompatScroller.d.ts +7 -0
  43. package/dist/recyclerview/components/CompatScroller.d.ts.map +1 -0
  44. package/dist/recyclerview/components/CompatScroller.js +8 -0
  45. package/dist/recyclerview/components/CompatScroller.js.map +1 -0
  46. package/dist/recyclerview/components/CompatView.d.ts +7 -0
  47. package/dist/recyclerview/components/CompatView.d.ts.map +1 -0
  48. package/dist/recyclerview/components/CompatView.js +8 -0
  49. package/dist/recyclerview/components/CompatView.js.map +1 -0
  50. package/dist/recyclerview/components/ScrollAnchor.d.ts +28 -0
  51. package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -0
  52. package/dist/recyclerview/components/ScrollAnchor.js +35 -0
  53. package/dist/recyclerview/components/ScrollAnchor.js.map +1 -0
  54. package/dist/recyclerview/components/StickyHeaders.d.ts +38 -0
  55. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -0
  56. package/dist/recyclerview/components/StickyHeaders.js +119 -0
  57. package/dist/recyclerview/components/StickyHeaders.js.map +1 -0
  58. package/dist/recyclerview/helpers/ConsecutiveNumbers.d.ts +51 -0
  59. package/dist/recyclerview/helpers/ConsecutiveNumbers.d.ts.map +1 -0
  60. package/dist/recyclerview/helpers/ConsecutiveNumbers.js +122 -0
  61. package/dist/recyclerview/helpers/ConsecutiveNumbers.js.map +1 -0
  62. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +59 -0
  63. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -0
  64. package/dist/recyclerview/helpers/EngagedIndicesTracker.js +138 -0
  65. package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -0
  66. package/dist/recyclerview/hooks/useBoundDetection.d.ts +19 -0
  67. package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -0
  68. package/dist/recyclerview/hooks/useBoundDetection.js +103 -0
  69. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -0
  70. package/dist/recyclerview/hooks/useLayoutState.d.ts +12 -0
  71. package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -0
  72. package/dist/recyclerview/hooks/useLayoutState.js +43 -0
  73. package/dist/recyclerview/hooks/useLayoutState.js.map +1 -0
  74. package/dist/recyclerview/hooks/useOnLoad.d.ts +25 -0
  75. package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -0
  76. package/dist/recyclerview/hooks/useOnLoad.js +73 -0
  77. package/dist/recyclerview/hooks/useOnLoad.js.map +1 -0
  78. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +72 -0
  79. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -0
  80. package/dist/recyclerview/hooks/useRecyclerViewController.js +370 -0
  81. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -0
  82. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +6 -0
  83. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -0
  84. package/dist/recyclerview/hooks/useRecyclerViewManager.js +27 -0
  85. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -0
  86. package/dist/recyclerview/hooks/useRecyclingState.d.ts +16 -0
  87. package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -0
  88. package/dist/recyclerview/hooks/useRecyclingState.js +54 -0
  89. package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -0
  90. package/dist/recyclerview/hooks/useSecondaryProps.d.ts +27 -0
  91. package/dist/recyclerview/hooks/useSecondaryProps.d.ts.map +1 -0
  92. package/dist/recyclerview/hooks/useSecondaryProps.js +93 -0
  93. package/dist/recyclerview/hooks/useSecondaryProps.js.map +1 -0
  94. package/dist/recyclerview/hooks/useUnmountFlag.d.ts +11 -0
  95. package/dist/recyclerview/hooks/useUnmountFlag.d.ts.map +1 -0
  96. package/dist/recyclerview/hooks/useUnmountFlag.js +28 -0
  97. package/dist/recyclerview/hooks/useUnmountFlag.js.map +1 -0
  98. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +65 -0
  99. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -0
  100. package/dist/recyclerview/layout-managers/GridLayoutManager.js +204 -0
  101. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -0
  102. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +281 -0
  103. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -0
  104. package/dist/recyclerview/layout-managers/LayoutManager.js +250 -0
  105. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -0
  106. package/dist/recyclerview/layout-managers/LinearLayoutManager.d.ts +52 -0
  107. package/dist/recyclerview/layout-managers/LinearLayoutManager.d.ts.map +1 -0
  108. package/dist/recyclerview/layout-managers/LinearLayoutManager.js +191 -0
  109. package/dist/recyclerview/layout-managers/LinearLayoutManager.js.map +1 -0
  110. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +73 -0
  111. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -0
  112. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +274 -0
  113. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -0
  114. package/dist/recyclerview/utils/adjustOffsetForRTL.d.ts +12 -0
  115. package/dist/recyclerview/utils/adjustOffsetForRTL.d.ts.map +1 -0
  116. package/dist/recyclerview/utils/adjustOffsetForRTL.js +18 -0
  117. package/dist/recyclerview/utils/adjustOffsetForRTL.js.map +1 -0
  118. package/dist/recyclerview/utils/componentUtils.d.ts +19 -0
  119. package/dist/recyclerview/utils/componentUtils.d.ts.map +1 -0
  120. package/dist/recyclerview/utils/componentUtils.js +32 -0
  121. package/dist/recyclerview/utils/componentUtils.js.map +1 -0
  122. package/dist/recyclerview/utils/findVisibleIndex.d.ts +24 -0
  123. package/dist/recyclerview/utils/findVisibleIndex.d.ts.map +1 -0
  124. package/dist/recyclerview/utils/findVisibleIndex.js +82 -0
  125. package/dist/recyclerview/utils/findVisibleIndex.js.map +1 -0
  126. package/dist/recyclerview/utils/measureLayout.d.ts +56 -0
  127. package/dist/recyclerview/utils/measureLayout.d.ts.map +1 -0
  128. package/dist/recyclerview/utils/measureLayout.js +77 -0
  129. package/dist/recyclerview/utils/measureLayout.js.map +1 -0
  130. package/dist/tsconfig.tsbuildinfo +1 -1
  131. package/dist/utils/AverageWindow.d.ts +13 -0
  132. package/dist/utils/AverageWindow.d.ts.map +1 -1
  133. package/dist/utils/AverageWindow.js +30 -1
  134. package/dist/utils/AverageWindow.js.map +1 -1
  135. package/package.json +1 -1
  136. package/src/FlashListProps.ts +73 -2
  137. package/src/__tests__/AverageWindow.test.ts +49 -1
  138. package/src/enableNewCore.ts +22 -0
  139. package/src/index.ts +21 -0
  140. package/src/recyclerview/RecycleKeyManager.ts +185 -0
  141. package/src/recyclerview/RecyclerView.tsx +500 -0
  142. package/src/recyclerview/RecyclerViewContextProvider.ts +19 -0
  143. package/src/recyclerview/RecyclerViewManager.ts +379 -0
  144. package/src/recyclerview/RecyclerViewProps.ts +10 -0
  145. package/src/recyclerview/ViewHolder.tsx +173 -0
  146. package/src/recyclerview/ViewHolderCollection.tsx +164 -0
  147. package/src/recyclerview/components/CompatScroller.ts +9 -0
  148. package/src/recyclerview/components/CompatView.ts +9 -0
  149. package/src/recyclerview/components/ScrollAnchor.tsx +53 -0
  150. package/src/recyclerview/components/StickyHeaders.tsx +210 -0
  151. package/src/recyclerview/helpers/ConsecutiveNumbers.ts +120 -0
  152. package/src/recyclerview/helpers/EngagedIndicesTracker.ts +191 -0
  153. package/src/recyclerview/hooks/useBoundDetection.ts +127 -0
  154. package/src/recyclerview/hooks/useLayoutState.ts +46 -0
  155. package/src/recyclerview/hooks/useOnLoad.ts +78 -0
  156. package/src/recyclerview/hooks/useRecyclerViewController.tsx +487 -0
  157. package/src/recyclerview/hooks/useRecyclerViewManager.ts +30 -0
  158. package/src/recyclerview/hooks/useRecyclingState.ts +63 -0
  159. package/src/recyclerview/hooks/useSecondaryProps.tsx +119 -0
  160. package/src/recyclerview/hooks/useUnmountFlag.ts +26 -0
  161. package/src/recyclerview/layout-managers/GridLayoutManager.ts +215 -0
  162. package/src/recyclerview/layout-managers/LayoutManager.ts +493 -0
  163. package/src/recyclerview/layout-managers/LinearLayoutManager.ts +167 -0
  164. package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +302 -0
  165. package/src/recyclerview/utils/adjustOffsetForRTL.ts +17 -0
  166. package/src/recyclerview/utils/componentUtils.ts +28 -0
  167. package/src/recyclerview/utils/findVisibleIndex.ts +94 -0
  168. package/src/recyclerview/utils/measureLayout.ts +89 -0
  169. package/src/utils/AverageWindow.ts +33 -0
  170. package/src/viewability/ViewToken.ts +1 -1
@@ -0,0 +1,164 @@
1
+ /**
2
+ * ViewHolderCollection is a container component that manages multiple ViewHolder instances.
3
+ * It handles the rendering of a collection of list items, manages layout updates,
4
+ * and coordinates with the RecyclerView context for layout changes.
5
+ */
6
+
7
+ import React, { useEffect, useImperativeHandle, useLayoutEffect } from "react";
8
+ import { ViewHolder, ViewHolderProps } from "./ViewHolder";
9
+ import { RVDimension, RVLayout } from "./layout-managers/LayoutManager";
10
+ import { FlashListProps } from "../FlashListProps";
11
+ import { CompatView } from "./components/CompatView";
12
+ import { useRecyclerViewContext } from "./RecyclerViewContextProvider";
13
+
14
+ /**
15
+ * Props interface for the ViewHolderCollection component
16
+ * @template TItem - The type of items in the data array
17
+ */
18
+ export interface ViewHolderCollectionProps<TItem> {
19
+ /** The data array to be rendered */
20
+ data: FlashListProps<TItem>["data"];
21
+ /** Map of indices to React keys for each rendered item */
22
+ renderStack: Map<number, string>;
23
+ /** Function to get layout information for a specific index */
24
+ getLayout: (index: number) => RVLayout;
25
+ /** Ref to control layout updates from parent components */
26
+ viewHolderCollectionRef: React.Ref<ViewHolderCollectionRef>;
27
+ /** Map to store refs for each ViewHolder instance */
28
+ refHolder: ViewHolderProps<TItem>["refHolder"];
29
+ /** Callback when any item's size changes */
30
+ onSizeChanged: ViewHolderProps<TItem>["onSizeChanged"];
31
+ /** Function to render each item */
32
+ renderItem: FlashListProps<TItem>["renderItem"];
33
+ /** Additional data passed to renderItem that can trigger re-renders */
34
+ extraData: any;
35
+ /** Function to get the container's layout dimensions */
36
+ getChildContainerLayout: () => RVDimension | undefined;
37
+ /** Callback after layout effects are committed */
38
+ onCommitLayoutEffect?: () => void;
39
+ /** Callback after effects are committed */
40
+ onCommitEffect?: () => void;
41
+ /** Optional custom component to wrap each item */
42
+ CellRendererComponent?: FlashListProps<TItem>["CellRendererComponent"];
43
+ /** Optional component to render between items */
44
+ ItemSeparatorComponent?: FlashListProps<TItem>["ItemSeparatorComponent"];
45
+ /** Whether the list is horizontal or vertical */
46
+ horizontal: FlashListProps<TItem>["horizontal"];
47
+ }
48
+
49
+ /**
50
+ * Ref interface for ViewHolderCollection that exposes methods to control layout updates
51
+ */
52
+ export interface ViewHolderCollectionRef {
53
+ /** Forces a layout update by triggering a re-render */
54
+ commitLayout: () => void;
55
+ }
56
+
57
+ /**
58
+ * ViewHolderCollection component that manages the rendering of multiple ViewHolder instances
59
+ * and handles layout updates for the entire collection
60
+ * @template TItem - The type of items in the data array
61
+ */
62
+ export const ViewHolderCollection = <TItem,>(
63
+ props: ViewHolderCollectionProps<TItem>
64
+ ) => {
65
+ const {
66
+ data,
67
+ renderStack,
68
+ getLayout,
69
+ refHolder,
70
+ onSizeChanged,
71
+ renderItem,
72
+ extraData,
73
+ viewHolderCollectionRef,
74
+ getChildContainerLayout,
75
+ onCommitLayoutEffect,
76
+ CellRendererComponent,
77
+ ItemSeparatorComponent,
78
+ onCommitEffect,
79
+ horizontal,
80
+ } = props;
81
+
82
+ const [renderId, setRenderId] = React.useState(0);
83
+
84
+ const containerLayout = getChildContainerLayout();
85
+
86
+ // TODO: guard againt precision issues
87
+ const fixedContainerSize = horizontal
88
+ ? containerLayout?.height
89
+ : containerLayout?.width;
90
+
91
+ const recyclerViewContext = useRecyclerViewContext();
92
+
93
+ useLayoutEffect(() => {
94
+ if (renderId > 0) {
95
+ // console.log(
96
+ // "parent layout trigger due to child container size change",
97
+ // fixedContainerSize
98
+ // );
99
+ recyclerViewContext?.layout();
100
+ }
101
+ }, [fixedContainerSize]);
102
+
103
+ useLayoutEffect(() => {
104
+ if (renderId > 0) {
105
+ onCommitLayoutEffect?.();
106
+ }
107
+ }, [renderId]);
108
+
109
+ useEffect(() => {
110
+ if (renderId > 0) {
111
+ onCommitEffect?.();
112
+ }
113
+ }, [renderId]);
114
+
115
+ // Expose forceUpdate through ref
116
+ useImperativeHandle(viewHolderCollectionRef, () => ({
117
+ commitLayout: () => {
118
+ // This will trigger a re-render of the component
119
+ setRenderId((prev) => prev + 1);
120
+ },
121
+ }));
122
+
123
+ const hasData = data && data.length > 0;
124
+
125
+ const containerStyle = {
126
+ width: horizontal ? containerLayout?.width : undefined,
127
+ height: containerLayout?.height,
128
+ };
129
+
130
+ return (
131
+ <CompatView
132
+ // TODO: Take care of web scroll bar here
133
+ style={hasData && containerStyle}
134
+ >
135
+ {containerLayout &&
136
+ hasData &&
137
+ Array.from(renderStack, ([index, reactKey]) => {
138
+ const item = data[index];
139
+ const trailingItem = ItemSeparatorComponent
140
+ ? data[index + 1]
141
+ : undefined;
142
+ return (
143
+ <ViewHolder
144
+ key={reactKey}
145
+ index={index}
146
+ item={item}
147
+ trailingItem={trailingItem}
148
+ layout={{
149
+ ...getLayout(index),
150
+ }}
151
+ refHolder={refHolder}
152
+ onSizeChanged={onSizeChanged}
153
+ target="Cell"
154
+ renderItem={renderItem}
155
+ extraData={extraData}
156
+ CellRendererComponent={CellRendererComponent}
157
+ ItemSeparatorComponent={ItemSeparatorComponent}
158
+ horizontal={horizontal}
159
+ />
160
+ );
161
+ })}
162
+ </CompatView>
163
+ );
164
+ };
@@ -0,0 +1,9 @@
1
+ import { ScrollView, Animated } from "react-native";
2
+
3
+ const AnimatedScrollView = Animated.ScrollView;
4
+
5
+ /** Regular scroll view component */
6
+ export { ScrollView as CompatScroller };
7
+
8
+ /** Animated scroll view component for smooth scrolling animations */
9
+ export { AnimatedScrollView as CompatAnimatedScroller };
@@ -0,0 +1,9 @@
1
+ import { View, Animated } from "react-native";
2
+
3
+ /** Regular view component */
4
+ export { View as CompatView };
5
+
6
+ const AnimatedView = Animated.View;
7
+
8
+ /** Animated view component for smooth animations */
9
+ export { AnimatedView as CompatAnimatedView };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * ScrollAnchor component provides a mechanism to programmatically scroll
3
+ * the list by manipulating an invisible anchor element's position.
4
+ * This helps us use ScrollView's maintainVisibleContentPosition property
5
+ * to adjust the scroll position of the list as the size of content changes without any glitches.
6
+ */
7
+
8
+ import { useImperativeHandle, useMemo, useState } from "react";
9
+ import { CompatView } from "./CompatView";
10
+ import React from "react";
11
+
12
+ /**
13
+ * Props for the ScrollAnchor component
14
+ */
15
+ export interface ScrollAnchorProps {
16
+ /** Ref to access scroll anchor methods */
17
+ scrollAnchorRef: React.Ref<ScrollAnchorRef>;
18
+ }
19
+
20
+ /**
21
+ * Ref interface for ScrollAnchor component
22
+ */
23
+ export interface ScrollAnchorRef {
24
+ /** Scrolls the list by the specified offset */
25
+ scrollBy: (offset: number) => void;
26
+ }
27
+
28
+ /**
29
+ * ScrollAnchor component that provides programmatic scrolling capabilities using maintainVisibleContentPosition property
30
+ * @param props - Component props
31
+ * @returns An invisible anchor element used for scrolling
32
+ */
33
+ export function ScrollAnchor({ scrollAnchorRef }: ScrollAnchorProps) {
34
+ const [scrollOffset, setScrollOffset] = useState(1000000); //TODO: Fix this value
35
+
36
+ // Expose scrollBy method through ref
37
+ useImperativeHandle(scrollAnchorRef, () => ({
38
+ scrollBy: (offset: number) => {
39
+ setScrollOffset((prev) => prev + offset);
40
+ },
41
+ }));
42
+
43
+ // Create an invisible anchor element that can be positioned
44
+ const anchor = useMemo(() => {
45
+ return (
46
+ <CompatView
47
+ style={{ position: "absolute", height: 0, top: scrollOffset }}
48
+ />
49
+ );
50
+ }, [scrollOffset]);
51
+
52
+ return anchor;
53
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * StickyHeaders component manages the sticky header behavior in a FlashList.
3
+ * It handles the animation and positioning of headers that should remain fixed
4
+ * at the top of the list while scrolling.
5
+ */
6
+
7
+ import React, {
8
+ useRef,
9
+ useState,
10
+ useMemo,
11
+ useImperativeHandle,
12
+ useCallback,
13
+ useEffect,
14
+ } from "react";
15
+ import { Animated, NativeScrollEvent } from "react-native";
16
+ import { FlashListProps } from "../..";
17
+ import { CompatAnimatedView } from "./CompatView";
18
+ import { RecyclerViewManager } from "../RecyclerViewManager";
19
+ import { ViewHolder } from "../ViewHolder";
20
+
21
+ /**
22
+ * Props for the StickyHeaders component
23
+ * @template TItem - The type of items in the list
24
+ */
25
+ export interface StickyHeaderProps<TItem> {
26
+ /** Array of indices that should have sticky headers */
27
+ stickyHeaderIndices: number[];
28
+ /** The data array being rendered */
29
+ data: readonly TItem[];
30
+ /** Animated value tracking scroll position */
31
+ scrollY: Animated.Value;
32
+ /** Function to render each item */
33
+ renderItem: FlashListProps<TItem>["renderItem"];
34
+ /** Ref to access sticky header methods */
35
+ stickyHeaderRef: React.RefObject<StickyHeaderRef>;
36
+ /** Manager for recycler view operations */
37
+ recyclerViewManager: RecyclerViewManager<TItem>;
38
+ /** Additional data to trigger re-renders */
39
+ extraData: FlashListProps<TItem>["extraData"];
40
+ }
41
+
42
+ /**
43
+ * Ref interface for StickyHeaders component
44
+ */
45
+ export interface StickyHeaderRef {
46
+ /** Reports scroll events to update sticky header positions */
47
+ reportScrollEvent: (event: NativeScrollEvent) => void;
48
+ }
49
+
50
+ export const StickyHeaders = <TItem,>({
51
+ stickyHeaderIndices,
52
+ renderItem,
53
+ stickyHeaderRef,
54
+ recyclerViewManager,
55
+ scrollY,
56
+ data,
57
+ extraData,
58
+ }: StickyHeaderProps<TItem>) => {
59
+ const [stickyIndices, setStickyIndices] = useState<{
60
+ currentStickyIndex: number;
61
+ nextStickyIndex: number;
62
+ }>({ currentStickyIndex: -1, nextStickyIndex: -1 });
63
+
64
+ const { currentStickyIndex, nextStickyIndex } = stickyIndices;
65
+ const hasLayout = recyclerViewManager.hasLayout();
66
+
67
+ // Memoize sorted indices based on their Y positions
68
+ const sortedIndices = useMemo(() => {
69
+ if (!hasLayout) {
70
+ return [];
71
+ }
72
+ return stickyHeaderIndices.sort((a, b) => a - b);
73
+ }, [stickyHeaderIndices, recyclerViewManager, hasLayout]);
74
+
75
+ const compute = useCallback(() => {
76
+ const adjustedValue = recyclerViewManager.getLastScrollOffset();
77
+
78
+ // Binary search for current sticky index
79
+ const currentIndexInArray = findCurrentStickyIndex(
80
+ sortedIndices,
81
+ adjustedValue,
82
+ (index) => recyclerViewManager.getLayout(index).y
83
+ );
84
+
85
+ const newStickyIndex = sortedIndices[currentIndexInArray] ?? -1;
86
+ let newNextStickyIndex = sortedIndices[currentIndexInArray + 1] ?? -1;
87
+
88
+ if (newNextStickyIndex > recyclerViewManager.getEngagedIndices().endIndex) {
89
+ newNextStickyIndex = -1;
90
+ }
91
+
92
+ if (
93
+ newStickyIndex !== currentStickyIndex ||
94
+ newNextStickyIndex !== nextStickyIndex
95
+ ) {
96
+ setStickyIndices({
97
+ currentStickyIndex: newStickyIndex,
98
+ nextStickyIndex: newNextStickyIndex,
99
+ });
100
+ }
101
+ }, [currentStickyIndex, nextStickyIndex, recyclerViewManager, sortedIndices]);
102
+
103
+ useEffect(() => {
104
+ compute();
105
+ }, [compute]);
106
+
107
+ // Optimized scroll handler using binary search pattern
108
+ useImperativeHandle(
109
+ stickyHeaderRef,
110
+ () => ({
111
+ reportScrollEvent: () => {
112
+ compute();
113
+ },
114
+ }),
115
+ [
116
+ stickyHeaderIndices,
117
+ recyclerViewManager,
118
+ currentStickyIndex,
119
+ nextStickyIndex,
120
+ ]
121
+ );
122
+
123
+ const refHolder = useRef(new Map()).current;
124
+
125
+ // Memoize translateY calculation
126
+ const translateY = useMemo(() => {
127
+ if (currentStickyIndex === -1 || nextStickyIndex === -1) {
128
+ return scrollY.interpolate({
129
+ inputRange: [0, Infinity],
130
+ outputRange: [0, 0],
131
+ extrapolate: "clamp",
132
+ });
133
+ }
134
+
135
+ const currentLayout = recyclerViewManager.getLayout(currentStickyIndex);
136
+ const nextLayout = recyclerViewManager.getLayout(nextStickyIndex);
137
+
138
+ const pushStartsAt = nextLayout.y - currentLayout.height;
139
+
140
+ return scrollY.interpolate({
141
+ inputRange: [
142
+ pushStartsAt + recyclerViewManager.firstItemOffset,
143
+ nextLayout.y + recyclerViewManager.firstItemOffset,
144
+ ],
145
+ outputRange: [0, -currentLayout.height],
146
+ extrapolate: "clamp",
147
+ });
148
+ }, [currentStickyIndex, nextStickyIndex, recyclerViewManager, scrollY]);
149
+
150
+ // Memoize header content
151
+ const headerContent = useMemo(() => {
152
+ if (currentStickyIndex === -1) return null;
153
+
154
+ return (
155
+ <CompatAnimatedView
156
+ style={{
157
+ position: "absolute",
158
+ top: 0,
159
+ left: 0,
160
+ right: 0,
161
+ transform: [{ translateY }],
162
+ }}
163
+ >
164
+ <ViewHolder
165
+ index={currentStickyIndex}
166
+ item={data[currentStickyIndex]}
167
+ renderItem={renderItem}
168
+ layout={{ x: 0, y: 0, width: 0, height: 0 }}
169
+ refHolder={refHolder}
170
+ extraData={extraData}
171
+ trailingItem={null}
172
+ target="StickyHeader"
173
+ />
174
+ </CompatAnimatedView>
175
+ );
176
+ }, [currentStickyIndex, data, renderItem, extraData, refHolder, translateY]);
177
+
178
+ return headerContent;
179
+ };
180
+
181
+ /**
182
+ * Binary search utility to find the current sticky header index based on scroll position
183
+ * @param sortedIndices - Array of indices sorted by Y position
184
+ * @param adjustedValue - Current scroll position
185
+ * @param getY - Function to get Y position for an index
186
+ * @returns Index of the current sticky header
187
+ */
188
+ function findCurrentStickyIndex(
189
+ sortedIndices: number[],
190
+ adjustedValue: number,
191
+ getY: (index: number) => number
192
+ ): number {
193
+ let left = 0;
194
+ let right = sortedIndices.length - 1;
195
+ let result = -1;
196
+
197
+ while (left <= right) {
198
+ const mid = Math.floor((left + right) / 2);
199
+ const currentY = getY(sortedIndices[mid]);
200
+
201
+ if (currentY <= adjustedValue) {
202
+ result = mid;
203
+ left = mid + 1;
204
+ } else {
205
+ right = mid - 1;
206
+ }
207
+ }
208
+
209
+ return result;
210
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * A simple wrapper for consecutive integer arrays
3
+ * Only stores start and end indices for faster computation as numbers are consecutive.
4
+ */
5
+ export class ConsecutiveNumbers {
6
+ constructor(
7
+ public readonly startIndex: number,
8
+ public readonly endIndex: number
9
+ ) {}
10
+
11
+ static readonly EMPTY = new ConsecutiveNumbers(0, -1);
12
+
13
+ /**
14
+ * Get the length of the array
15
+ */
16
+ get length(): number {
17
+ return Math.max(0, this.endIndex - this.startIndex + 1);
18
+ }
19
+
20
+ /**
21
+ * Get element at specified index
22
+ */
23
+ at(index: number): number {
24
+ return this.startIndex + index;
25
+ }
26
+
27
+ /**
28
+ * Check if two consecutive numbers are equal
29
+ */
30
+ equals(other: ConsecutiveNumbers): boolean {
31
+ return (
32
+ this.startIndex === other.startIndex && this.endIndex === other.endIndex
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Converts the consecutive range to an actual array
38
+ * @returns An array containing all consecutive numbers
39
+ */
40
+ toArray(): number[] {
41
+ if (this.length === 0) {
42
+ return [];
43
+ }
44
+ const array = new Array(this.length);
45
+ for (let i = 0; i < this.length; i++) {
46
+ array[i] = this.startIndex + i;
47
+ }
48
+ return array;
49
+ }
50
+
51
+ /**
52
+ * Check if array includes a value
53
+ */
54
+ includes(value: number): boolean {
55
+ return value >= this.startIndex && value <= this.endIndex;
56
+ }
57
+
58
+ /**
59
+ * Get index of a value in the consecutive range
60
+ */
61
+ indexOf(value: number): number {
62
+ return this.includes(value) ? value - this.startIndex : -1;
63
+ }
64
+
65
+ findValue(
66
+ predicate: (
67
+ value: number,
68
+ index: number,
69
+ array: ConsecutiveNumbers
70
+ ) => boolean | undefined
71
+ ): number | undefined {
72
+ for (let i = 0; i < this.length; i++) {
73
+ const value = this.startIndex + i;
74
+ if (predicate(value, i, this)) {
75
+ return value;
76
+ }
77
+ }
78
+ return undefined;
79
+ }
80
+
81
+ /**
82
+ * Tests whether all elements in the consecutive range pass the provided test function
83
+ * @param predicate A function that tests each element
84
+ * @returns true if all elements pass the test; otherwise, false
85
+ */
86
+ every(
87
+ predicate: (
88
+ value: number,
89
+ index: number,
90
+ array: ConsecutiveNumbers
91
+ ) => boolean | undefined
92
+ ): boolean {
93
+ for (let i = 0; i < this.length; i++) {
94
+ const value = this.startIndex + i;
95
+ if (!predicate(value, i, this)) {
96
+ return false;
97
+ }
98
+ }
99
+ return true;
100
+ }
101
+
102
+ /**
103
+ * Get a slice of the consecutive array
104
+ */
105
+ slice(start = 0, end = this.length): ConsecutiveNumbers {
106
+ const newStart = this.startIndex + start;
107
+ const newEnd = this.startIndex + Math.min(end, this.length) - 1;
108
+
109
+ return new ConsecutiveNumbers(newStart, Math.max(newStart - 1, newEnd));
110
+ }
111
+
112
+ /**
113
+ * Implement iterator to enable for...of
114
+ */
115
+ *[Symbol.iterator]() {
116
+ for (let i = this.startIndex; i <= this.endIndex; i++) {
117
+ yield i;
118
+ }
119
+ }
120
+ }