@shopify/flash-list 2.0.0-alpha.14 → 2.0.0-alpha.16

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 (63) hide show
  1. package/README.md +33 -97
  2. package/dist/FlashListProps.d.ts +2 -2
  3. package/dist/FlashListProps.d.ts.map +1 -1
  4. package/dist/MasonryFlashList.js.map +1 -1
  5. package/dist/__tests__/RenderStackManager.test.js +1 -2
  6. package/dist/__tests__/RenderStackManager.test.js.map +1 -1
  7. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  8. package/dist/recyclerview/RecyclerView.js +7 -1
  9. package/dist/recyclerview/RecyclerView.js.map +1 -1
  10. package/dist/recyclerview/RenderStackManager.d.ts +1 -0
  11. package/dist/recyclerview/RenderStackManager.d.ts.map +1 -1
  12. package/dist/recyclerview/RenderStackManager.js +26 -7
  13. package/dist/recyclerview/RenderStackManager.js.map +1 -1
  14. package/dist/recyclerview/components/StickyHeaders.js +1 -1
  15. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  16. package/dist/recyclerview/hooks/useLayoutState.d.ts +3 -1
  17. package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -1
  18. package/dist/recyclerview/hooks/useLayoutState.js +5 -3
  19. package/dist/recyclerview/hooks/useLayoutState.js.map +1 -1
  20. package/dist/recyclerview/hooks/useMappingHelper.d.ts +1 -1
  21. package/dist/recyclerview/hooks/useMappingHelper.d.ts.map +1 -1
  22. package/dist/recyclerview/hooks/useMappingHelper.js +1 -1
  23. package/dist/recyclerview/hooks/useMappingHelper.js.map +1 -1
  24. package/dist/recyclerview/hooks/useRecyclingState.d.ts +4 -2
  25. package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -1
  26. package/dist/recyclerview/hooks/useRecyclingState.js +2 -2
  27. package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -1
  28. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +9 -1
  29. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  30. package/dist/recyclerview/layout-managers/GridLayoutManager.js +22 -7
  31. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  32. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +26 -6
  33. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
  34. package/dist/recyclerview/layout-managers/LayoutManager.js +69 -12
  35. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  36. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +9 -1
  37. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -1
  38. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +28 -12
  39. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/dist/viewability/ViewToken.d.ts +2 -2
  42. package/dist/viewability/ViewToken.d.ts.map +1 -1
  43. package/dist/viewability/ViewabilityHelper.js +1 -1
  44. package/dist/viewability/ViewabilityHelper.js.map +1 -1
  45. package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
  46. package/dist/viewability/ViewabilityManager.js +11 -5
  47. package/dist/viewability/ViewabilityManager.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/FlashListProps.ts +4 -1
  50. package/src/MasonryFlashList.tsx +2 -2
  51. package/src/__tests__/RenderStackManager.test.ts +1 -2
  52. package/src/recyclerview/RecyclerView.tsx +13 -6
  53. package/src/recyclerview/RenderStackManager.ts +32 -6
  54. package/src/recyclerview/components/StickyHeaders.tsx +1 -1
  55. package/src/recyclerview/hooks/useLayoutState.ts +15 -6
  56. package/src/recyclerview/hooks/useMappingHelper.ts +1 -1
  57. package/src/recyclerview/hooks/useRecyclingState.ts +11 -7
  58. package/src/recyclerview/layout-managers/GridLayoutManager.ts +26 -6
  59. package/src/recyclerview/layout-managers/LayoutManager.ts +74 -15
  60. package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +30 -8
  61. package/src/viewability/ViewToken.ts +2 -2
  62. package/src/viewability/ViewabilityHelper.ts +1 -1
  63. package/src/viewability/ViewabilityManager.ts +16 -9
@@ -459,7 +459,7 @@ describe("RenderStackManager edge cases", () => {
459
459
  runSyncAndGetEntireKeyMapKeys(rsm, mock6);
460
460
  runSyncAndGetEntireKeyMapKeys(rsm, mock7, new ConsecutiveNumbers(3, 5));
461
461
  const keys = getKeysForMockItems(rsm, mock7);
462
- expect(keys).toEqual(["0", "1", "2", "3", "4", "5", "6"]);
462
+ expect(keys).toEqual(["0", "1", "2", "3", "4", "5", "6", "7"]);
463
463
  });
