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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +37 -97
  2. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/BlankAreaEvent.kt +2 -2
  3. package/dist/AnimatedFlashList.d.ts.map +1 -1
  4. package/dist/AnimatedFlashList.js +3 -3
  5. package/dist/AnimatedFlashList.js.map +1 -1
  6. package/dist/FlashList.d.ts +9 -0
  7. package/dist/FlashList.d.ts.map +1 -1
  8. package/dist/FlashList.js +20 -0
  9. package/dist/FlashList.js.map +1 -1
  10. package/dist/FlashListProps.d.ts +30 -10
  11. package/dist/FlashListProps.d.ts.map +1 -1
  12. package/dist/FlashListProps.js.map +1 -1
  13. package/dist/FlashListRef.d.ts +305 -0
  14. package/dist/FlashListRef.d.ts.map +1 -0
  15. package/dist/FlashListRef.js +3 -0
  16. package/dist/FlashListRef.js.map +1 -0
  17. package/dist/MasonryFlashList.js.map +1 -1
  18. package/dist/__tests__/RecyclerView.test.js +72 -28
  19. package/dist/__tests__/RecyclerView.test.js.map +1 -1
  20. package/dist/__tests__/RenderStackManager.test.d.ts +2 -0
  21. package/dist/__tests__/RenderStackManager.test.d.ts.map +1 -0
  22. package/dist/__tests__/RenderStackManager.test.js +485 -0
  23. package/dist/__tests__/RenderStackManager.test.js.map +1 -0
  24. package/dist/__tests__/helpers/createLayoutManager.d.ts.map +1 -1
  25. package/dist/__tests__/helpers/createLayoutManager.js +3 -4
  26. package/dist/__tests__/helpers/createLayoutManager.js.map +1 -1
  27. package/dist/__tests__/useUnmountAwareCallbacks.test.js +1 -1
  28. package/dist/__tests__/useUnmountAwareCallbacks.test.js.map +1 -1
  29. package/dist/benchmark/useBenchmark.js +0 -25
  30. package/dist/benchmark/useBenchmark.js.map +1 -1
  31. package/dist/benchmark/useFlatListBenchmark.js +8 -7
  32. package/dist/benchmark/useFlatListBenchmark.js.map +1 -1
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/native/config/PlatformHelper.android.d.ts +1 -0
  38. package/dist/native/config/PlatformHelper.android.d.ts.map +1 -1
  39. package/dist/native/config/PlatformHelper.android.js +1 -0
  40. package/dist/native/config/PlatformHelper.android.js.map +1 -1
  41. package/dist/native/config/PlatformHelper.d.ts +1 -0
  42. package/dist/native/config/PlatformHelper.d.ts.map +1 -1
  43. package/dist/native/config/PlatformHelper.ios.d.ts +1 -0
  44. package/dist/native/config/PlatformHelper.ios.d.ts.map +1 -1
  45. package/dist/native/config/PlatformHelper.ios.js +1 -0
  46. package/dist/native/config/PlatformHelper.ios.js.map +1 -1
  47. package/dist/native/config/PlatformHelper.js +1 -0
  48. package/dist/native/config/PlatformHelper.js.map +1 -1
  49. package/dist/native/config/PlatformHelper.web.d.ts +1 -0
  50. package/dist/native/config/PlatformHelper.web.d.ts.map +1 -1
  51. package/dist/native/config/PlatformHelper.web.js +1 -0
  52. package/dist/native/config/PlatformHelper.web.js.map +1 -1
  53. package/dist/recyclerview/RecyclerView.d.ts +2 -1
  54. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  55. package/dist/recyclerview/RecyclerView.js +104 -57
  56. package/dist/recyclerview/RecyclerView.js.map +1 -1
  57. package/dist/recyclerview/RecyclerViewContextProvider.d.ts +41 -6
  58. package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -1
  59. package/dist/recyclerview/RecyclerViewContextProvider.js +4 -0
  60. package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
  61. package/dist/recyclerview/RecyclerViewManager.d.ts +24 -7
  62. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  63. package/dist/recyclerview/RecyclerViewManager.js +119 -113
  64. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  65. package/dist/recyclerview/RenderStackManager.d.ts +86 -0
  66. package/dist/recyclerview/RenderStackManager.d.ts.map +1 -0
  67. package/dist/recyclerview/RenderStackManager.js +343 -0
  68. package/dist/recyclerview/RenderStackManager.js.map +1 -0
  69. package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
  70. package/dist/recyclerview/ViewHolder.js +5 -3
  71. package/dist/recyclerview/ViewHolder.js.map +1 -1
  72. package/dist/recyclerview/ViewHolderCollection.d.ts +9 -3
  73. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
  74. package/dist/recyclerview/ViewHolderCollection.js +26 -9
  75. package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
  76. package/dist/recyclerview/components/ScrollAnchor.d.ts +2 -2
  77. package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
  78. package/dist/recyclerview/components/ScrollAnchor.js +9 -5
  79. package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
  80. package/dist/recyclerview/components/StickyHeaders.d.ts +1 -1
  81. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
  82. package/dist/recyclerview/components/StickyHeaders.js +40 -33
  83. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  84. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +45 -1
  85. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -1
  86. package/dist/recyclerview/helpers/EngagedIndicesTracker.js +77 -20
  87. package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -1
  88. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +11 -0
  89. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -0
  90. package/dist/recyclerview/helpers/RenderTimeTracker.js +42 -0
  91. package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -0
  92. package/dist/recyclerview/helpers/VelocityTracker.d.ts +29 -0
  93. package/dist/recyclerview/helpers/VelocityTracker.d.ts.map +1 -0
  94. package/dist/recyclerview/helpers/VelocityTracker.js +70 -0
  95. package/dist/recyclerview/helpers/VelocityTracker.js.map +1 -0
  96. package/dist/recyclerview/hooks/useBoundDetection.d.ts +1 -2
  97. package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
  98. package/dist/recyclerview/hooks/useBoundDetection.js +56 -22
  99. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
  100. package/dist/recyclerview/hooks/useLayoutState.d.ts +3 -1
  101. package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -1
  102. package/dist/recyclerview/hooks/useLayoutState.js +5 -3
  103. package/dist/recyclerview/hooks/useLayoutState.js.map +1 -1
  104. package/dist/recyclerview/hooks/useMappingHelper.d.ts +1 -1
  105. package/dist/recyclerview/hooks/useMappingHelper.d.ts.map +1 -1
  106. package/dist/recyclerview/hooks/useMappingHelper.js +1 -1
  107. package/dist/recyclerview/hooks/useMappingHelper.js.map +1 -1
  108. package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -1
  109. package/dist/recyclerview/hooks/useOnLoad.js +4 -6
  110. package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
  111. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +5 -49
  112. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  113. package/dist/recyclerview/hooks/useRecyclerViewController.js +315 -204
  114. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  115. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +2 -0
  116. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
  117. package/dist/recyclerview/hooks/useRecyclerViewManager.js +11 -1
  118. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
  119. package/dist/recyclerview/hooks/useRecyclingState.d.ts +4 -2
  120. package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -1
  121. package/dist/recyclerview/hooks/useRecyclingState.js +2 -2
  122. package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -1
  123. package/dist/recyclerview/hooks/useSecondaryProps.js +1 -1
  124. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts +10 -3
  125. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts.map +1 -1
  126. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +33 -4
  127. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
  128. package/dist/recyclerview/hooks/useUnmountFlag.d.ts.map +1 -1
  129. package/dist/recyclerview/hooks/useUnmountFlag.js +1 -0
  130. package/dist/recyclerview/hooks/useUnmountFlag.js.map +1 -1
  131. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +18 -4
  132. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  133. package/dist/recyclerview/layout-managers/GridLayoutManager.js +60 -21
  134. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  135. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +35 -21
  136. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
  137. package/dist/recyclerview/layout-managers/LayoutManager.js +92 -28
  138. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  139. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +9 -1
  140. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -1
  141. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +28 -12
  142. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
  143. package/dist/recyclerview/utils/measureLayout.web.d.ts.map +1 -1
  144. package/dist/recyclerview/utils/measureLayout.web.js +1 -3
  145. package/dist/recyclerview/utils/measureLayout.web.js.map +1 -1
  146. package/dist/tsconfig.tsbuildinfo +1 -1
  147. package/dist/viewability/ViewToken.d.ts +2 -2
  148. package/dist/viewability/ViewToken.d.ts.map +1 -1
  149. package/dist/viewability/ViewabilityHelper.js +1 -1
  150. package/dist/viewability/ViewabilityHelper.js.map +1 -1
  151. package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
  152. package/dist/viewability/ViewabilityManager.js +11 -5
  153. package/dist/viewability/ViewabilityManager.js.map +1 -1
  154. package/jestSetup.js +30 -11
  155. package/package.json +2 -1
  156. package/src/AnimatedFlashList.ts +3 -2
  157. package/src/FlashList.tsx +24 -0
  158. package/src/FlashListProps.ts +41 -10
  159. package/src/FlashListRef.ts +320 -0
  160. package/src/MasonryFlashList.tsx +2 -2
  161. package/src/__tests__/RecyclerView.test.tsx +106 -31
  162. package/src/__tests__/RenderStackManager.test.ts +574 -0
  163. package/src/__tests__/helpers/createLayoutManager.ts +2 -3
  164. package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
  165. package/src/benchmark/useBenchmark.ts +0 -37
  166. package/src/benchmark/useFlatListBenchmark.ts +2 -2
  167. package/src/index.ts +2 -1
  168. package/src/native/config/PlatformHelper.android.ts +1 -0
  169. package/src/native/config/PlatformHelper.ios.ts +1 -0
  170. package/src/native/config/PlatformHelper.ts +1 -0
  171. package/src/native/config/PlatformHelper.web.ts +1 -0
  172. package/src/recyclerview/RecyclerView.tsx +139 -75
  173. package/src/recyclerview/RecyclerViewContextProvider.ts +52 -7
  174. package/src/recyclerview/RecyclerViewManager.ts +135 -98
  175. package/src/recyclerview/RenderStackManager.ts +317 -0
  176. package/src/recyclerview/ViewHolder.tsx +5 -3
  177. package/src/recyclerview/ViewHolderCollection.tsx +42 -14
  178. package/src/recyclerview/components/ScrollAnchor.tsx +21 -9
  179. package/src/recyclerview/components/StickyHeaders.tsx +63 -45
  180. package/src/recyclerview/helpers/EngagedIndicesTracker.ts +118 -23
  181. package/src/recyclerview/helpers/RenderTimeTracker.ts +42 -0
  182. package/src/recyclerview/helpers/VelocityTracker.ts +77 -0
  183. package/src/recyclerview/hooks/useBoundDetection.ts +72 -23
  184. package/src/recyclerview/hooks/useLayoutState.ts +15 -6
  185. package/src/recyclerview/hooks/useMappingHelper.ts +1 -1
  186. package/src/recyclerview/hooks/useOnLoad.ts +4 -6
  187. package/src/recyclerview/hooks/useRecyclerViewController.tsx +364 -254
  188. package/src/recyclerview/hooks/useRecyclerViewManager.ts +13 -1
  189. package/src/recyclerview/hooks/useRecyclingState.ts +11 -7
  190. package/src/recyclerview/hooks/useSecondaryProps.tsx +1 -1
  191. package/src/recyclerview/hooks/useUnmountAwareCallbacks.ts +39 -3
  192. package/src/recyclerview/hooks/useUnmountFlag.ts +1 -0
  193. package/src/recyclerview/layout-managers/GridLayoutManager.ts +67 -23
  194. package/src/recyclerview/layout-managers/LayoutManager.ts +110 -41
  195. package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +30 -8
  196. package/src/recyclerview/utils/measureLayout.web.ts +1 -3
  197. package/src/viewability/ViewToken.ts +2 -2
  198. package/src/viewability/ViewabilityHelper.ts +1 -1
  199. package/src/viewability/ViewabilityManager.ts +16 -9
  200. package/dist/__tests__/RecycleKeyManager.test.d.ts +0 -2
  201. package/dist/__tests__/RecycleKeyManager.test.d.ts.map +0 -1
  202. package/dist/__tests__/RecycleKeyManager.test.js +0 -210
  203. package/dist/__tests__/RecycleKeyManager.test.js.map +0 -1
  204. package/dist/recyclerview/RecycleKeyManager.d.ts +0 -82
  205. package/dist/recyclerview/RecycleKeyManager.d.ts.map +0 -1
  206. package/dist/recyclerview/RecycleKeyManager.js +0 -135
  207. package/dist/recyclerview/RecycleKeyManager.js.map +0 -1
  208. package/src/__tests__/RecycleKeyManager.test.ts +0 -254
  209. package/src/recyclerview/RecycleKeyManager.ts +0 -185
