@shopify/flash-list 2.0.0-alpha.15 → 2.0.0-alpha.17

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 (39) hide show
  1. package/dist/FlashListProps.d.ts +10 -2
  2. package/dist/FlashListProps.d.ts.map +1 -1
  3. package/dist/FlashListProps.js.map +1 -1
  4. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  5. package/dist/recyclerview/RecyclerView.js +7 -1
  6. package/dist/recyclerview/RecyclerView.js.map +1 -1
  7. package/dist/recyclerview/hooks/useLayoutState.d.ts +3 -1
  8. package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -1
  9. package/dist/recyclerview/hooks/useLayoutState.js +5 -3
  10. package/dist/recyclerview/hooks/useLayoutState.js.map +1 -1
  11. package/dist/recyclerview/hooks/useRecyclingState.d.ts +4 -2
  12. package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -1
  13. package/dist/recyclerview/hooks/useRecyclingState.js +2 -2
  14. package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -1
  15. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +14 -6
  16. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  17. package/dist/recyclerview/layout-managers/GridLayoutManager.js +40 -23
  18. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  19. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +26 -6
  20. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
  21. package/dist/recyclerview/layout-managers/LayoutManager.js +69 -12
  22. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  23. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +9 -1
  24. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -1
  25. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +28 -12
  26. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
  29. package/dist/viewability/ViewabilityManager.js +10 -3
  30. package/dist/viewability/ViewabilityManager.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/FlashListProps.ts +16 -2
  33. package/src/recyclerview/RecyclerView.tsx +13 -6
  34. package/src/recyclerview/hooks/useLayoutState.ts +15 -6
  35. package/src/recyclerview/hooks/useRecyclingState.ts +11 -7
  36. package/src/recyclerview/layout-managers/GridLayoutManager.ts +44 -23
  37. package/src/recyclerview/layout-managers/LayoutManager.ts +74 -15
  38. package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +30 -8
  39. package/src/viewability/ViewabilityManager.ts +10 -6
@@ -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
  );
@@ -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,13 +79,21 @@ 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 totalHeight = this.computeTotalHeight(this.layouts.length - 1);
96
+ const totalHeight = this.computeTotalHeightTillRow(this.layouts.length - 1);
82
97
  return {
83
98
  width: this.boundedSize,
84
99
  height: totalHeight,
@@ -91,7 +106,7 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
91
106
  * @param endIndex Ending index of items to recompute
92
107
  */
93
108
  recomputeLayouts(startIndex: number, endIndex: number): void {
94
- const newStartIndex = this.locateFirstNeighbourIndex(
109
+ const newStartIndex = this.locateFirstIndexInRow(
95
110
  Math.max(0, startIndex - 1)
96
111
  );
97
112
  const startVal = this.getLayout(newStartIndex);
@@ -122,25 +137,23 @@ 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
  /**
130
144
  * Processes items in a row and returns the tallest item.
131
145
  * Also handles height normalization for items in the same row.
132
146
  * Tallest item per row helps in forcing tallest items height on neighbouring items.
133
- * @param index Index of the last item in the row
147
+ * @param endIndex Index of the last item in the row
134
148
  * @returns The tallest item in the row
135
149
  */
136
- private processAndReturnTallestItemInRow(index: number): RVLayout {
137
- const startIndex = this.locateFirstNeighbourIndex(index);
138
- const y = this.layouts[startIndex].y;
150
+ private processAndReturnTallestItemInRow(endIndex: number): RVLayout {
151
+ const startIndex = this.locateFirstIndexInRow(endIndex);
139
152
  let tallestItem: RVLayout | undefined;
140
153
  let maxHeight = 0;
141
154
  let i = startIndex;
142
155
  let isMeasured = false;
143
- while (Math.ceil(this.layouts[i].y) === Math.ceil(y)) {
156
+ while (i <= endIndex) {
144
157
  const layout = this.layouts[i];
145
158
  isMeasured = isMeasured || Boolean(layout.isHeightMeasured);
146
159
  maxHeight = Math.max(maxHeight, layout.height);
@@ -156,7 +169,9 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
156
169
  break;
157
170
  }
158
171
  }
159
-
172
+ if (!tallestItem && maxHeight > 0) {
173
+ maxHeight = Number.MAX_SAFE_INTEGER;
174
+ }
160
175
  tallestItem = tallestItem ?? this.layouts[startIndex];
161
176
 
162
177
  if (!isMeasured) {
@@ -170,7 +185,7 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
170
185
  this.requiresRepaint = true;
171
186
  }
172
187
  i = startIndex;
173
- while (Math.ceil(this.layouts[i].y) === Math.ceil(y)) {
188
+ while (i <= endIndex) {
174
189
  this.layouts[i].minHeight = targetHeight;
175
190
  if (targetHeight > 0) {
176
191
  this.layouts[i].height = targetHeight;
@@ -187,15 +202,15 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
187
202
 
188
203
  /**
189
204
  * Computes the total height of the layout.
190
- * @param index Index of the last item in the layout
205
+ * @param endIndex Index of the last item in the row
191
206
  * @returns Total height of the layout
192
207
  */
193
- private computeTotalHeight(index: number): number {
194
- const startIndex = this.locateFirstNeighbourIndex(index);
208
+ private computeTotalHeightTillRow(endIndex: number): number {
209
+ const startIndex = this.locateFirstIndexInRow(endIndex);
195
210
  const y = this.layouts[startIndex].y;
196
211
  let maxHeight = 0;
197
212
  let i = startIndex;
198
- while (Math.ceil(this.layouts[i].y) === Math.ceil(y)) {
213
+ while (i <= endIndex) {
199
214
  maxHeight = Math.max(maxHeight, this.layouts[i].height);
200
215
  i++;
201
216
  if (i >= this.layouts.length) {
@@ -205,6 +220,12 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
205
220
  return y + maxHeight;
206
221
  }
207
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
+
208
229
  /**
209
230
  * Checks if an item can fit within the bounded width.
210
231
  * @param itemX Starting X position of the item
@@ -217,14 +238,14 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
217
238
 
218
239
  /**
219
240
  * Locates the index of the first item in the current row.
220
- * @param startIndex Index to start searching from
241
+ * @param itemIndex Index to start searching from
221
242
  * @returns Index of the first item in the row
222
243
  */
223
- private locateFirstNeighbourIndex(startIndex: number): number {
224
- if (startIndex === 0) {
244
+ private locateFirstIndexInRow(itemIndex: number): number {
245
+ if (itemIndex === 0) {
225
246
  return 0;
226
247
  }
227
- let i = startIndex;
248
+ let i = itemIndex;
228
249
  for (; i >= 0; i--) {
229
250
  if (this.layouts[i].x === 0) {
230
251
  break;
@@ -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
  /**
@@ -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
  );