@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.
- package/README.md +33 -97
- package/dist/FlashListProps.d.ts +2 -2
- package/dist/FlashListProps.d.ts.map +1 -1
- package/dist/MasonryFlashList.js.map +1 -1
- package/dist/__tests__/RenderStackManager.test.js +1 -2
- package/dist/__tests__/RenderStackManager.test.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/RenderStackManager.d.ts +1 -0
- package/dist/recyclerview/RenderStackManager.d.ts.map +1 -1
- package/dist/recyclerview/RenderStackManager.js +26 -7
- package/dist/recyclerview/RenderStackManager.js.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.js +1 -1
- package/dist/recyclerview/components/StickyHeaders.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/useMappingHelper.d.ts +1 -1
- package/dist/recyclerview/hooks/useMappingHelper.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useMappingHelper.js +1 -1
- package/dist/recyclerview/hooks/useMappingHelper.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 +9 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.js +22 -7
- 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/ViewToken.d.ts +2 -2
- package/dist/viewability/ViewToken.d.ts.map +1 -1
- package/dist/viewability/ViewabilityHelper.js +1 -1
- package/dist/viewability/ViewabilityHelper.js.map +1 -1
- package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
- package/dist/viewability/ViewabilityManager.js +11 -5
- package/dist/viewability/ViewabilityManager.js.map +1 -1
- package/package.json +1 -1
- package/src/FlashListProps.ts +4 -1
- package/src/MasonryFlashList.tsx +2 -2
- package/src/__tests__/RenderStackManager.test.ts +1 -2
- package/src/recyclerview/RecyclerView.tsx +13 -6
- package/src/recyclerview/RenderStackManager.ts +32 -6
- package/src/recyclerview/components/StickyHeaders.tsx +1 -1
- package/src/recyclerview/hooks/useLayoutState.ts +15 -6
- package/src/recyclerview/hooks/useMappingHelper.ts +1 -1
- package/src/recyclerview/hooks/useRecyclingState.ts +11 -7
- package/src/recyclerview/layout-managers/GridLayoutManager.ts +26 -6
- package/src/recyclerview/layout-managers/LayoutManager.ts +74 -15
- package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +30 -8
- package/src/viewability/ViewToken.ts +2 -2
- package/src/viewability/ViewabilityHelper.ts +1 -1
- 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
|
-
{
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
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
|
);
|
|
@@ -10,7 +10,7 @@ import { useRecyclerViewContext } from "../RecyclerViewContextProvider";
|
|
|
10
10
|
export const useMappingHelper = () => {
|
|
11
11
|
const recyclerViewContext = useRecyclerViewContext();
|
|
12
12
|
const getMappingKey = useCallback(
|
|
13
|
-
(
|
|
13
|
+
(itemKey: string | number | bigint, index: number) => {
|
|
14
14
|
return recyclerViewContext ? index : itemKey;
|
|
15
15
|
},
|
|
16
16
|
[recyclerViewContext]
|
|
@@ -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,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
|
-
|
|
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.
|
|
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
|
/**
|
|
@@ -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
|
-
|
|
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
|
);
|
|
@@ -102,15 +106,18 @@ export default class ViewabilityManager<T> {
|
|
|
102
106
|
private createViewabilityHelper = (
|
|
103
107
|
viewabilityConfig: ViewabilityConfig | null | undefined,
|
|
104
108
|
onViewableItemsChanged:
|
|
105
|
-
| ((info: {
|
|
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
|
|
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()
|