@@ -2,15 +2,20 @@ import { useEffect, useMemo, useState } from "react";
2
2
 
3
3
  import { RecyclerViewProps } from "../RecyclerViewProps";
4
4
  import { RecyclerViewManager } from "../RecyclerViewManager";
5
+ import { VelocityTracker } from "../helpers/VelocityTracker";
5
6
 
6
7
  export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
7
8
  const [recyclerViewManager] = useState<RecyclerViewManager<T>>(
8
9
  () => new RecyclerViewManager(props)
9
10
  );
11
+ const [velocityTracker] = useState(() => new VelocityTracker());
12
+
10
13
  const { data } = props;
11
14
 
12
15
  useMemo(() => {
13
16
  recyclerViewManager.updateProps(props);
17
+ // used to update props so rule can be disabled
18
+ // eslint-disable-next-line react-hooks/exhaustive-deps
14
19
  }, [props]);
15
20
 
16
21
  /**
@@ -18,13 +23,20 @@ export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
18
23
  */
19
24
  useMemo(() => {
20
25
  recyclerViewManager.processDataUpdate();
26
+ // used to process data update so rule can be disabled
27
+ // eslint-disable-next-line react-hooks/exhaustive-deps
21
28
  }, [data]);
22
29
 
23
30
  useEffect(() => {
31
+ recyclerViewManager.restoreIfNeeded();
32
+
24
33
  return () => {
25
34
  recyclerViewManager.dispose();
35
+ velocityTracker.cleanUp();
26
36
  };
37
+ // Used to perform cleanup on unmount
38
+ // eslint-disable-next-line react-hooks/exhaustive-deps
27
39
  }, []);