464
464
 
465
465
  it("should not delete keys from pool if they are not visible on index changes when going from mock3 to mock8", () => {
@@ -467,7 +467,6 @@ describe("RenderStackManager edge cases", () => {
467
467
  runSyncAndGetEntireKeyMapKeys(rsm, mock3, new ConsecutiveNumbers(0, 10));
468
468
  runSyncAndGetEntireKeyMapKeys(rsm, mock8, new ConsecutiveNumbers(0, 13));
469
469
  const keys = getKeysForMockItems(rsm, mock8);
470
- console.log("keys", keys);
471
470
  expect(keys).toEqual([
472
471
  "0",
473
472
  "1",
@@ -439,6 +439,18 @@ const RecyclerViewComponent = <T,>(
439
439
  );
440
440
  }, [horizontal, shouldRenderFromBottom, adjustmentMinHeight]);
441
441
 
442
+ const scrollAnchor = useMemo(() => {
443
+ if (shouldMaintainVisibleContentPosition) {
444
+ return (
445
+ <ScrollAnchor
446
+ horizontal={Boolean(horizontal)}
447
+ scrollAnchorRef={scrollAnchorRef}
448
+ />
449
+ );
450
+ }
451
+ return null;
452
+ }, [horizontal, shouldMaintainVisibleContentPosition]);
453
+
442
454
  // console.log("render", recyclerViewManager.getRenderStack());
443
455
 
444
456
  // Render the main RecyclerView structure
@@ -485,12 +497,7 @@ const RecyclerViewComponent = <T,>(
485
497
  {...overrideProps}
486
498
  >
487
499
  {/* Scroll anchor for maintaining content position */}
488
- {maintainVisibleContentPositionInternal && (
489
- <ScrollAnchor
490
- horizontal={Boolean(horizontal)}
491
- scrollAnchorRef={scrollAnchorRef}
492
- />
493
- )}
500
+ {scrollAnchor}
494
501
  {isHorizontalRTL && viewToMeasureBoundedSize}
495
502
  {renderHeader}
496
503
  {!isHorizontalRTL && viewToMeasureBoundedSize}
@@ -26,6 +26,8 @@ export class RenderStackManager {
26
26
  // Counter for generating unique sequential keys
27
27
  private keyCounter: number;
28
28
 
29
+ private unProcessedIndices: Set<number>;
30
+
29
31
  /**
30
32
  * @param maxItemsInRecyclePool - Maximum number of items that can be in the recycle pool
31
33
  */
@@ -35,6 +37,7 @@ export class RenderStackManager {
35
37
  this.keyMap = new Map();
36
38
  this.stableIdMap = new Map();
37
39
  this.keyCounter = 0;
40
+ this.unProcessedIndices = new Set();
38
41
  }
39
42
 
40
43
  /**
@@ -57,6 +60,7 @@ export class RenderStackManager {
57
60
  dataLength: number
58
61
  ) {
59
62
  this.clearRecyclePool();
63
+ this.unProcessedIndices.clear();
60
64
 
61
65
  // Recycle keys for items that are no longer valid or visible
62
66
  this.keyMap.forEach((keyInfo, key) => {
@@ -65,6 +69,9 @@ export class RenderStackManager {
65
69
  this.recycleKey(key);
66
70
  return;
67
71
  }
72
+ if (!this.disableRecycling) {
73
+ this.unProcessedIndices.add(index);
74
+ }
68
75
  if (!engagedIndices.includes(index)) {
69
76
  this.recycleKey(key);
70
77
  return;
@@ -114,7 +121,7 @@ export class RenderStackManager {
114
121
  }
115
122
 
116
123
  // Clean up stale items and manage the recycle pool size
117
- this.cleanup(getStableId, engagedIndices, dataLength);
124
+ this.cleanup(getStableId, getItemType, engagedIndices, dataLength);
118
125
  }
119
126
 
120
127
  /**
@@ -131,6 +138,7 @@ export class RenderStackManager {
131
138
  */
132
139
  private cleanup(
133
140
  getStableId: (index: number) => string,
141
+ getItemType: (index: number) => string,
134
142
  engagedIndices: ConsecutiveNumbers,
135
143
  dataLength: number
136
144
  ) {
@@ -139,11 +147,27 @@ export class RenderStackManager {
139
147
  // Remove items that are no longer in the dataset
140
148
  for (const [key, keyInfo] of this.keyMap.entries()) {
141
149
  const { index, itemType, stableId } = keyInfo;
142
- if (index >= dataLength || getStableId(index) !== stableId) {
143
- // TODO: Find a way to reusue the key, instead of deleting it
144
- this.deleteKeyFromRecyclePool(itemType, key);
145
- this.stableIdMap.delete(stableId);
146
- itemsToDelete.push(key);
150
+ const indexOutOfBounds = index >= dataLength;
151
+ const hasStableIdChanged =
152
+ !indexOutOfBounds && getStableId(index) !== stableId;
153
+
154
+ if (indexOutOfBounds || hasStableIdChanged) {
155
+ const nextIndex = this.unProcessedIndices.values().next().value;
156
+ let shouldDeleteKey = true;
157
+
158
+ if (nextIndex !== undefined) {
159
+ const nextItemType = getItemType(nextIndex);
160
+ const nextStableId = getStableId(nextIndex);
161
+ if (itemType === nextItemType) {
162
+ this.syncItem(nextIndex, nextItemType, nextStableId);
163
+ shouldDeleteKey = false;
164
+ }
165
+ }
166
+ if (shouldDeleteKey) {
167
+ this.deleteKeyFromRecyclePool(itemType, key);
168
+ this.stableIdMap.delete(stableId);
169
+ itemsToDelete.push(key);
170
+ }
147
171
  }
148
172
  }
149
173
 
@@ -215,6 +239,8 @@ export class RenderStackManager {
215
239
  this.getKeyFromRecyclePool(itemType) ||
216
240
  this.generateKey();
217
241
 
242
+ this.unProcessedIndices.delete(index);
243
+
218
244
  const keyInfo = this.keyMap.get(newKey);
219
245
  if (keyInfo) {
220
246
  // Update an existing key's metadata
@@ -74,7 +74,7 @@ export const StickyHeaders = <TItem,>({
74
74
 
75
75
  // sort indices and memoize compute
76
76
  const sortedIndices = useMemo(() => {
77
- return stickyHeaderIndices.sort((first, second) => first - second);
77
+ return [...stickyHeaderIndices].sort((first, second) => first - second);
78
78
  }, [stickyHeaderIndices]);
79
79
 
80
80
  const legthInvalid =
@@ -2,6 +2,13 @@ import { useState, useCallback } from "react";
2
2
 
3
3
  import { useRecyclerViewContext } from "../RecyclerViewContextProvider";
4
4
 
5
+ export type LayoutStateSetter<T> = (
6
+ newValue: T | ((prevValue: T) => T),
7
+ skipParentLayout?: boolean
8
+ ) => void;
9
+
10
+ export type LayoutStateInitialValue<T> = T | (() => T);
11
+
5
12
  /**
6
13
  * Custom hook that combines state management with RecyclerView layout updates.
7
14
  * This hook provides a way to manage state that affects the layout of the RecyclerView,
@@ -13,8 +20,8 @@ import { useRecyclerViewContext } from "../RecyclerViewContextProvider";
13
20
  * - A setter function that updates the state and triggers a layout recalculation
14
21
  */
15
22
  export function useLayoutState<T>(
16
- initialState: T | (() => T)
17
- ): [T, (newValue: T | ((prevValue: T) => T)) => void] {
23
+ initialState: LayoutStateInitialValue<T>
24
+ ): [T, LayoutStateSetter<T>] {
18
25
  // Initialize state with the provided initial value
19
26
  const [state, setState] = useState<T>(initialState);
20
27
  // Get the RecyclerView context for layout management
@@ -28,16 +35,18 @@ export function useLayoutState<T>(
28
35
  * @param newValue - Either a new state value or a function that receives the previous state
29
36
  * and returns the new state
30
37
  */
31
- const setLayoutState = useCallback(
32
- (newValue: T | ((prevValue: T) => T)) => {
38
+ const setLayoutState: LayoutStateSetter<T> = useCallback(
39
+ (newValue, skipParentLayout) => {
33
40
  // Update the state using either the new value or the result of the updater function
34
41
  setState((prevValue) =>
35
42
  typeof newValue === "function"
36
43
  ? (newValue as (prevValue: T) => T)(prevValue)
37
44
  : newValue
38
45
  );
39
- // Trigger a layout recalculation in the RecyclerView
40
- recyclerViewContext?.layout();
46
+ if (!skipParentLayout) {
47
+ // Trigger a layout recalculation in the RecyclerView
48
+ recyclerViewContext?.layout();
49
+ }
41
50
  },
42
51
  [recyclerViewContext]
43
52
  );
@@ -10,7 +10,7 @@ import { useRecyclerViewContext } from "../RecyclerViewContextProvider";
10
10
  export const useMappingHelper = () => {
11
11
  const recyclerViewContext = useRecyclerViewContext();
12
12
  const getMappingKey = useCallback(
13
- (index: number, itemKey: string | number | bigint) => {
13
+ (itemKey: string | number | bigint, index: number) => {
14
14
  return recyclerViewContext ? index : itemKey;
15
15
  },
16
16
  [recyclerViewContext]
@@ -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]
@@ -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,6 +79,14 @@ 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
@@ -122,8 +137,7 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
122
137
  * @returns Width of the item
123
138
  */
124
139
  private getWidth(index: number): number {
125
- const span = this.getSpanSizeInfo(index).span ?? 1;
126
- return (this.boundedSize / this.maxColumns) * span;
140
+ return (this.boundedSize / this.maxColumns) * this.getSpan(index);
127
141
  }
128
142
 
129
143
  /**
@@ -205,6 +219,12 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
205
219
  return y + maxHeight;
206
220
  }
207
221
 
222
+ private updateAllWidths() {
223
+ for (let i = 0; i < this.layouts.length; i++) {
224
+ this.layouts[i].width = this.getWidth(i);
225
+ }
226
+ }
227
+
208
228
  /**
209
229
  * Checks if an item can fit within the bounded width.
210
230
  * @param itemX Starting X position of the item
@@ -20,8 +20,6 @@ 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
25
 
@@ -41,6 +39,13 @@ export abstract class RVLayoutManager {
41
39
  private widthAverageWindow: MultiTypeAverageWindow;
42
40
  /** Maximum number of items to process in a single layout pass */
43
41
  private maxItemsToProcess = 250; // TODO: make this dynamic
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;
44
49
 
45
50
  constructor(params: LayoutParams, previousLayoutManager?: RVLayoutManager) {
46
51
  this.heightAverageWindow = new MultiTypeAverageWindow(5, 200);
@@ -164,37 +169,46 @@ export abstract class RVLayoutManager {
164
169
 
165
170
  if (this.layouts.length > totalItemCount) {
166
171
  this.layouts.length = totalItemCount;
172
+ this.spanTracker.length = totalItemCount;
167
173
  minRecomputeIndex = totalItemCount - 1; // <0 gets skipped so it's safe to set to totalItemCount - 1
168
174
  }
169
175
  // update average windows
170
176
  minRecomputeIndex = Math.min(
171
177
  minRecomputeIndex,
172
- this.computeEstimatesAndMinRecomputeIndex(layoutInfo)
178
+ this.computeEstimatesAndMinMaxChangedLayout(layoutInfo)
173
179
  );
174
180
 
175
181
  if (this.layouts.length < totalItemCount && totalItemCount > 0) {
176
182
  const startIndex = this.layouts.length;
177
183
  this.layouts.length = totalItemCount;
184
+ this.spanTracker.length = totalItemCount;
178
185
  for (let i = startIndex; i < totalItemCount; i++) {
179
186
  this.getLayout(i);
187
+ this.getSpan(i);
180
188
  }
181
189
  this.recomputeLayouts(startIndex, totalItemCount - 1);
182
190
  }
183
- minRecomputeIndex = Math.min(
184
- minRecomputeIndex,
185
- this.processLayoutInfo(layoutInfo, totalItemCount) ?? minRecomputeIndex
186
- );
191
+
187
192
  // compute minRecomputeIndex
193
+
188
194
  minRecomputeIndex = Math.min(
189
195
  minRecomputeIndex,
190
- this.computeEstimatesAndMinRecomputeIndex(layoutInfo)
196
+ this.computeMinIndexWithChangedSpan(layoutInfo),
197
+ this.processLayoutInfo(layoutInfo, totalItemCount) ?? minRecomputeIndex,
198
+ this.computeEstimatesAndMinMaxChangedLayout(layoutInfo)
191
199
  );
200
+
192
201
  if (minRecomputeIndex >= 0 && minRecomputeIndex < totalItemCount) {
202
+ const maxRecomputeIndex = this.getMaxRecomputeIndex(minRecomputeIndex);
193
203
  this.recomputeLayouts(
194
204
  this.getMinRecomputeIndex(minRecomputeIndex),
195
- this.getMaxRecomputeIndex(minRecomputeIndex)
205
+ maxRecomputeIndex
196
206
  );
207
+ if (maxRecomputeIndex + 1 < totalItemCount) {
208
+ this.layouts[maxRecomputeIndex + 1].repositionPending = true;
209
+ }
197
210
  }
211
+ this.currentMaxIndexWithChangedLayout = -1;
198
212
  }
199
213
 
200
214
  /**
@@ -260,16 +274,30 @@ export abstract class RVLayoutManager {
260
274
  protected abstract estimateLayout(index: number): void;
261
275
 
262
276
  /**
263
- * Gets span size information for an item, applying any overrides.
277
+ * Gets span for an item, applying any overrides.
278
+ * This is intended to be called during a relayout call. The value is tracked and used to determine if a span change has occurred.
279
+ * If skipTracking is true, the operation is not tracked. Can be useful if span is required outside of a relayout call.
280
+ * The tracker is used to call handleSpanChange if a span change has occurred before relayout call.
281
+ * // TODO: improve this contract.
264
282
  * @param index Index of the item
265
- * @returns SpanSizeInfo for the item
283
+ * @returns Span for the item
266
284
  */
267
- protected getSpanSizeInfo(index: number): SpanSizeInfo {
285
+ protected getSpan(index: number, skipTracking = false): number {
268
286
  this.spanSizeInfo.span = undefined;
269
287
  this.overrideItemLayout(index, this.spanSizeInfo);
270
- return this.spanSizeInfo;
288
+ const span = Math.min(this.spanSizeInfo.span ?? 1, this.maxColumns);
289
+ if (!skipTracking) {
290
+ this.spanTracker[index] = span;
291
+ }
292
+ return span;
271
293
  }
272
294
 
295
+ /**
296
+ * Method to handle span change for an item. Can be overridden by subclasses.
297
+ * @param index Index of the item
298
+ */
299
+ protected handleSpanChange(index: number) {}
300
+
273
301
  /**
274
302
  * Gets the maximum index to process in a single layout pass.
275
303
  * @param startIndex Starting index
@@ -277,7 +305,8 @@ export abstract class RVLayoutManager {
277
305
  */
278
306
  private getMaxRecomputeIndex(startIndex: number): number {
279
307
  return Math.min(
280
- startIndex + this.maxItemsToProcess,
308
+ Math.max(startIndex, this.currentMaxIndexWithChangedLayout) +
309
+ this.maxItemsToProcess,
281
310
  this.layouts.length - 1
282
311
  );
283
312
  }
@@ -296,7 +325,7 @@ export abstract class RVLayoutManager {
296
325
  * @param layoutInfo Array of layout information for items
297
326
  * @returns Minimum index that needs recomputation
298
327
  */
299
- private computeEstimatesAndMinRecomputeIndex(
328
+ private computeEstimatesAndMinMaxChangedLayout(
300
329
  layoutInfo: RVLayoutInfo[]
301
330
  ): number {
302
331
  let minRecomputeIndex = Number.MAX_VALUE;
@@ -307,10 +336,18 @@ export abstract class RVLayoutManager {
307
336
  !storedLayout ||
308
337
  !storedLayout.isHeightMeasured ||
309
338
  !storedLayout.isWidthMeasured ||
339
+ storedLayout.repositionPending ||
310
340
  areDimensionsNotEqual(storedLayout.height, dimensions.height) ||
311
341
  areDimensionsNotEqual(storedLayout.width, dimensions.width)
312
342
  ) {
313
343
  minRecomputeIndex = Math.min(minRecomputeIndex, index);
344
+ this.currentMaxIndexWithChangedLayout = Math.max(
345
+ this.currentMaxIndexWithChangedLayout,
346
+ index
347
+ );
348
+ if (storedLayout?.repositionPending) {
349
+ storedLayout.repositionPending = false;
350
+ }
314
351
  }
315
352
  this.heightAverageWindow.addValue(
316
353
  dimensions.height,
@@ -323,6 +360,21 @@ export abstract class RVLayoutManager {
323
360
  }
324
361
  return minRecomputeIndex;
325
362
  }
363
+
364
+ private computeMinIndexWithChangedSpan(layoutInfo: RVLayoutInfo[]): number {
365
+ let minIndexWithChangedSpan = Number.MAX_VALUE;
366
+ for (const info of layoutInfo) {
367
+ const { index } = info;
368
+ const span = this.getSpan(index, true);
369
+ const storedSpan = this.spanTracker[index];
370
+ if (span !== storedSpan) {
371
+ this.spanTracker[index] = span;
372
+ this.handleSpanChange(index);
373
+ minIndexWithChangedSpan = Math.min(minIndexWithChangedSpan, index);
374
+ }
375
+ }
376
+ return minIndexWithChangedSpan;
377
+ }
326
378
  }
327
379
 
328
380
  /**
@@ -467,6 +519,13 @@ export interface RVLayout extends RVDimension {
467
519
  * When false, the height is determined by content
468
520
  */
469
521
  enforcedHeight?: boolean;
522
+
523
+ /**
524
+ * When true, the layout is pending repositioning
525
+ * When false, the layout is up to date
526
+ * ViewHolder update is not required.
527
+ */
528
+ repositionPending?: boolean;
470
529
  }
471
530
 
472
531
  /**
@@ -19,10 +19,13 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
19
19
  /** Current column index for sequential placement */
20
20
  private currentColumn = 0;
21
21
 
22
+ /** If there's a span change for masonry layout, we need to recompute all the widths */
23
+ private fullRelayoutRequired = false;
24
+
22
25
  constructor(params: LayoutParams, previousLayoutManager?: RVLayoutManager) {
23
26
  super(params, previousLayoutManager);
24
27
  this.boundedSize = params.windowSize.width;
25
- this.optimizeItemArrangement = params.optimizeItemArrangement ?? false;
28
+ this.optimizeItemArrangement = params.optimizeItemArrangement;
26
29
  this.columnHeights = this.columnHeights ?? Array(this.maxColumns).fill(0);
27
30
  }
28
31
 
@@ -44,10 +47,7 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
44
47
  // console.log("-----> recomputeLayouts");
45
48
 
46
49
  // update all widths
47
- for (let i = 0; i < this.layouts.length; i++) {
48
- this.layouts[i].width = this.getWidth(i);
49
- this.layouts[i].minHeight = undefined;
50
- }
50
+ this.updateAllWidths();
51
51
  this.recomputeLayouts(0, this.layouts.length - 1);
52
52
  this.requiresRepaint = true;
53
53
  }
@@ -69,6 +69,13 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
69
69
  layout.isWidthMeasured = true;
70
70
  this.layouts[index] = layout;
71
71
  }
72
+
73
+ // TODO: Can be optimized
74
+ if (this.fullRelayoutRequired) {
75
+ this.updateAllWidths();
76
+ this.fullRelayoutRequired = false;
77
+ return 0;
78
+ }
72
79
  }
73
80
 
74
81
  /**
@@ -87,6 +94,14 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
87
94
  layout.enforcedWidth = true;
88
95
  }
89
96
 
97
+ /**
98
+ * Handles span change for an item.
99
+ * @param index Index of the item
100
+ */
101
+ handleSpanChange(index: number) {
102
+ this.fullRelayoutRequired = true;
103
+ }
104
+
90
105
  /**
91
106
  * Returns the total size of the layout area.
92
107
  * @returns RVDimension containing width and height of the layout
@@ -124,7 +139,8 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
124
139
 
125
140
  for (let i = startIndex; i < itemCount; i++) {
126
141
  const layout = this.getLayout(i);
127
- const span = this.getSpanSizeInfo(i).span ?? 1;
142
+ // Skip tracking span because we're not changing widths
143
+ const span = this.getSpan(i, true);
128
144
 
129
145
  if (this.optimizeItemArrangement) {
130
146
  if (span === 1) {
@@ -147,8 +163,14 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
147
163
  * @returns Width of the item
148
164
  */
149
165
  private getWidth(index: number): number {
150
- const span = this.getSpanSizeInfo(index).span ?? 1;
151
- return (this.boundedSize / this.maxColumns) * span;
166
+ return (this.boundedSize / this.maxColumns) * this.getSpan(index);
167
+ }
168
+
169
+ private updateAllWidths() {
170
+ for (let i = 0; i < this.layouts.length; i++) {
171
+ this.layouts[i].width = this.getWidth(i);
172
+ this.layouts[i].minHeight = undefined;
173
+ }
152
174
  }
153
175
 
154
176
  /**
@@ -1,5 +1,5 @@
1
- export default interface ViewToken {
2
- item: any; // TODO: fix this type
1
+ export default interface ViewToken<T> {
2
+ item: T;
3
3
  key: string;
4
4
  index: number | null;
5
5
  isViewable: boolean;
@@ -89,8 +89,8 @@ class ViewabilityHelper {
89
89
  const timeoutId = setTimeout(() => {
90
90
  this.timers.delete(timeoutId);
91
91
  this.checkViewableIndicesChanges(newViewableIndices);
92
- this.timers.add(timeoutId);
93
92
  }, minimumViewTime);
93
+ this.timers.add(timeoutId);
94
94
  } else {
95
95
  this.checkViewableIndicesChanges(newViewableIndices);
96
96
  }
@@ -22,17 +22,21 @@ export default class ViewabilityManager<T> {
22
22
  this.viewabilityHelpers.push(
23
23
  this.createViewabilityHelper(
24
24
  flashListRef.props.viewabilityConfig,
25
- flashListRef.props.onViewableItemsChanged
25
+ (info) => {
26
+ flashListRef.props.onViewableItemsChanged?.(info);
27
+ }
26
28
  )
27
29
  );
28
30
  }
29
31
  (flashListRef.props.viewabilityConfigCallbackPairs ?? []).forEach(
30
- (pair) => {
32
+ (pair, index) => {
31
33
  this.viewabilityHelpers.push(
32
- this.createViewabilityHelper(
33
- pair.viewabilityConfig,
34
- pair.onViewableItemsChanged
35
- )
34
+ this.createViewabilityHelper(pair.viewabilityConfig, (info) => {
35
+ const callback =
36
+ flashListRef.props.viewabilityConfigCallbackPairs?.[index]
37
+ ?.onViewableItemsChanged;
38
+ callback?.(info);
39
+ })
36
40
  );
37
41
  }
38
42
  );
@@ -102,15 +106,18 @@ export default class ViewabilityManager<T> {
102
106
  private createViewabilityHelper = (
103
107
  viewabilityConfig: ViewabilityConfig | null | undefined,
104
108
  onViewableItemsChanged:
105
- | ((info: { viewableItems: ViewToken[]; changed: ViewToken[] }) => void)
109
+ | ((info: {
110
+ viewableItems: ViewToken<T>[];
111
+ changed: ViewToken<T>[];
112
+ }) => void)
106
113
  | null
107
114
  | undefined
108
115
  ) => {
109
- const mapViewToken: (index: number, isViewable: boolean) => ViewToken = (
116
+ const mapViewToken: (index: number, isViewable: boolean) => ViewToken<T> = (
110
117
  index: number,
111
118
  isViewable: boolean
112
119
  ) => {
113
- const item = this.flashListRef.props.data?.[index];
120
+ const item = this.flashListRef.props.data![index];
114
121
  const key =
115
122
  item === undefined || this.flashListRef.props.keyExtractor === undefined
116
123
  ? index.toString()