@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.
- package/dist/FlashListProps.d.ts +10 -2
- package/dist/FlashListProps.d.ts.map +1 -1
- package/dist/FlashListProps.js.map +1 -1
- package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerView.js +7 -1
- package/dist/recyclerview/RecyclerView.js.map +1 -1
- package/dist/recyclerview/hooks/useLayoutState.d.ts +3 -1
- package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useLayoutState.js +5 -3
- package/dist/recyclerview/hooks/useLayoutState.js.map +1 -1
- package/dist/recyclerview/hooks/useRecyclingState.d.ts +4 -2
- package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useRecyclingState.js +2 -2
- package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +14 -6
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.js +40 -23
- package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts +26 -6
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/LayoutManager.js +69 -12
- package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +9 -1
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +28 -12
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
- package/dist/viewability/ViewabilityManager.js +10 -3
- package/dist/viewability/ViewabilityManager.js.map +1 -1
- package/package.json +1 -1
- package/src/FlashListProps.ts +16 -2
- package/src/recyclerview/RecyclerView.tsx +13 -6
- package/src/recyclerview/hooks/useLayoutState.ts +15 -6
- package/src/recyclerview/hooks/useRecyclingState.ts +11 -7
- package/src/recyclerview/layout-managers/GridLayoutManager.ts +44 -23
- package/src/recyclerview/layout-managers/LayoutManager.ts +74 -15
- package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +30 -8
- 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
|
|
17
|
-
): [T,
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
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 {
|
|
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
|
|
23
|
+
initialState: RecyclingStateInitialValue<T>,
|
|
20
24
|
deps: React.DependencyList,
|
|
21
25
|
onReset?: () => void
|
|
22
|
-
): [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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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(
|
|
137
|
-
const startIndex = this.
|
|
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 (
|
|
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 (
|
|
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
|
|
205
|
+
* @param endIndex Index of the last item in the row
|
|
191
206
|
* @returns Total height of the layout
|
|
192
207
|
*/
|
|
193
|
-
private
|
|
194
|
-
const startIndex = this.
|
|
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 (
|
|
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
|
|
241
|
+
* @param itemIndex Index to start searching from
|
|
221
242
|
* @returns Index of the first item in the row
|
|
222
243
|
*/
|
|
223
|
-
private
|
|
224
|
-
if (
|
|
244
|
+
private locateFirstIndexInRow(itemIndex: number): number {
|
|
245
|
+
if (itemIndex === 0) {
|
|
225
246
|
return 0;
|
|
226
247
|
}
|
|
227
|
-
let i =
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
283
|
+
* @returns Span for the item
|
|
266
284
|
*/
|
|
267
|
-
protected
|
|
285
|
+
protected getSpan(index: number, skipTracking = false): number {
|
|
268
286
|
this.spanSizeInfo.span = undefined;
|
|
269
287
|
this.overrideItemLayout(index, this.spanSizeInfo);
|
|
270
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
);
|