28
40
 
29
- return { recyclerViewManager };
41
+ return { recyclerViewManager, velocityTracker };
30
42
  };
@@ -1,6 +1,10 @@
1
- import { Dispatch, SetStateAction, useCallback, useMemo, useRef } from "react";
1
+ import { useCallback, useMemo, useRef } from "react";
2
2
 
3
- import { useLayoutState } from "./useLayoutState";
3
+ import { LayoutStateSetter, useLayoutState } from "./useLayoutState";
4
+
5
+ export type RecyclingStateSetter<T> = LayoutStateSetter<T>;
6
+
7
+ export type RecyclingStateInitialValue<T> = T | (() => T);
4
8
 
5
9
  /**
6
10
  * A custom hook that provides state management with automatic reset functionality.
@@ -16,10 +20,10 @@ import { useLayoutState } from "./useLayoutState";
16
20
  * - A setState function that works like useState's setState
17
21
  */
18
22
  export function useRecyclingState<T>(
19
- initialState: T | (() => T),
23
+ initialState: RecyclingStateInitialValue<T>,
20
24
  deps: React.DependencyList,
21
25
  onReset?: () => void
22
- ): [T, Dispatch<SetStateAction<T>>] {
26
+ ): [T, RecyclingStateSetter<T>] {
23
27
  // Store the current state value in a ref to persist between renders
24
28
  const valueStore = useRef<T>();
25
29
  // Use layoutState to trigger re-renders when state changes
@@ -42,8 +46,8 @@ export function useRecyclingState<T>(
42
46
  * Proxy setState function that updates the stored value and triggers a re-render.
43
47
  * Only triggers a re-render if the new value is different from the current value.
44
48
  */
45
- const setStateProxy = useCallback(
46
- (newValue: T | ((prevValue: T) => T)) => {
49
+ const setStateProxy: RecyclingStateSetter<T> = useCallback(
50
+ (newValue, skipParentLayout) => {
47
51
  // Calculate next state value from function or direct value
48
52
  const nextState =
49
53
  typeof newValue === "function"
@@ -53,7 +57,7 @@ export function useRecyclingState<T>(
53
57
  // Only update and trigger re-render if value has changed
54
58
  if (nextState !== valueStore.current) {
55
59
  valueStore.current = nextState;
56
- setCounter((prev) => prev + 1);
60
+ setCounter((prev) => prev + 1, skipParentLayout);
57
61
  }
58
62
  },
59
63
  [setCounter]
@@ -105,7 +105,7 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
105
105
  const ForwardedScrollComponent = React.forwardRef((_props, ref) =>
106
106
  (renderScrollComponent as any)({ ..._props, ref } as any)
107
107
  );
108
- ForwardedScrollComponent.displayName = "CompatScrollView";
108
+ ForwardedScrollComponent.displayName = "CustomScrollView";
109
109
  scrollComponent = ForwardedScrollComponent as any;
110
110
  } else if (renderScrollComponent) {
111
111
  scrollComponent = renderScrollComponent;
@@ -1,10 +1,10 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
2
 
3
3
  /**
4
- * Hook that provides callbacks which are aware of component unmount state.
5
- * Any timeouts created with these callbacks will be automatically cleared when the component unmounts.
4
+ * Hook that provides a setTimeout which is aware of component unmount state.
5
+ * Any timeouts created with this hook will be automatically cleared when the component unmounts.
6
6
  */
7
- export function useUnmountAwareCallbacks() {
7
+ export function useUnmountAwareTimeout() {
8
8
  // Store active timeout IDs in a Set for more efficient add/remove operations
9
9
  const [timeoutIds] = useState<Set<NodeJS.Timeout>>(() => new Set());
10
10
 
@@ -35,3 +35,39 @@ export function useUnmountAwareCallbacks() {
35
35
  setTimeout,
36
36
  };
37
37
  }
38
+
39
+ /**
40
+ * Hook that provides a requestAnimationFrame which is aware of component unmount state.
41
+ * Any animation frames requested with this hook will be automatically canceled when the component unmounts.
42
+ */
43
+ export function useUnmountAwareAnimationFrame() {
44
+ // Store active animation frame request IDs in a Set for more efficient add/remove operations
45
+ const [requestIds] = useState<Set<number>>(() => new Set());
46
+
47
+ // Cancel all animation frame requests on unmount
48
+ useEffect(() => {
49
+ return () => {
50
+ requestIds.forEach((id) => cancelAnimationFrame(id));
51
+ requestIds.clear();
52
+ };
53
+ }, [requestIds]);
54
+
55
+ // Create a safe requestAnimationFrame that will be canceled on unmount
56
+ const requestAnimationFrame = useCallback(
57
+ (callback: FrameRequestCallback): void => {
58
+ const id = global.requestAnimationFrame((timestamp) => {
59
+ // Remove this request ID from the tracking set
60
+ requestIds.delete(id);
61
+ callback(timestamp);
62
+ });
63
+
64
+ // Track this request ID
65
+ requestIds.add(id);
66
+ },
67
+ [requestIds]
68
+ );
69
+
70
+ return {
71
+ requestAnimationFrame,
72
+ };
73
+ }
@@ -16,6 +16,7 @@ export const useUnmountFlag = () => {
16
16
  // Use layoutEffect to set up cleanup on unmount
17
17
  // This ensures the flag is set before any other cleanup effects run
18
18
  useLayoutEffect(() => {
19
+ isUnmounted.current = false;
19
20
  // Cleanup function that runs when the component unmounts
20
21
  return () => {
21
22
  isUnmounted.current = true;
@@ -14,6 +14,9 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
14
14
  /** The width of the bounded area for the grid */
15
15
  private boundedSize: number;
16
16
 
17
+ /** If there's a span change for grid layout, we need to recompute all the widths */
18
+ private fullRelayoutRequired = false;
19
+
17
20
  constructor(params: LayoutParams, previousLayoutManager?: RVLayoutManager) {
18
21
  super(params, previousLayoutManager);
19
22
  this.boundedSize = params.windowSize.width;
@@ -33,10 +36,7 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
33
36
  this.boundedSize = params.windowSize.width;
34
37
  if (this.layouts.length > 0) {
35
38
  // update all widths
36
- for (let i = 0; i < this.layouts.length; i++) {
37
- this.layouts[i].width = this.getWidth(i);
38
- }
39
- // console.log("-----> recomputeLayouts");
39
+ this.updateAllWidths();
40
40
 
41
41
  this.recomputeLayouts(0, this.layouts.length - 1);
42
42
  this.requiresRepaint = true;
@@ -57,6 +57,13 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
57
57
  layout.isHeightMeasured = true;
58
58
  layout.isWidthMeasured = true;
59
59
  }
60
+
61
+ // TODO: Can be optimized
62
+ if (this.fullRelayoutRequired) {
63
+ this.updateAllWidths();
64
+ this.fullRelayoutRequired = false;
65
+ return 0;
66
+ }
60
67
  }
61
68
 
62
69
  /**
@@ -72,18 +79,24 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
72
79
  layout.enforcedWidth = true;
73
80
  }
74
81
 
82
+ /**
83
+ * Handles span change for an item.
84
+ * @param index Index of the item
85
+ */
86
+ handleSpanChange(index: number) {
87
+ this.fullRelayoutRequired = true;
88
+ }
89
+
75
90
  /**
76
91
  * Returns the total size of the layout area.
77
92
  * @returns RVDimension containing width and height of the layout
78
93
  */
79
94
  getLayoutSize(): RVDimension {
80
95
  if (this.layouts.length === 0) return { width: 0, height: 0 };
81
- const lastRowTallestItem = this.processAndReturnTallestItemInRow(
82
- this.layouts.length - 1
83
- );
96
+ const totalHeight = this.computeTotalHeightTillRow(this.layouts.length - 1);
84
97
  return {
85
98
  width: this.boundedSize,
86
- height: lastRowTallestItem.y + lastRowTallestItem.height,
99
+ height: totalHeight,
87
100
  };
88
101
  }
89
102
 
@@ -93,7 +106,9 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
93
106
  * @param endIndex Ending index of items to recompute
94
107
  */
95
108
  recomputeLayouts(startIndex: number, endIndex: number): void {
96
- const newStartIndex = this.locateFirstNeighbourIndex(startIndex);
109
+ const newStartIndex = this.locateFirstIndexInRow(
110
+ Math.max(0, startIndex - 1)
111
+ );
97
112
  const startVal = this.getLayout(newStartIndex);
98
113
 
99
114
  let startX = startVal.x;
@@ -102,7 +117,7 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
102
117
  for (let i = newStartIndex; i <= endIndex; i++) {
103
118
  const layout = this.getLayout(i);
104
119
  if (!this.checkBounds(startX, layout.width)) {
105
- const tallestItem = this.processAndReturnTallestItemInRow(i);
120
+ const tallestItem = this.processAndReturnTallestItemInRow(i - 1);
106
121
  startY = tallestItem.y + tallestItem.height;
107
122
  startX = 0;
108
123
  }
@@ -111,6 +126,9 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
111
126
  layout.y = startY;
112
127
  startX += layout.width;
113
128
  }
129
+ if (endIndex === this.layouts.length - 1) {
130
+ this.processAndReturnTallestItemInRow(endIndex);
131
+ }
114
132
  }
115
133
 
116
134
  /**
@@ -119,25 +137,23 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
119
137
  * @returns Width of the item
120
138
  */
121
139
  private getWidth(index: number): number {
122
- const span = this.getSpanSizeInfo(index).span ?? 1;
123
- return (this.boundedSize / this.maxColumns) * span;
140
+ return (this.boundedSize / this.maxColumns) * this.getSpan(index);
124
141
  }
125
142
 
126
143
  /**
127
144
  * Processes items in a row and returns the tallest item.
128
145
  * Also handles height normalization for items in the same row.
129
146
  * Tallest item per row helps in forcing tallest items height on neighbouring items.
130
- * @param index Index of the last item in the row
147
+ * @param endIndex Index of the last item in the row
131
148
  * @returns The tallest item in the row
132
149
  */
133
- private processAndReturnTallestItemInRow(index: number): RVLayout {
134
- const startIndex = this.locateFirstNeighbourIndex(index);
135
- const y = this.layouts[startIndex].y;
150
+ private processAndReturnTallestItemInRow(endIndex: number): RVLayout {
151
+ const startIndex = this.locateFirstIndexInRow(endIndex);
136
152
  let tallestItem: RVLayout | undefined;
137
153
  let maxHeight = 0;
138
154
  let i = startIndex;
139
155
  let isMeasured = false;
140
- while (Math.ceil(this.layouts[i].y) === Math.ceil(y)) {
156
+ while (i <= endIndex) {
141
157
  const layout = this.layouts[i];
142
158
  isMeasured = isMeasured || Boolean(layout.isHeightMeasured);
143
159
  maxHeight = Math.max(maxHeight, layout.height);
@@ -153,7 +169,9 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
153
169
  break;
154
170
  }
155
171
  }
156
-
172
+ if (!tallestItem && maxHeight > 0) {
173
+ maxHeight = Number.MAX_SAFE_INTEGER;
174
+ }
157
175
  tallestItem = tallestItem ?? this.layouts[startIndex];
158
176
 
159
177
  if (!isMeasured) {
@@ -167,7 +185,7 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
167
185
  this.requiresRepaint = true;
168
186
  }
169
187
  i = startIndex;
170
- while (Math.ceil(this.layouts[i].y) === Math.ceil(y)) {
188
+ while (i <= endIndex) {
171
189
  this.layouts[i].minHeight = targetHeight;
172
190
  if (targetHeight > 0) {
173
191
  this.layouts[i].height = targetHeight;
@@ -182,6 +200,32 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
182
200
  return tallestItem;
183
201
  }
184
202
 
203
+ /**
204
+ * Computes the total height of the layout.
205
+ * @param endIndex Index of the last item in the row
206
+ * @returns Total height of the layout
207
+ */
208
+ private computeTotalHeightTillRow(endIndex: number): number {
209
+ const startIndex = this.locateFirstIndexInRow(endIndex);
210
+ const y = this.layouts[startIndex].y;
211
+ let maxHeight = 0;
212
+ let i = startIndex;
213
+ while (i <= endIndex) {
214
+ maxHeight = Math.max(maxHeight, this.layouts[i].height);
215
+ i++;
216
+ if (i >= this.layouts.length) {
217
+ break;
218
+ }
219
+ }
220
+ return y + maxHeight;
221
+ }
222
+
223
+ private updateAllWidths() {
224
+ for (let i = 0; i < this.layouts.length; i++) {
225
+ this.layouts[i].width = this.getWidth(i);
226
+ }
227
+ }
228
+
185
229
  /**
186
230
  * Checks if an item can fit within the bounded width.
187
231
  * @param itemX Starting X position of the item
@@ -194,14 +238,14 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
194
238
 
195
239
  /**
196
240
  * Locates the index of the first item in the current row.
197
- * @param startIndex Index to start searching from
241
+ * @param itemIndex Index to start searching from
198
242
  * @returns Index of the first item in the row
199
243
  */
200
- private locateFirstNeighbourIndex(startIndex: number): number {
201
- if (startIndex === 0) {
244
+ private locateFirstIndexInRow(itemIndex: number): number {
245
+ if (itemIndex === 0) {
202
246
  return 0;
203
247
  }
204
- let i = startIndex - 1;
248
+ let i = itemIndex;
205
249
  for (; i >= 0; i--) {
206
250
  if (this.layouts[i].x === 0) {
207
251
  break;
@@ -20,12 +20,8 @@ export abstract class RVLayoutManager {
20
20
  protected layouts: RVLayout[];
21
21
  /** Dimensions of the visible window/viewport */
22
22
  protected windowSize: RVDimension;
23
- /** Information about item spans and sizes */
24
- protected spanSizeInfo: SpanSizeInfo = {};
25
23
  /** Maximum number of columns in the layout */
26
24
  protected maxColumns: number;
27
- /** Optional callback to override default item layout */
28
- protected overrideItemLayout?: (index: number, layout: SpanSizeInfo) => void;
29
25
 
30
26
  /** Whether to optimize item placement for better space utilization */
31
27
  protected optimizeItemArrangement: boolean;
@@ -33,19 +29,34 @@ export abstract class RVLayoutManager {
33
29
  /** Flag indicating if the layout requires repainting */
34
30
  public requiresRepaint = false;
35
31
 
32
+ /** Optional callback to override default item layout */
33
+ private overrideItemLayout: (index: number, layout: SpanSizeInfo) => void;
36
34
  /** Optional function to determine item type */
37
- private _getItemType?: (index: number) => string | number;
35
+ private getItemType: (index: number) => string;
38
36
  /** Window for tracking average heights by item type */
39
37
  private heightAverageWindow: MultiTypeAverageWindow;
40
38
  /** Window for tracking average widths by item type */
41
39
  private widthAverageWindow: MultiTypeAverageWindow;
42
40
  /** Maximum number of items to process in a single layout pass */
43
- private maxItemsToProcess = 250; // TODO: make this dynamic
41
+ private maxItemsToProcess = 250;
42
+ /** Information about item spans and sizes */
43
+ private spanSizeInfo: SpanSizeInfo = {};
44
+ /** Span tracker for each item */
45
+ private spanTracker: (number | undefined)[] = [];
46
+
47
+ /** Current max index with changed layout */
48
+ private currentMaxIndexWithChangedLayout = -1;
49
+
50
+ /**
51
+ * Last index that was skipped during layout computation.
52
+ * Used to determine if a layout needs to be recomputed.
53
+ */
54
+ private lastSkippedLayoutIndex = Number.MAX_VALUE;
44
55
 
45
56
  constructor(params: LayoutParams, previousLayoutManager?: RVLayoutManager) {
46
57
  this.heightAverageWindow = new MultiTypeAverageWindow(5, 200);
47
58
  this.widthAverageWindow = new MultiTypeAverageWindow(5, 200);
48
- this._getItemType = params.getItemType;
59
+ this.getItemType = params.getItemType;
49
60
  this.overrideItemLayout = params.overrideItemLayout;
50
61
  this.layouts = previousLayoutManager?.layouts ?? [];
51
62
  if (previousLayoutManager) {
@@ -57,15 +68,6 @@ export abstract class RVLayoutManager {
57
68
  }
58
69
  }
59
70
 
60
- /**
61
- * Gets the type of an item at the given index.
62
- * @param index Index of the item
63
- * @returns Item type or "default" if not specified
64
- */
65
- private getItemType(index: number): string | number {
66
- return this._getItemType?.(index) ?? "default";
67
- }
68
-
69
71
  /**
70
72
  * Gets the estimated width for an item based on its type.
71
73
  * @param index Index of the item
@@ -114,8 +116,8 @@ export abstract class RVLayoutManager {
114
116
  /**
115
117
  * Gets indices of items currently visible in the viewport.
116
118
  * Uses binary search for efficient lookup.
117
- * @param unboundDimensionStart Start position of viewport
118
- * @param unboundDimensionEnd End position of viewport
119
+ * @param unboundDimensionStart Start position of viewport (start X or start Y)
120
+ * @param unboundDimensionEnd End position of viewport (end X or end Y)
119
121
  * @returns ConsecutiveNumbers containing visible indices
120
122
  */
121
123
  getVisibleLayouts(
@@ -157,7 +159,7 @@ export abstract class RVLayoutManager {
157
159
  }
158
160
  const startIndex = Math.min(...indices);
159
161
  // Recompute layouts starting from the smallest index in the original indices array
160
- this.recomputeLayouts(
162
+ this._recomputeLayouts(
161
163
  this.getMinRecomputeIndex(startIndex),
162
164
  this.getMaxRecomputeIndex(startIndex)
163
165
  );
@@ -169,41 +171,49 @@ export abstract class RVLayoutManager {
169
171
  * @param totalItemCount Total number of items in the list
170
172
  */
171
173
  modifyLayout(layoutInfo: RVLayoutInfo[], totalItemCount: number): void {
174
+ this.maxItemsToProcess = Math.max(
175
+ this.maxItemsToProcess,
176
+ layoutInfo.length * 10
177
+ );
172
178
  let minRecomputeIndex = Number.MAX_VALUE;
173
179
 
174
180
  if (this.layouts.length > totalItemCount) {
175
181
  this.layouts.length = totalItemCount;
182
+ this.spanTracker.length = totalItemCount;
176
183
  minRecomputeIndex = totalItemCount - 1; // <0 gets skipped so it's safe to set to totalItemCount - 1
177
184
  }
178
185
  // update average windows
179
186
  minRecomputeIndex = Math.min(
180
187
  minRecomputeIndex,
181
- this.computeEstimatesAndMinRecomputeIndex(layoutInfo)
188
+ this.computeEstimatesAndMinMaxChangedLayout(layoutInfo)
182
189
  );
183
190
 
184
191
  if (this.layouts.length < totalItemCount && totalItemCount > 0) {
185
192
  const startIndex = this.layouts.length;
186
193
  this.layouts.length = totalItemCount;
194
+ this.spanTracker.length = totalItemCount;
187
195
  for (let i = startIndex; i < totalItemCount; i++) {
188
196
  this.getLayout(i);
197
+ this.getSpan(i);
189
198
  }
190
199
  this.recomputeLayouts(startIndex, totalItemCount - 1);
191
200
  }
192
- minRecomputeIndex = Math.min(
193
- minRecomputeIndex,
194
- this.processLayoutInfo(layoutInfo, totalItemCount) ?? minRecomputeIndex
195
- );
201
+
196
202
  // compute minRecomputeIndex
203
+
197
204
  minRecomputeIndex = Math.min(
198
205
  minRecomputeIndex,
199
- this.computeEstimatesAndMinRecomputeIndex(layoutInfo)
206
+ this.lastSkippedLayoutIndex,
207
+ this.computeMinIndexWithChangedSpan(layoutInfo),
208
+ this.processLayoutInfo(layoutInfo, totalItemCount) ?? minRecomputeIndex,
209
+ this.computeEstimatesAndMinMaxChangedLayout(layoutInfo)
200
210
  );
211
+
201
212
  if (minRecomputeIndex >= 0 && minRecomputeIndex < totalItemCount) {
202
- this.recomputeLayouts(
203
- this.getMinRecomputeIndex(minRecomputeIndex),
204
- this.getMaxRecomputeIndex(minRecomputeIndex)
205
- );
213
+ const maxRecomputeIndex = this.getMaxRecomputeIndex(minRecomputeIndex);
214
+ this._recomputeLayouts(minRecomputeIndex, maxRecomputeIndex);
206
215
  }
216
+ this.currentMaxIndexWithChangedLayout = -1;
207
217
  }
208
218
 
209
219
  /**
@@ -269,16 +279,30 @@ export abstract class RVLayoutManager {
269
279
  protected abstract estimateLayout(index: number): void;
270
280
 
271
281
  /**
272
- * Gets span size information for an item, applying any overrides.
282
+ * Gets span for an item, applying any overrides.
283
+ * This is intended to be called during a relayout call. The value is tracked and used to determine if a span change has occurred.
284
+ * If skipTracking is true, the operation is not tracked. Can be useful if span is required outside of a relayout call.
285
+ * The tracker is used to call handleSpanChange if a span change has occurred before relayout call.
286
+ * // TODO: improve this contract.
273
287
  * @param index Index of the item
274
- * @returns SpanSizeInfo for the item
288
+ * @returns Span for the item
275
289
  */
276
- protected getSpanSizeInfo(index: number): SpanSizeInfo {
290
+ protected getSpan(index: number, skipTracking = false): number {
277
291
  this.spanSizeInfo.span = undefined;
278
- this.overrideItemLayout?.(index, this.spanSizeInfo);
279
- return this.spanSizeInfo;
292
+ this.overrideItemLayout(index, this.spanSizeInfo);
293
+ const span = Math.min(this.spanSizeInfo.span ?? 1, this.maxColumns);
294
+ if (!skipTracking) {
295
+ this.spanTracker[index] = span;
296
+ }
297
+ return span;
280
298
  }
281
299
 
300
+ /**
301
+ * Method to handle span change for an item. Can be overridden by subclasses.
302
+ * @param index Index of the item
303
+ */
304
+ protected handleSpanChange(index: number) {}
305
+
282
306
  /**
283
307
  * Gets the maximum index to process in a single layout pass.
284
308
  * @param startIndex Starting index
@@ -286,7 +310,8 @@ export abstract class RVLayoutManager {
286
310
  */
287
311
  private getMaxRecomputeIndex(startIndex: number): number {
288
312
  return Math.min(
289
- startIndex + this.maxItemsToProcess,
313
+ Math.max(startIndex, this.currentMaxIndexWithChangedLayout) +
314
+ this.maxItemsToProcess,
290
315
  this.layouts.length - 1
291
316
  );
292
317
  }
@@ -300,12 +325,36 @@ export abstract class RVLayoutManager {
300
325
  return startIndex;
301
326
  }
302
327
 
328
+ private _recomputeLayouts(startIndex: number, endIndex: number): void {
329
+ this.recomputeLayouts(startIndex, endIndex);
330
+ if (
331
+ this.lastSkippedLayoutIndex >= startIndex &&
332
+ this.lastSkippedLayoutIndex <= endIndex
333
+ ) {
334
+ this.lastSkippedLayoutIndex = Number.MAX_VALUE;
335
+ }
336
+
337
+ if (endIndex + 1 < this.layouts.length) {
338
+ this.lastSkippedLayoutIndex = Math.min(
339
+ endIndex + 1,
340
+ this.lastSkippedLayoutIndex
341
+ );
342
+ const lastIndex = this.layouts.length - 1;
343
+ // Since layout managers derive height from last indices we need to make
344
+ // sure they're not too much out of sync.
345
+ if (this.layouts[lastIndex].y < this.layouts[endIndex].y) {
346
+ this.recomputeLayouts(this.lastSkippedLayoutIndex, lastIndex);
347
+ this.lastSkippedLayoutIndex = Number.MAX_VALUE;
348
+ }
349
+ }
350
+ }
351
+
303
352
  /**
304
353
  * Computes size estimates and finds the minimum recompute index.
305
354
  * @param layoutInfo Array of layout information for items
306
355
  * @returns Minimum index that needs recomputation
307
356
  */
308
- private computeEstimatesAndMinRecomputeIndex(
357
+ private computeEstimatesAndMinMaxChangedLayout(
309
358
  layoutInfo: RVLayoutInfo[]
310
359
  ): number {
311
360
  let minRecomputeIndex = Number.MAX_VALUE;
@@ -313,6 +362,7 @@ export abstract class RVLayoutManager {
313
362
  const { index, dimensions } = info;
314
363
  const storedLayout = this.layouts[index];
315
364
  if (
365
+ index >= this.lastSkippedLayoutIndex ||
316
366
  !storedLayout ||
317
367
  !storedLayout.isHeightMeasured ||
318
368
  !storedLayout.isWidthMeasured ||
@@ -320,6 +370,10 @@ export abstract class RVLayoutManager {
320
370
  areDimensionsNotEqual(storedLayout.width, dimensions.width)
321
371
  ) {
322
372
  minRecomputeIndex = Math.min(minRecomputeIndex, index);
373
+ this.currentMaxIndexWithChangedLayout = Math.max(
374
+ this.currentMaxIndexWithChangedLayout,
375
+ index
376
+ );
323
377
  }
324
378
  this.heightAverageWindow.addValue(
325
379
  dimensions.height,
@@ -332,6 +386,21 @@ export abstract class RVLayoutManager {
332
386
  }
333
387
  return minRecomputeIndex;
334
388
  }
389
+
390
+ private computeMinIndexWithChangedSpan(layoutInfo: RVLayoutInfo[]): number {
391
+ let minIndexWithChangedSpan = Number.MAX_VALUE;
392
+ for (const info of layoutInfo) {
393
+ const { index } = info;
394
+ const span = this.getSpan(index, true);
395
+ const storedSpan = this.spanTracker[index];
396
+ if (span !== storedSpan) {
397
+ this.spanTracker[index] = span;
398
+ this.handleSpanChange(index);
399
+ minIndexWithChangedSpan = Math.min(minIndexWithChangedSpan, index);
400
+ }
401
+ }
402
+ return minIndexWithChangedSpan;
403
+ }
335
404
  }
336
405
 
337
406
  /**
@@ -348,31 +417,31 @@ export interface LayoutParams {
348
417
  * Determines if the list scrolls horizontally (true) or vertically (false)
349
418
  * Affects how items are positioned and which dimension is used for scrolling
350
419
  */
351
- horizontal?: boolean;
420
+ horizontal: boolean;
352
421
 
353
422
  /**
354
423
  * Maximum number of columns in a grid layout
355
424
  * Controls how many items can be placed side by side
356
425
  */
357
- maxColumns?: number;
426
+ maxColumns: number;
358
427
 
359
428
  /**
360
429
  * When true, attempts to optimize item placement for better space utilization
361
430
  * May affect the ordering of items to minimize empty space
362
431
  */
363
- optimizeItemArrangement?: boolean;
432
+ optimizeItemArrangement: boolean;
364
433
 
365
434
  /**
366
435
  * Callback to manually override layout properties for specific items
367
436
  * Allows custom control over span and size for individual items
368
437
  */
369
- overrideItemLayout?: (index: number, layout: SpanSizeInfo) => void;
438
+ overrideItemLayout: (index: number, layout: SpanSizeInfo) => void;
370
439
 
371
440
  /**
372
441
  * Function to determine the type of an item at a specific index
373
442
  * Used for size estimation and optimization based on item types
374
443
  */
375
- getItemType?: (index: number) => string | number;
444
+ getItemType: (index: number) => string;
376
445
  }
377
446
 
378
447
  /**