@souscheflabs/reanimated-flashlist 0.4.4 → 0.5.0
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/lib/AnimatedFlashList.d.ts +2 -2
- package/lib/AnimatedFlashList.d.ts.map +1 -1
- package/lib/AnimatedFlashList.js +291 -33
- package/lib/AnimatedFlashListItem.d.ts.map +1 -1
- package/lib/AnimatedFlashListItem.js +59 -48
- package/lib/__tests__/utils/test-utils.d.ts +62 -1
- package/lib/__tests__/utils/test-utils.d.ts.map +1 -1
- package/lib/__tests__/utils/test-utils.js +110 -2
- package/lib/constants/drag.d.ts +42 -7
- package/lib/constants/drag.d.ts.map +1 -1
- package/lib/constants/drag.js +41 -8
- package/lib/contexts/DragStateContext.d.ts +105 -2
- package/lib/contexts/DragStateContext.d.ts.map +1 -1
- package/lib/contexts/DragStateContext.js +178 -12
- package/lib/hooks/drag/index.d.ts +8 -0
- package/lib/hooks/drag/index.d.ts.map +1 -1
- package/lib/hooks/drag/index.js +10 -1
- package/lib/hooks/drag/useAutoscroll.d.ts +41 -0
- package/lib/hooks/drag/useAutoscroll.d.ts.map +1 -0
- package/lib/hooks/drag/useAutoscroll.js +60 -0
- package/lib/hooks/drag/useDragAnimatedStyle.d.ts +8 -12
- package/lib/hooks/drag/useDragAnimatedStyle.d.ts.map +1 -1
- package/lib/hooks/drag/useDragAnimatedStyle.js +279 -243
- package/lib/hooks/drag/useDragCleanup.d.ts +27 -0
- package/lib/hooks/drag/useDragCleanup.d.ts.map +1 -0
- package/lib/hooks/drag/useDragCleanup.js +100 -0
- package/lib/hooks/drag/useDragGesture.d.ts +6 -1
- package/lib/hooks/drag/useDragGesture.d.ts.map +1 -1
- package/lib/hooks/drag/useDragGesture.js +179 -412
- package/lib/hooks/drag/useDragOverlay.d.ts +48 -0
- package/lib/hooks/drag/useDragOverlay.d.ts.map +1 -0
- package/lib/hooks/drag/useDragOverlay.js +87 -0
- package/lib/hooks/drag/useDragShift.d.ts +7 -5
- package/lib/hooks/drag/useDragShift.d.ts.map +1 -1
- package/lib/hooks/drag/useDragShift.js +36 -243
- package/lib/hooks/drag/useDropCompensation.d.ts +6 -10
- package/lib/hooks/drag/useDropCompensation.d.ts.map +1 -1
- package/lib/hooks/drag/useDropCompensation.js +16 -49
- package/lib/hooks/drag/useDropHandler.d.ts +48 -0
- package/lib/hooks/drag/useDropHandler.d.ts.map +1 -0
- package/lib/hooks/drag/useDropHandler.js +221 -0
- package/lib/hooks/drag/useOverlayCrossfade.d.ts +25 -0
- package/lib/hooks/drag/useOverlayCrossfade.d.ts.map +1 -0
- package/lib/hooks/drag/useOverlayCrossfade.js +70 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +5 -1
- package/lib/types/drag.d.ts +0 -4
- package/lib/types/drag.d.ts.map +1 -1
- package/lib/utils/dragStateReset.d.ts +134 -0
- package/lib/utils/dragStateReset.d.ts.map +1 -0
- package/lib/utils/dragStateReset.js +114 -0
- package/lib/utils/hoverCalculation.d.ts +81 -0
- package/lib/utils/hoverCalculation.d.ts.map +1 -0
- package/lib/utils/hoverCalculation.js +87 -0
- package/package.json +12 -10
- package/src/AnimatedFlashList.tsx +483 -53
- package/src/AnimatedFlashListItem.tsx +72 -59
- package/src/__tests__/components/AnimatedFlashList.test.tsx +51 -51
- package/src/__tests__/components/AnimatedFlashListItem.test.tsx +33 -33
- package/src/__tests__/contexts/DragStateContext.test.tsx +30 -30
- package/src/__tests__/contexts/ListAnimationContext.test.tsx +58 -58
- package/src/__tests__/hooks/useDragAnimatedStyle.test.tsx +15 -15
- package/src/__tests__/hooks/useDragGesture.test.tsx +27 -29
- package/src/__tests__/hooks/useDragShift.test.tsx +23 -24
- package/src/__tests__/hooks/useDropCompensation.test.tsx +36 -35
- package/src/__tests__/hooks/useListEntryAnimation.test.tsx +24 -24
- package/src/__tests__/hooks/useListExitAnimation.test.tsx +34 -34
- package/src/__tests__/hooks/useOverlayCrossfade.test.tsx +57 -0
- package/src/__tests__/utils/test-utils.tsx +148 -2
- package/src/constants/drag.ts +46 -7
- package/src/contexts/DragStateContext.tsx +304 -16
- package/src/hooks/drag/index.ts +18 -0
- package/src/hooks/drag/useAutoscroll.ts +109 -0
- package/src/hooks/drag/useDragAnimatedStyle.ts +344 -293
- package/src/hooks/drag/useDragCleanup.ts +134 -0
- package/src/hooks/drag/useDragGesture.ts +244 -526
- package/src/hooks/drag/useDragOverlay.ts +141 -0
- package/src/hooks/drag/useDragShift.ts +37 -307
- package/src/hooks/drag/useDropCompensation.ts +16 -57
- package/src/hooks/drag/useDropHandler.ts +329 -0
- package/src/hooks/drag/useOverlayCrossfade.ts +84 -0
- package/src/index.ts +5 -0
- package/src/types/drag.ts +0 -4
- package/src/utils/dragStateReset.ts +202 -0
- package/src/utils/hoverCalculation.ts +180 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import type { AnimatedListItem, AnimatedFlashListProps, AnimatedFlashListRef } from
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { AnimatedListItem, AnimatedFlashListProps, AnimatedFlashListRef } from "./types";
|
|
3
3
|
export declare const AnimatedFlashList: <T extends AnimatedListItem>(props: AnimatedFlashListProps<T> & {
|
|
4
4
|
ref?: React.ForwardedRef<AnimatedFlashListRef>;
|
|
5
5
|
}) => React.ReactElement | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnimatedFlashList.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashList.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"AnimatedFlashList.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashList.tsx"],"names":[],"mappings":"AAAA,OAAO,KASN,MAAM,OAAO,CAAC;AA2Bf,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EAErB,MAAM,SAAS,CAAC;AA65BjB,eAAO,MAAM,iBAAiB,EAAyC,CACrE,CAAC,SAAS,gBAAgB,EAE1B,KAAK,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAAG;IACjC,GAAG,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,oBAAoB,CAAC,CAAC;CAChD,KACE,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC"}
|
package/lib/AnimatedFlashList.js
CHANGED
|
@@ -36,10 +36,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.AnimatedFlashList = void 0;
|
|
37
37
|
const react_1 = __importStar(require("react"));
|
|
38
38
|
const react_native_1 = require("react-native");
|
|
39
|
+
const react_native_reanimated_1 = __importStar(require("react-native-reanimated"));
|
|
40
|
+
const react_native_gesture_handler_1 = require("react-native-gesture-handler");
|
|
41
|
+
const react_native_worklets_1 = require("react-native-worklets");
|
|
39
42
|
const flash_list_1 = require("@shopify/flash-list");
|
|
40
|
-
const
|
|
43
|
+
const DragStateContext_1 = require("./contexts/DragStateContext");
|
|
44
|
+
const useOverlayCrossfade_1 = require("./hooks/drag/useOverlayCrossfade");
|
|
45
|
+
const ListAnimationContext_1 = require("./contexts/ListAnimationContext");
|
|
41
46
|
const AnimatedFlashListItem_1 = require("./AnimatedFlashListItem");
|
|
42
|
-
const
|
|
47
|
+
const drag_1 = require("./constants/drag");
|
|
43
48
|
class AnimationErrorBoundary extends react_1.default.Component {
|
|
44
49
|
state = { hasError: false };
|
|
45
50
|
static getDerivedStateFromError() {
|
|
@@ -47,8 +52,8 @@ class AnimationErrorBoundary extends react_1.default.Component {
|
|
|
47
52
|
}
|
|
48
53
|
componentDidCatch(error) {
|
|
49
54
|
if (__DEV__) {
|
|
50
|
-
console.warn(
|
|
51
|
-
|
|
55
|
+
console.warn("[AnimatedFlashList] Animation error caught by error boundary. " +
|
|
56
|
+
"Falling back to non-animated list.", error);
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
render() {
|
|
@@ -58,23 +63,270 @@ class AnimationErrorBoundary extends react_1.default.Component {
|
|
|
58
63
|
return this.props.children;
|
|
59
64
|
}
|
|
60
65
|
}
|
|
66
|
+
const DragOverlay = react_1.default.memo(function DragOverlay({ data, totalItemsRef, renderItem }) {
|
|
67
|
+
const { overlayActive, overlayItemId, overlayBaseX, overlayBaseY, overlayWidth, overlayHeight, overlayTranslateY, overlayReady, overlayVisible, scrollOffset, dragStartScrollOffset, draggedScale, isDragging, overlayTouchOffset, overlayAbsoluteY, overlayContainerY, dragOverlayOpacity, overlayFrozenItemId, overlayRenderedOnUI, } = (0, DragStateContext_1.useDragState)();
|
|
68
|
+
// Drive overlay crossfade animation on UI thread
|
|
69
|
+
// This ensures smooth visibility transitions without JS scheduling races
|
|
70
|
+
(0, useOverlayCrossfade_1.useOverlayCrossfade)();
|
|
71
|
+
const [overlayId, setOverlayId] = (0, react_1.useState)(null);
|
|
72
|
+
const [frozenId, setFrozenId] = (0, react_1.useState)(null);
|
|
73
|
+
const setOverlayIdSafe = (0, react_1.useCallback)((nextId) => {
|
|
74
|
+
setOverlayId(nextId);
|
|
75
|
+
}, []);
|
|
76
|
+
const setFrozenIdSafe = (0, react_1.useCallback)((nextId) => {
|
|
77
|
+
setFrozenId(nextId);
|
|
78
|
+
}, []);
|
|
79
|
+
(0, react_native_reanimated_1.useAnimatedReaction)(() => overlayItemId.value, (current, prev) => {
|
|
80
|
+
"worklet";
|
|
81
|
+
if (current === prev)
|
|
82
|
+
return;
|
|
83
|
+
(0, react_native_worklets_1.scheduleOnRN)(setOverlayIdSafe, current || null);
|
|
84
|
+
}, [setOverlayIdSafe]);
|
|
85
|
+
// Track frozen ID for rendering during fade-out
|
|
86
|
+
(0, react_native_reanimated_1.useAnimatedReaction)(() => overlayFrozenItemId.value, (current, prev) => {
|
|
87
|
+
"worklet";
|
|
88
|
+
if (current === prev)
|
|
89
|
+
return;
|
|
90
|
+
(0, react_native_worklets_1.scheduleOnRN)(setFrozenIdSafe, current || null);
|
|
91
|
+
}, [setFrozenIdSafe]);
|
|
92
|
+
(0, react_1.useEffect)(() => {
|
|
93
|
+
overlayVisible.value = Boolean(overlayId);
|
|
94
|
+
}, [overlayId, overlayVisible]);
|
|
95
|
+
// Use frozenId to keep overlay rendered during fade-out
|
|
96
|
+
const overlayItem = (0, react_1.useMemo)(() => {
|
|
97
|
+
const id = frozenId || overlayId;
|
|
98
|
+
if (!id)
|
|
99
|
+
return null;
|
|
100
|
+
return data.find((item) => item.id === id) ?? null;
|
|
101
|
+
}, [data, frozenId, overlayId]);
|
|
102
|
+
// Create disabled gesture and false isDragging for overlay rendering
|
|
103
|
+
// This allows renderItem to show drag handle UI without functional gesture
|
|
104
|
+
const disabledGesture = (0, react_1.useMemo)(() => react_native_gesture_handler_1.Gesture.Pan().enabled(false), []);
|
|
105
|
+
const falseIsDragging = (0, react_native_reanimated_1.useSharedValue)(false);
|
|
106
|
+
const overlayStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => {
|
|
107
|
+
// Use frozenItemId so overlay stays renderable during fade-out
|
|
108
|
+
const itemId = overlayFrozenItemId.value || overlayItemId.value;
|
|
109
|
+
// Keep rendering if frozenItemId is set (crossfade in progress) even if overlayReady is false.
|
|
110
|
+
// This prevents the overlay from becoming invisible before the crossfade completes,
|
|
111
|
+
// which would cause a brief moment where both overlay AND original are invisible.
|
|
112
|
+
// FIXED: Also require overlayVisible to ensure JS-side overlayItem exists before fading.
|
|
113
|
+
// overlayVisible is only cleared AFTER fade-out completes (in useOverlayCrossfade),
|
|
114
|
+
// so this doesn't break fade-out.
|
|
115
|
+
if (!itemId || (!overlayFrozenItemId.value && !overlayReady.value) || !overlayVisible.value) {
|
|
116
|
+
// Mark overlay as NOT rendered when returning invisible style
|
|
117
|
+
overlayRenderedOnUI.value = false;
|
|
118
|
+
return { opacity: 0, position: "absolute" };
|
|
119
|
+
}
|
|
120
|
+
// Mark overlay as rendered on UI thread - this is the KEY fix for the vanish bug.
|
|
121
|
+
// Setting this here (in useAnimatedStyle, which runs on UI thread) instead of
|
|
122
|
+
// in useEffect (JS thread) ensures the original item knows the overlay has
|
|
123
|
+
// actually rendered BEFORE the crossfade starts. This prevents the race condition
|
|
124
|
+
// where the original starts fading out before the overlay appears.
|
|
125
|
+
overlayRenderedOnUI.value = true;
|
|
126
|
+
const scale = isDragging.value ? draggedScale.value : 1;
|
|
127
|
+
// During active drag, use absolute finger position for reliable overlay tracking.
|
|
128
|
+
// This avoids lag from JS-thread scroll offset updates during simultaneous scroll+drag.
|
|
129
|
+
// Formula: top = absoluteY - touchOffset - containerY
|
|
130
|
+
// Where:
|
|
131
|
+
// - absoluteY = finger's current screen Y position
|
|
132
|
+
// - touchOffset = how far down in the item the finger started
|
|
133
|
+
// - containerY = overlay container's screen Y position
|
|
134
|
+
//
|
|
135
|
+
// During drop animation (isDropping), switch back to slot-based positioning
|
|
136
|
+
// since the finger is no longer tracking and we need to animate to final slot.
|
|
137
|
+
let top;
|
|
138
|
+
if (isDragging.value) {
|
|
139
|
+
// Active drag: follow finger precisely
|
|
140
|
+
top = overlayAbsoluteY.value - overlayTouchOffset.value - overlayContainerY.value;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Drop animation: use slot-based positioning with scroll compensation
|
|
144
|
+
// Note: scrollDelta is SUBTRACTED because overlayBaseY was measured at drag start
|
|
145
|
+
// (when scroll was dragStartScrollOffset). As user scrolls down (scrollDelta > 0),
|
|
146
|
+
// the target slot moves UP visually, so we subtract.
|
|
147
|
+
const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
|
|
148
|
+
top = overlayBaseY.value + overlayTranslateY.value - scrollDelta;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
position: "absolute",
|
|
152
|
+
left: overlayBaseX.value,
|
|
153
|
+
top,
|
|
154
|
+
width: overlayWidth.value,
|
|
155
|
+
height: overlayHeight.value,
|
|
156
|
+
zIndex: 1000,
|
|
157
|
+
transform: [{ scale }],
|
|
158
|
+
opacity: dragOverlayOpacity.value, // Uses shared crossfade value
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
// Only return null when BOTH overlayItem is null AND frozenId is empty
|
|
162
|
+
// This keeps the Animated.View mounted during fade-out (while frozenId is set)
|
|
163
|
+
if (!overlayItem && !frozenId)
|
|
164
|
+
return null;
|
|
165
|
+
const overlayIndex = overlayItem
|
|
166
|
+
? data.findIndex((item) => item.id === overlayItem.id)
|
|
167
|
+
: -1;
|
|
168
|
+
const totalItems = totalItemsRef.current ?? data.length;
|
|
169
|
+
return (<react_native_reanimated_1.default.View pointerEvents="none" style={overlayStyle}>
|
|
170
|
+
{overlayItem &&
|
|
171
|
+
renderItem({
|
|
172
|
+
item: overlayItem,
|
|
173
|
+
index: overlayIndex,
|
|
174
|
+
totalItems,
|
|
175
|
+
animatedStyle: {},
|
|
176
|
+
dragHandleProps: {
|
|
177
|
+
gesture: disabledGesture,
|
|
178
|
+
isDragging: falseIsDragging,
|
|
179
|
+
},
|
|
180
|
+
isDragging: false,
|
|
181
|
+
isDragEnabled: false,
|
|
182
|
+
triggerExitAnimation: () => { },
|
|
183
|
+
resetExitAnimation: () => { },
|
|
184
|
+
})}
|
|
185
|
+
</react_native_reanimated_1.default.View>);
|
|
186
|
+
});
|
|
187
|
+
const ShiftedItemOverlays = react_1.default.memo(function ShiftedItemOverlays({ data, totalItemsRef, renderItem }) {
|
|
188
|
+
const { shiftedOverlays, shiftedOverlaysActive, shiftedOverlaysRendered } = (0, DragStateContext_1.useDragState)();
|
|
189
|
+
// State to hold overlay items on JS thread
|
|
190
|
+
const [overlayItems, setOverlayItems] = (0, react_1.useState)([]);
|
|
191
|
+
// Callback to update overlay items from JS thread
|
|
192
|
+
// v40.1: Y position is now viewport-relative (captured with scroll offset subtracted)
|
|
193
|
+
const updateOverlayItems = (0, react_1.useCallback)((overlays, active) => {
|
|
194
|
+
if (active && Object.keys(overlays).length > 0) {
|
|
195
|
+
const items = Object.entries(overlays)
|
|
196
|
+
.map(([id, pos]) => ({
|
|
197
|
+
item: data.find((d) => d.id === id),
|
|
198
|
+
position: { y: pos.baseY, height: pos.height },
|
|
199
|
+
}))
|
|
200
|
+
.filter((x) => x.item !== undefined);
|
|
201
|
+
setOverlayItems(items);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
setOverlayItems([]);
|
|
205
|
+
}
|
|
206
|
+
}, [data]);
|
|
207
|
+
// Watch for overlay changes
|
|
208
|
+
(0, react_native_reanimated_1.useAnimatedReaction)(() => ({
|
|
209
|
+
active: shiftedOverlaysActive.value,
|
|
210
|
+
overlays: shiftedOverlays.value,
|
|
211
|
+
}), (state, prev) => {
|
|
212
|
+
"worklet";
|
|
213
|
+
// Only update when state changes
|
|
214
|
+
if (prev !== null &&
|
|
215
|
+
state.active === prev.active &&
|
|
216
|
+
Object.keys(state.overlays).length === Object.keys(prev.overlays).length) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
(0, react_native_worklets_1.scheduleOnRN)(updateOverlayItems, state.overlays, state.active);
|
|
220
|
+
}, [updateOverlayItems]);
|
|
221
|
+
// v41.6: Use useLayoutEffect for earlier signal (runs after commit, before paint)
|
|
222
|
+
// This reduces the timing gap between overlay render and originals being hidden
|
|
223
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
224
|
+
shiftedOverlaysRendered.value = overlayItems.length > 0;
|
|
225
|
+
}, [overlayItems.length, shiftedOverlaysRendered]);
|
|
226
|
+
// Create disabled gesture and false isDragging for overlay rendering
|
|
227
|
+
const disabledGesture = (0, react_1.useMemo)(() => react_native_gesture_handler_1.Gesture.Pan().enabled(false), []);
|
|
228
|
+
const falseIsDragging = (0, react_native_reanimated_1.useSharedValue)(false);
|
|
229
|
+
if (overlayItems.length === 0)
|
|
230
|
+
return null;
|
|
231
|
+
const totalItems = totalItemsRef.current ?? data.length;
|
|
232
|
+
return (<>
|
|
233
|
+
{overlayItems.map(({ item, position }) => (<react_native_1.View key={`shifted-overlay-${item.id}`} style={{
|
|
234
|
+
position: "absolute",
|
|
235
|
+
top: position.y,
|
|
236
|
+
left: 0,
|
|
237
|
+
right: 0,
|
|
238
|
+
height: position.height,
|
|
239
|
+
zIndex: 999,
|
|
240
|
+
}} pointerEvents="none">
|
|
241
|
+
{renderItem({
|
|
242
|
+
item,
|
|
243
|
+
index: data.findIndex((d) => d.id === item.id),
|
|
244
|
+
totalItems,
|
|
245
|
+
animatedStyle: {},
|
|
246
|
+
dragHandleProps: {
|
|
247
|
+
gesture: disabledGesture,
|
|
248
|
+
isDragging: falseIsDragging,
|
|
249
|
+
},
|
|
250
|
+
isDragging: false,
|
|
251
|
+
isDragEnabled: false,
|
|
252
|
+
triggerExitAnimation: () => { },
|
|
253
|
+
resetExitAnimation: () => { },
|
|
254
|
+
})}
|
|
255
|
+
</react_native_1.View>))}
|
|
256
|
+
</>);
|
|
257
|
+
});
|
|
258
|
+
const DragContainer = react_1.default.memo(function DragContainer({ children, }) {
|
|
259
|
+
const { overlayContainerX, overlayContainerY, overlayContainerYLayout, overlayContainerReady, dragContainerRef, } = (0, DragStateContext_1.useDragState)();
|
|
260
|
+
const handleLayout = (0, react_1.useCallback)(() => {
|
|
261
|
+
const node = dragContainerRef.current;
|
|
262
|
+
if (!node)
|
|
263
|
+
return;
|
|
264
|
+
node.measureInWindow((x, y) => {
|
|
265
|
+
overlayContainerX.value = x;
|
|
266
|
+
overlayContainerY.value = y;
|
|
267
|
+
overlayContainerYLayout.value = y; // Keep measureInWindow value for shifted overlay calculations
|
|
268
|
+
overlayContainerReady.value = true;
|
|
269
|
+
});
|
|
270
|
+
}, [
|
|
271
|
+
overlayContainerX,
|
|
272
|
+
overlayContainerY,
|
|
273
|
+
overlayContainerYLayout,
|
|
274
|
+
overlayContainerReady,
|
|
275
|
+
dragContainerRef,
|
|
276
|
+
]);
|
|
277
|
+
return (<react_native_reanimated_1.default.View ref={dragContainerRef} style={containerStyle} onLayout={handleLayout}>
|
|
278
|
+
{children}
|
|
279
|
+
</react_native_reanimated_1.default.View>);
|
|
280
|
+
});
|
|
61
281
|
const ItemWrapper = react_1.default.memo(function ItemWrapper({ item, index, totalItemsRef, renderItem, canDrag, dragEnabled, onReorderByDelta, onHapticFeedback, }) {
|
|
62
282
|
const isDragEnabled = dragEnabled && (canDrag ? canDrag(item, index) : true);
|
|
63
283
|
return (<AnimatedFlashListItem_1.AnimatedFlashListItem item={item} index={index} totalItems={totalItemsRef.current ?? 0} isDragEnabled={isDragEnabled} renderItem={renderItem} onReorderByDelta={onReorderByDelta} onHapticFeedback={onHapticFeedback}/>);
|
|
64
284
|
});
|
|
65
285
|
function InnerFlashList({ data, totalItemsRef, flashListRef, renderItem, keyExtractor, canDrag, dragEnabled, onReorderByDelta, onHapticFeedback, itemHeight, ListFooterComponent, ListEmptyComponent, onEndReached, onEndReachedThreshold, onRefresh, refreshing, refreshTintColor, contentContainerStyle, drawDistance = 500, showsVerticalScrollIndicator = true, extraData, onCommitLayoutEffect, }) {
|
|
66
286
|
// Get drag state context for scroll tracking
|
|
67
|
-
const { scrollOffset, contentHeight, visibleHeight, listTopY, setListRef, totalItems, notifyLayoutCommit, } = (0,
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}, [
|
|
287
|
+
const { scrollOffset, contentHeight, visibleHeight, listTopY, listLeftX, setListRef, totalItems, notifyLayoutCommit, isDragging, isDropping, itemIndexRegistry, } = (0, DragStateContext_1.useDragState)();
|
|
288
|
+
// v40.1: Disable scroll while dropping to prevent overlay drift
|
|
289
|
+
// FlashList needs scrollEnabled as a regular boolean, not a SharedValue
|
|
290
|
+
const [scrollEnabled, setScrollEnabled] = (0, react_1.useState)(true);
|
|
291
|
+
const setScrollEnabledSafe = (0, react_1.useCallback)((enabled) => {
|
|
292
|
+
setScrollEnabled(enabled);
|
|
293
|
+
}, []);
|
|
294
|
+
(0, react_native_reanimated_1.useAnimatedReaction)(() => isDragging.value || isDropping.value, (draggingOrDropping, prevDraggingOrDropping) => {
|
|
295
|
+
"worklet";
|
|
296
|
+
if (draggingOrDropping !== prevDraggingOrDropping) {
|
|
297
|
+
(0, react_native_worklets_1.scheduleOnRN)(setScrollEnabledSafe, !draggingOrDropping);
|
|
298
|
+
}
|
|
299
|
+
}, [setScrollEnabledSafe]);
|
|
300
|
+
// Callback ref that notifies DragStateContext when FlashList is available.
|
|
301
|
+
// This fires immediately when FlashList sets the ref, unlike useEffect which
|
|
302
|
+
// would run after render with flashListRef.current potentially still null.
|
|
303
|
+
const handleFlashListRef = (0, react_1.useCallback)((instance) => {
|
|
304
|
+
// Update the ref object for other uses (scrollToOffset, prepareForLayoutAnimationRender, etc.)
|
|
305
|
+
flashListRef.current =
|
|
306
|
+
instance;
|
|
307
|
+
// Notify DragStateContext - this triggers native layout observation setup
|
|
308
|
+
setListRef(instance);
|
|
309
|
+
}, [flashListRef, setListRef]);
|
|
74
310
|
// Sync totalItems SharedValue with data.length for worklet access
|
|
75
311
|
(0, react_1.useEffect)(() => {
|
|
76
312
|
totalItems.value = data.length;
|
|
77
313
|
}, [data.length, totalItems]);
|
|
314
|
+
// Prune stale entries from itemIndexRegistry when data changes.
|
|
315
|
+
// Skip during active drag/drop to avoid corrupting indices mid-operation.
|
|
316
|
+
(0, react_1.useEffect)(() => {
|
|
317
|
+
if (isDragging.value || isDropping.value)
|
|
318
|
+
return;
|
|
319
|
+
const currentIds = new Set(data.map((item) => item.id));
|
|
320
|
+
const registry = itemIndexRegistry.value;
|
|
321
|
+
const staleIds = Object.keys(registry).filter((id) => !currentIds.has(id));
|
|
322
|
+
if (staleIds.length > 0) {
|
|
323
|
+
const pruned = { ...registry };
|
|
324
|
+
for (const id of staleIds) {
|
|
325
|
+
delete pruned[id];
|
|
326
|
+
}
|
|
327
|
+
itemIndexRegistry.value = pruned;
|
|
328
|
+
}
|
|
329
|
+
}, [data, itemIndexRegistry, isDragging, isDropping]);
|
|
78
330
|
// Update scroll offset on scroll
|
|
79
331
|
const handleScroll = (0, react_1.useCallback)((event) => {
|
|
80
332
|
scrollOffset.value = event.nativeEvent.contentOffset.y;
|
|
@@ -89,14 +341,15 @@ function InnerFlashList({ data, totalItemsRef, flashListRef, renderItem, keyExtr
|
|
|
89
341
|
// Get native scroll ref and validate it has measureInWindow before using
|
|
90
342
|
const nativeRef = flashListRef.current?.getNativeScrollRef?.();
|
|
91
343
|
if (nativeRef &&
|
|
92
|
-
typeof nativeRef ===
|
|
93
|
-
|
|
94
|
-
typeof nativeRef.measureInWindow ===
|
|
95
|
-
nativeRef.measureInWindow((
|
|
344
|
+
typeof nativeRef === "object" &&
|
|
345
|
+
"measureInWindow" in nativeRef &&
|
|
346
|
+
typeof nativeRef.measureInWindow === "function") {
|
|
347
|
+
nativeRef.measureInWindow((x, y) => {
|
|
96
348
|
listTopY.value = y;
|
|
349
|
+
listLeftX.value = x;
|
|
97
350
|
});
|
|
98
351
|
}
|
|
99
|
-
}, [visibleHeight, listTopY, flashListRef]);
|
|
352
|
+
}, [visibleHeight, listTopY, listLeftX, flashListRef]);
|
|
100
353
|
const handleCommitLayoutEffect = (0, react_1.useCallback)(() => {
|
|
101
354
|
notifyLayoutCommit();
|
|
102
355
|
onCommitLayoutEffect?.();
|
|
@@ -111,14 +364,14 @@ function InnerFlashList({ data, totalItemsRef, flashListRef, renderItem, keyExtr
|
|
|
111
364
|
onHapticFeedback,
|
|
112
365
|
]);
|
|
113
366
|
// getItemType for FlashList recycling optimization
|
|
114
|
-
const getItemType = (0, react_1.useCallback)(() =>
|
|
367
|
+
const getItemType = (0, react_1.useCallback)(() => "animated-item", []);
|
|
115
368
|
// Override item layout for consistent drag calculations
|
|
116
369
|
// Note: We cast the layout to include size for drag calculations
|
|
117
370
|
const overrideItemLayout = (0, react_1.useCallback)((layout, _item, _index) => {
|
|
118
371
|
// FlashList v2 uses this for span, but we extend for size in drag calculations
|
|
119
372
|
layout.size = itemHeight;
|
|
120
373
|
}, [itemHeight]);
|
|
121
|
-
return (<flash_list_1.FlashList ref={
|
|
374
|
+
return (<flash_list_1.FlashList ref={handleFlashListRef} data={data} renderItem={flashListRenderItem} keyExtractor={keyExtractor} getItemType={getItemType} overrideItemLayout={overrideItemLayout} onScroll={handleScroll} onContentSizeChange={handleContentSizeChange} onLayout={handleLayout} scrollEventThrottle={16} drawDistance={drawDistance} maintainVisibleContentPosition={{ disabled: true }} showsVerticalScrollIndicator={showsVerticalScrollIndicator} onCommitLayoutEffect={handleCommitLayoutEffect} extraData={extraData} contentContainerStyle={contentContainerStyle} ListFooterComponent={ListFooterComponent ?? undefined} ListEmptyComponent={ListEmptyComponent ?? undefined} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} scrollEnabled={scrollEnabled} refreshControl={onRefresh ? (<react_native_1.RefreshControl refreshing={refreshing ?? false} onRefresh={onRefresh} tintColor={refreshTintColor} colors={refreshTintColor ? [refreshTintColor] : undefined}/>) : undefined}/>);
|
|
122
375
|
}
|
|
123
376
|
/**
|
|
124
377
|
* AnimatedFlashList - High-performance animated list with drag-to-reorder
|
|
@@ -150,15 +403,15 @@ function InnerFlashList({ data, totalItemsRef, flashListRef, renderItem, keyExtr
|
|
|
150
403
|
* ```
|
|
151
404
|
*/
|
|
152
405
|
function AnimatedFlashListInner(props, ref) {
|
|
153
|
-
const { data, keyExtractor, renderItem, dragEnabled = false, onReorder, onReorderByNeighbors, canDrag, onHapticFeedback, config, onPrepareLayoutAnimation, ListFooterComponent, ListEmptyComponent, onRefresh, refreshing = false, onEndReached, onEndReachedThreshold = 0.5, contentContainerStyle,
|
|
406
|
+
const { data, keyExtractor, renderItem, dragEnabled = false, onReorder, onReorderByNeighbors, canDrag, onHapticFeedback, config, onPrepareLayoutAnimation, ListFooterComponent, ListEmptyComponent, onRefresh, refreshing = false, onEndReached, onEndReachedThreshold = 0.5, contentContainerStyle, ...flashListProps } = props;
|
|
154
407
|
const { extraData: userExtraData, onCommitLayoutEffect: userOnCommitLayoutEffect, ...restFlashListProps } = flashListProps;
|
|
155
408
|
// Merge config with defaults
|
|
156
|
-
//
|
|
409
|
+
// Note: estimatedItemSize was removed in FlashList v2
|
|
410
|
+
// Users should configure itemHeight via config.drag.itemHeight
|
|
157
411
|
const dragConfig = (0, react_1.useMemo)(() => ({
|
|
158
|
-
...
|
|
412
|
+
...drag_1.DEFAULT_DRAG_CONFIG,
|
|
159
413
|
...config?.drag,
|
|
160
|
-
|
|
161
|
-
}), [config?.drag, estimatedItemSize]);
|
|
414
|
+
}), [config?.drag]);
|
|
162
415
|
// Ref to FlashList
|
|
163
416
|
const flashListRef = (0, react_1.useRef)(null);
|
|
164
417
|
// Expose methods to parent via ref
|
|
@@ -184,11 +437,12 @@ function AnimatedFlashListInner(props, ref) {
|
|
|
184
437
|
setLayoutVersion((version) => version + 1);
|
|
185
438
|
}, []);
|
|
186
439
|
// Handle reorder by index delta - converts to various callback formats
|
|
440
|
+
// NOTE: No LayoutAnimation needed - layout commit compensation handles visual transitions
|
|
187
441
|
const handleReorderByDelta = (0, react_1.useCallback)((itemId, indexDelta) => {
|
|
188
442
|
if (indexDelta === 0)
|
|
189
443
|
return;
|
|
190
444
|
const currentItems = dataRef.current;
|
|
191
|
-
const currentIndex = currentItems.findIndex(item => item.id === itemId);
|
|
445
|
+
const currentIndex = currentItems.findIndex((item) => item.id === itemId);
|
|
192
446
|
if (currentIndex === -1)
|
|
193
447
|
return;
|
|
194
448
|
const newIndex = Math.max(0, Math.min(currentItems.length - 1, currentIndex + indexDelta));
|
|
@@ -206,12 +460,12 @@ function AnimatedFlashListInner(props, ref) {
|
|
|
206
460
|
afterItemId = currentItems[newIndex]?.id ?? null;
|
|
207
461
|
beforeItemId =
|
|
208
462
|
newIndex < currentItems.length - 1
|
|
209
|
-
? currentItems[newIndex + 1]?.id ?? null
|
|
463
|
+
? (currentItems[newIndex + 1]?.id ?? null)
|
|
210
464
|
: null;
|
|
211
465
|
}
|
|
212
466
|
else {
|
|
213
467
|
afterItemId =
|
|
214
|
-
newIndex > 0 ? currentItems[newIndex - 1]?.id ?? null : null;
|
|
468
|
+
newIndex > 0 ? (currentItems[newIndex - 1]?.id ?? null) : null;
|
|
215
469
|
beforeItemId = currentItems[newIndex]?.id ?? null;
|
|
216
470
|
}
|
|
217
471
|
onReorderByNeighbors(itemId, afterItemId, beforeItemId);
|
|
@@ -256,13 +510,17 @@ function AnimatedFlashListInner(props, ref) {
|
|
|
256
510
|
})} keyExtractor={keyExtractor} contentContainerStyle={contentContainerStyle} ListFooterComponent={ListFooterComponent ?? undefined} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold}/>
|
|
257
511
|
</react_native_1.View>);
|
|
258
512
|
return (<AnimationErrorBoundary fallback={plainFlashListFallback}>
|
|
259
|
-
<
|
|
260
|
-
<
|
|
261
|
-
<
|
|
262
|
-
<InnerFlashList {...restFlashListProps} data={data} totalItemsRef={totalItemsRef} flashListRef={flashListRef} renderItem={renderItem} keyExtractor={keyExtractor} canDrag={canDrag} dragEnabled={dragEnabled} onReorderByDelta={onReorder || onReorderByNeighbors
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
513
|
+
<ListAnimationContext_1.ListAnimationProvider>
|
|
514
|
+
<DragStateContext_1.DragStateProvider config={dragConfig}>
|
|
515
|
+
<DragContainer>
|
|
516
|
+
<InnerFlashList {...restFlashListProps} data={data} totalItemsRef={totalItemsRef} flashListRef={flashListRef} renderItem={renderItem} keyExtractor={keyExtractor} canDrag={canDrag} dragEnabled={dragEnabled} onReorderByDelta={onReorder || onReorderByNeighbors
|
|
517
|
+
? handleReorderByDelta
|
|
518
|
+
: undefined} onHapticFeedback={onHapticFeedback} itemHeight={dragConfig.itemHeight} ListFooterComponent={ListFooterComponent} ListEmptyComponent={ListEmptyComponent} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} onRefresh={onRefresh} refreshing={refreshing} contentContainerStyle={contentContainerStyle} extraData={combinedExtraData} onCommitLayoutEffect={userOnCommitLayoutEffect}/>
|
|
519
|
+
<DragOverlay data={data} totalItemsRef={totalItemsRef} renderItem={renderItem}/>
|
|
520
|
+
<ShiftedItemOverlays data={data} totalItemsRef={totalItemsRef} renderItem={renderItem}/>
|
|
521
|
+
</DragContainer>
|
|
522
|
+
</DragStateContext_1.DragStateProvider>
|
|
523
|
+
</ListAnimationContext_1.ListAnimationProvider>
|
|
266
524
|
</AnimationErrorBoundary>);
|
|
267
525
|
}
|
|
268
526
|
const containerStyle = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnimatedFlashListItem.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashListItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"AnimatedFlashListItem.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashListItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAWxF,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAEjB,UAAU,0BAA0B,CAAC,CAAC,SAAS,gBAAgB;IAC7D,oBAAoB;IACpB,IAAI,EAAE,CAAC,CAAC;IACR,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,aAAa,EAAE,OAAO,CAAC;IACvB,kCAAkC;IAClC,UAAU,EAAE,CAAC,IAAI,EAAE,sBAAsB,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,YAAY,CAAC;IACpE,mCAAmC;IACnC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;CACvD;AAED;;;;;;;;;;GAUG;AACH,iBAAS,0BAA0B,CAAC,CAAC,SAAS,gBAAgB,EAAE,EAC9D,IAAI,EACJ,KAAK,EACL,UAAU,EACV,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,gBAAgB,GACjB,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAwP3D;AAGD,eAAO,MAAM,qBAAqB,EAE7B,OAAO,0BAA0B,CAAC"}
|
|
@@ -36,12 +36,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.AnimatedFlashListItem = void 0;
|
|
37
37
|
const react_1 = __importStar(require("react"));
|
|
38
38
|
const react_native_reanimated_1 = __importStar(require("react-native-reanimated"));
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
const useDragGesture_1 = require("./hooks/drag/useDragGesture");
|
|
40
|
+
const useDragShift_1 = require("./hooks/drag/useDragShift");
|
|
41
|
+
const useDragAnimatedStyle_1 = require("./hooks/drag/useDragAnimatedStyle");
|
|
42
|
+
const useDropCompensation_1 = require("./hooks/drag/useDropCompensation");
|
|
43
|
+
const useListExitAnimation_1 = require("./hooks/animations/useListExitAnimation");
|
|
44
|
+
const useListEntryAnimation_1 = require("./hooks/animations/useListEntryAnimation");
|
|
45
|
+
const ListAnimationContext_1 = require("./contexts/ListAnimationContext");
|
|
46
|
+
const DragStateContext_1 = require("./contexts/DragStateContext");
|
|
45
47
|
/**
|
|
46
48
|
* Internal item wrapper that provides all animation functionality.
|
|
47
49
|
*
|
|
@@ -54,41 +56,17 @@ let instanceCounter = 0;
|
|
|
54
56
|
* @internal
|
|
55
57
|
*/
|
|
56
58
|
function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, renderItem, onReorderByDelta, onHapticFeedback, }) {
|
|
57
|
-
// Debug: Track this component instance (only incremented in DEV)
|
|
58
|
-
const instanceIdRef = (0, react_1.useRef)(null);
|
|
59
|
-
if (__DEV__ && instanceIdRef.current === null) {
|
|
60
|
-
instanceIdRef.current = ++instanceCounter;
|
|
61
|
-
}
|
|
62
|
-
const instanceId = instanceIdRef.current;
|
|
63
|
-
// Track previous itemId to detect recycling
|
|
64
|
-
const prevItemIdRef = (0, react_1.useRef)(item.id);
|
|
65
|
-
// Log every render with instance tracking (verbose only - fires per render per item)
|
|
66
|
-
if (__DEV__ && constants_1.VERBOSE_DRAG_DEBUG) {
|
|
67
|
-
const isRecycled = prevItemIdRef.current !== item.id;
|
|
68
|
-
console.log(`[FlashListItem:inst${instanceId}] RENDER: itemId=${item.id}, index=${index}` +
|
|
69
|
-
(isRecycled ? ` (RECYCLED from ${prevItemIdRef.current})` : ''));
|
|
70
|
-
prevItemIdRef.current = item.id;
|
|
71
|
-
}
|
|
72
|
-
// Track mount/unmount
|
|
73
|
-
(0, react_1.useEffect)(() => {
|
|
74
|
-
if (__DEV__) {
|
|
75
|
-
console.log(`[FlashListItem:inst${instanceId}] MOUNT: itemId=${item.id}, index=${index}`);
|
|
76
|
-
}
|
|
77
|
-
return () => {
|
|
78
|
-
if (__DEV__) {
|
|
79
|
-
console.log(`[FlashListItem:inst${instanceId}] UNMOUNT: was itemId=${item.id}`);
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
|
-
}, []); // Empty deps - only track actual mount/unmount
|
|
84
59
|
// Create shared value for index (for UI-thread access in animations)
|
|
85
60
|
// This allows worklets to read the current index without closure stale capture
|
|
86
61
|
const indexShared = (0, react_native_reanimated_1.useSharedValue)(index);
|
|
87
|
-
const layoutSignal = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
88
62
|
// Register this item's index in the central registry
|
|
89
63
|
// This handles FlashList's recycling behavior where components may not re-render
|
|
90
64
|
// after data changes, leaving their index props stale
|
|
91
|
-
const { updateItemIndex, isDragging: globalIsDragging, isDropping
|
|
65
|
+
const { updateItemIndex, isDragging: globalIsDragging, isDropping, draggedItemId,
|
|
66
|
+
// v40.7: For shifted item overlay hiding
|
|
67
|
+
shiftedOverlaysActive, shiftedOverlays, shiftedOverlaysRendered, // v41.4: Wait for React render before hiding
|
|
68
|
+
// Crossfade support
|
|
69
|
+
dragOverlayOpacity, overlayFrozenItemId, overlayRenderedOnUI, } = (0, DragStateContext_1.useDragState)();
|
|
92
70
|
// Sync index to SharedValue on every render, but skip during active drag/drop
|
|
93
71
|
// This prevents stale React props from corrupting the index during the drop phase
|
|
94
72
|
// when FlashList re-renders with new data order
|
|
@@ -112,9 +90,6 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
|
|
|
112
90
|
(0, react_1.useEffect)(() => {
|
|
113
91
|
if (globalIsDragging.value || isDropping.value || indexShared.value === index)
|
|
114
92
|
return;
|
|
115
|
-
if (__DEV__ && constants_1.VERBOSE_DRAG_DEBUG) {
|
|
116
|
-
console.log(`[FlashListItem:${item.id}] Secondary sync: indexShared ${indexShared.value} → ${index}`);
|
|
117
|
-
}
|
|
118
93
|
indexShared.value = index;
|
|
119
94
|
});
|
|
120
95
|
// Update item index in useLayoutEffect to avoid side effects during render
|
|
@@ -128,11 +103,11 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
|
|
|
128
103
|
// Track measured height for layout compensation
|
|
129
104
|
const measuredHeightRef = (0, react_1.useRef)(0);
|
|
130
105
|
// List animation context for subscription-triggered animations and layout compensation
|
|
131
|
-
const animationContext = (0,
|
|
106
|
+
const animationContext = (0, ListAnimationContext_1.useListAnimationOptional)();
|
|
132
107
|
// === DRAG HOOKS ===
|
|
133
108
|
// Pan gesture for drag-to-reorder
|
|
134
109
|
// Note: totalItems is now accessed via SharedValue from DragStateContext
|
|
135
|
-
const { panGesture, isDragging, translateY } = (0,
|
|
110
|
+
const { panGesture, isDragging, translateY } = (0, useDragGesture_1.useDragGesture)({
|
|
136
111
|
itemId: item.id,
|
|
137
112
|
index,
|
|
138
113
|
enabled: isDragEnabled,
|
|
@@ -142,11 +117,46 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
|
|
|
142
117
|
onHapticFeedback,
|
|
143
118
|
});
|
|
144
119
|
// Shift animation for non-dragged items
|
|
145
|
-
const { shiftY } = (0,
|
|
120
|
+
const { shiftY } = (0, useDragShift_1.useDragShift)({ itemId: item.id, index });
|
|
146
121
|
// Handle index changes after cache updates (drop compensation)
|
|
147
|
-
(0,
|
|
122
|
+
(0, useDropCompensation_1.useDropCompensation)({ itemId: item.id, index, translateY });
|
|
148
123
|
// Animated style for drag transforms
|
|
149
|
-
const { dragAnimatedStyle } = (0,
|
|
124
|
+
const { dragAnimatedStyle } = (0, useDragAnimatedStyle_1.useDragAnimatedStyle)(item.id, isDragging, translateY, shiftY, containerRef);
|
|
125
|
+
const hideOriginalStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => {
|
|
126
|
+
// Crossfade visibility: use inverse of overlay opacity for smooth transition
|
|
127
|
+
// This ensures the original and overlay opacity always sum to 1, preventing
|
|
128
|
+
// any frame where both are invisible (the "vanish" bug).
|
|
129
|
+
const isThisDragged = draggedItemId.value === item.id;
|
|
130
|
+
const isThisFrozenOverlay = overlayFrozenItemId.value === item.id;
|
|
131
|
+
// Crossfade path: applies during drag/drop OR while frozen overlay fades out
|
|
132
|
+
if (isThisDragged || isThisFrozenOverlay) {
|
|
133
|
+
// During drag/drop or while overlay is fading out, crossfade with overlay
|
|
134
|
+
if (globalIsDragging.value || isDropping.value || overlayFrozenItemId.value === item.id) {
|
|
135
|
+
// FIX: Don't start fading out until overlay has rendered ON THE UI THREAD.
|
|
136
|
+
// overlayRenderedOnUI is set from useAnimatedStyle (UI thread) in DragOverlay,
|
|
137
|
+
// unlike overlayVisible which is set from useEffect (JS thread) causing races.
|
|
138
|
+
// Without this guard, the original fades out before overlay appears = vanish.
|
|
139
|
+
if (!overlayRenderedOnUI.value) {
|
|
140
|
+
return { opacity: 1 }; // Stay fully visible until overlay renders
|
|
141
|
+
}
|
|
142
|
+
// Always use crossfade - when overlay is at opacity X,
|
|
143
|
+
// the original is at opacity (1-X), so they always sum to 1.
|
|
144
|
+
return { opacity: 1 - dragOverlayOpacity.value };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// v41.4: Hide shifted items during drop ONLY after overlays have rendered.
|
|
148
|
+
// Previously (v41.2) we hid immediately when shiftedOverlaysActive=true,
|
|
149
|
+
// but overlays take ~2-5ms to render (React async), causing a blank flash.
|
|
150
|
+
// The shift values are FROZEN during drop (v41.1), so originals stay correctly
|
|
151
|
+
// positioned until overlays are ready.
|
|
152
|
+
if (isDropping.value &&
|
|
153
|
+
shiftedOverlaysActive.value &&
|
|
154
|
+
shiftedOverlaysRendered.value && // Wait for React render
|
|
155
|
+
shiftedOverlays.value[item.id] !== undefined) {
|
|
156
|
+
return { opacity: 0 };
|
|
157
|
+
}
|
|
158
|
+
return { opacity: 1 };
|
|
159
|
+
});
|
|
150
160
|
// === ANIMATION HOOKS ===
|
|
151
161
|
// Callbacks for layout compensation (register/unregister exiting items)
|
|
152
162
|
const onExitStart = (0, react_1.useCallback)((exitIndex, height) => {
|
|
@@ -156,14 +166,14 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
|
|
|
156
166
|
animationContext?.unregisterExitingItem(item.id);
|
|
157
167
|
}, [animationContext, item.id]);
|
|
158
168
|
// Exit animation for smooth slide-out (with layout compensation callbacks)
|
|
159
|
-
const { exitAnimatedStyle, triggerExit, resetAnimation } = (0,
|
|
169
|
+
const { exitAnimatedStyle, triggerExit, resetAnimation } = (0, useListExitAnimation_1.useListExitAnimation)(item.id, {
|
|
160
170
|
index,
|
|
161
171
|
measuredHeight: measuredHeightRef.current,
|
|
162
172
|
onExitStart,
|
|
163
173
|
onExitComplete,
|
|
164
174
|
});
|
|
165
175
|
// Entry animation for items appearing
|
|
166
|
-
const { entryAnimatedStyle } = (0,
|
|
176
|
+
const { entryAnimatedStyle } = (0, useListEntryAnimation_1.useListEntryAnimation)(item.id);
|
|
167
177
|
// Register exit animation trigger (O(1) direct calls from subscriptions)
|
|
168
178
|
(0, react_1.useLayoutEffect)(() => {
|
|
169
179
|
if (!animationContext)
|
|
@@ -173,9 +183,9 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
|
|
|
173
183
|
}, [item.id, triggerExit, animationContext]);
|
|
174
184
|
// Track measured height for layout compensation
|
|
175
185
|
const handleLayout = (0, react_1.useCallback)((event) => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}, [
|
|
186
|
+
const { height } = event.nativeEvent.layout;
|
|
187
|
+
measuredHeightRef.current = height;
|
|
188
|
+
}, []);
|
|
179
189
|
// Create drag handle props
|
|
180
190
|
const dragHandleProps = (0, react_1.useMemo)(() => isDragEnabled
|
|
181
191
|
? {
|
|
@@ -215,6 +225,7 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
|
|
|
215
225
|
return (<react_native_reanimated_1.default.View ref={containerRef} onLayout={handleLayout} style={[
|
|
216
226
|
exitAnimatedStyle,
|
|
217
227
|
entryAnimatedStyle,
|
|
228
|
+
hideOriginalStyle,
|
|
218
229
|
isDragEnabled && dragAnimatedStyle,
|
|
219
230
|
]}>
|
|
220
231
|
{renderedItem}
|