@souscheflabs/reanimated-flashlist 0.2.8 → 0.3.1
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/contexts/DragStateContext.d.ts +6 -4
- package/lib/contexts/DragStateContext.d.ts.map +1 -1
- package/lib/contexts/DragStateContext.js +26 -12
- package/lib/hooks/drag/useDragAnimatedStyle.d.ts +2 -2
- package/lib/hooks/drag/useDragAnimatedStyle.d.ts.map +1 -1
- package/lib/hooks/drag/useDragAnimatedStyle.js +7 -21
- package/lib/hooks/drag/useDragGesture.d.ts +1 -1
- package/lib/hooks/drag/useDragGesture.d.ts.map +1 -1
- package/lib/hooks/drag/useDragGesture.js +59 -81
- package/lib/hooks/drag/useDragShift.d.ts +1 -1
- package/lib/hooks/drag/useDragShift.d.ts.map +1 -1
- package/lib/hooks/drag/useDragShift.js +21 -12
- package/lib/hooks/drag/useDropCompensation.d.ts +1 -1
- package/lib/hooks/drag/useDropCompensation.d.ts.map +1 -1
- package/lib/hooks/drag/useDropCompensation.js +29 -33
- package/package.json +1 -1
- package/src/contexts/DragStateContext.tsx +53 -19
- package/src/hooks/drag/useDragAnimatedStyle.ts +21 -30
- package/src/hooks/drag/useDragGesture.ts +86 -102
- package/src/hooks/drag/useDragShift.ts +35 -21
- package/src/hooks/drag/useDropCompensation.ts +44 -44
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import React, { type ReactNode } from
|
|
2
|
-
import { type SharedValue } from
|
|
3
|
-
import type { FlashListRef } from
|
|
4
|
-
import type { DragConfig } from
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import { type SharedValue } from "react-native-reanimated";
|
|
3
|
+
import type { FlashListRef } from "@shopify/flash-list";
|
|
4
|
+
import type { DragConfig } from "../types";
|
|
5
5
|
/**
|
|
6
6
|
* Centralized drag state for coordinating animations across list items.
|
|
7
7
|
* All animation values are Reanimated SharedValues for 60fps UI thread performance.
|
|
@@ -43,6 +43,8 @@ export interface DragStateContextValue {
|
|
|
43
43
|
setListRef: (ref: FlashListRef<unknown> | null) => void;
|
|
44
44
|
/** Scroll the list to a specific offset (for autoscroll during drag) */
|
|
45
45
|
scrollToOffset: (offset: number, animated?: boolean) => void;
|
|
46
|
+
/** Ask FlashList to prepare cells for layout animation on the next render */
|
|
47
|
+
prepareForLayoutAnimationRender: () => void;
|
|
46
48
|
/** Reset drag state after drop animation completes */
|
|
47
49
|
resetDragState: () => void;
|
|
48
50
|
/** Current drag configuration */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DragStateContext.d.ts","sourceRoot":"","sources":["../../src/contexts/DragStateContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"DragStateContext.d.ts","sourceRoot":"","sources":["../../src/contexts/DragStateContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAgC3C;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC,2CAA2C;IAC3C,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,oEAAoE;IACpE,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,oFAAoF;IACpF,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,iFAAiF;IACjF,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,sEAAsE;IACtE,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,+DAA+D;IAC/D,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,uFAAuF;IACvF,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3C,yEAAyE;IACzE,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,qDAAqD;IACrD,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,mFAAmF;IACnF,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B,4EAA4E;IAC5E,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACxC,yDAAyD;IACzD,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,gFAAgF;IAChF,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,2DAA2D;IAC3D,UAAU,EAAE,CAAC,GAAG,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IACxD,wEAAwE;IACxE,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7D,6EAA6E;IAC7E,+BAA+B,EAAE,MAAM,IAAI,CAAC;IAC5C,sDAAsD;IACtD,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,iCAAiC;IACjC,MAAM,EAAE,UAAU,CAAC;IACnB;;;;OAIG;IACH,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,mEAAmE;IACnE,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,4CAA4C;IAC5C,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACrD;;;;OAIG;IACH,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5D;;;OAGG;IACH,uBAAuB,EAAE,MAAM,IAAI,CAAC;IACpC;;;OAGG;IACH,sBAAsB,EAAE,CACtB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,KACZ,IAAI,CAAC;IACV;;;OAGG;IACH,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACjC;AAID;;;GAGG;AACH,eAAO,MAAM,YAAY,QAAO,qBAM/B,CAAC;AAEF,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,SAAS,CAAC;IACpB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;CAC9B;AAmDD,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAyO9D,CAAC"}
|
|
@@ -44,12 +44,21 @@ const constants_1 = require("../constants");
|
|
|
44
44
|
* visible to other worklets (like onStart in useDragGesture).
|
|
45
45
|
*/
|
|
46
46
|
const updateRegistryOnUI = (registry, itemId, index) => {
|
|
47
|
-
|
|
47
|
+
"worklet";
|
|
48
48
|
const existingIndex = registry.value[itemId];
|
|
49
49
|
if (existingIndex === undefined || existingIndex !== index) {
|
|
50
50
|
registry.value = { ...registry.value, [itemId]: index };
|
|
51
51
|
}
|
|
52
52
|
};
|
|
53
|
+
// In tests or non-worklet environments, scheduleOnUI may be undefined. Provide a safe fallback.
|
|
54
|
+
const scheduleOnUIOrImmediate = (fn, ...args) => {
|
|
55
|
+
if (typeof react_native_worklets_1.scheduleOnUI === "function") {
|
|
56
|
+
(0, react_native_worklets_1.scheduleOnUI)(fn, ...args);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
fn(...args);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
53
62
|
const DragStateContext = (0, react_1.createContext)(null);
|
|
54
63
|
/**
|
|
55
64
|
* Hook to access shared drag state from context.
|
|
@@ -58,7 +67,7 @@ const DragStateContext = (0, react_1.createContext)(null);
|
|
|
58
67
|
const useDragState = () => {
|
|
59
68
|
const context = (0, react_1.useContext)(DragStateContext);
|
|
60
69
|
if (!context) {
|
|
61
|
-
throw new Error(
|
|
70
|
+
throw new Error("useDragState must be used within DragStateProvider");
|
|
62
71
|
}
|
|
63
72
|
return context;
|
|
64
73
|
};
|
|
@@ -80,19 +89,19 @@ exports.useDragState = useDragState;
|
|
|
80
89
|
function validateConfig(config) {
|
|
81
90
|
if (__DEV__) {
|
|
82
91
|
if (config.itemHeight <= 0) {
|
|
83
|
-
console.warn(
|
|
92
|
+
console.warn("[AnimatedFlashList] Invalid itemHeight: must be positive. Got:", config.itemHeight);
|
|
84
93
|
}
|
|
85
94
|
if (config.longPressDuration <= 0) {
|
|
86
|
-
console.warn(
|
|
95
|
+
console.warn("[AnimatedFlashList] Invalid longPressDuration: must be positive. Got:", config.longPressDuration);
|
|
87
96
|
}
|
|
88
97
|
if (config.dragScale <= 0) {
|
|
89
|
-
console.warn(
|
|
98
|
+
console.warn("[AnimatedFlashList] Invalid dragScale: must be positive. Got:", config.dragScale);
|
|
90
99
|
}
|
|
91
100
|
if (config.edgeThreshold < 0) {
|
|
92
|
-
console.warn(
|
|
101
|
+
console.warn("[AnimatedFlashList] Invalid edgeThreshold: must be non-negative. Got:", config.edgeThreshold);
|
|
93
102
|
}
|
|
94
103
|
if (config.maxScrollSpeed <= 0) {
|
|
95
|
-
console.warn(
|
|
104
|
+
console.warn("[AnimatedFlashList] Invalid maxScrollSpeed: must be positive. Got:", config.maxScrollSpeed);
|
|
96
105
|
}
|
|
97
106
|
}
|
|
98
107
|
}
|
|
@@ -106,7 +115,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
106
115
|
// Shared values are created once and persist for the lifetime of the provider
|
|
107
116
|
const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
|
|
108
117
|
const draggedIndex = (0, react_native_reanimated_1.useSharedValue)(-1);
|
|
109
|
-
const draggedItemId = (0, react_native_reanimated_1.useSharedValue)(
|
|
118
|
+
const draggedItemId = (0, react_native_reanimated_1.useSharedValue)("");
|
|
110
119
|
const currentTranslateY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
111
120
|
const draggedScale = (0, react_native_reanimated_1.useSharedValue)(1);
|
|
112
121
|
// Scroll state for viewport-aware calculations and autoscroll
|
|
@@ -139,6 +148,10 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
139
148
|
const scrollToOffset = (0, react_1.useCallback)((offset, animated = false) => {
|
|
140
149
|
listRef.current?.scrollToOffset({ offset, animated });
|
|
141
150
|
}, []);
|
|
151
|
+
// Request FlashList to prepare cells for layout animation on next render
|
|
152
|
+
const prepareForLayoutAnimationRender = (0, react_1.useCallback)(() => {
|
|
153
|
+
listRef.current?.prepareForLayoutAnimationRender?.();
|
|
154
|
+
}, []);
|
|
142
155
|
// Update an item's index in the registry
|
|
143
156
|
// Uses scheduleOnUI to ensure SharedValue modifications are immediately visible
|
|
144
157
|
// to UI thread worklets (fixing the registry sync bug).
|
|
@@ -149,9 +162,9 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
149
162
|
const existingIndex = itemIndexRegistry.value[itemId];
|
|
150
163
|
const timeSinceUpdate = Date.now() - registryUpdateTimestamp.current;
|
|
151
164
|
const isProtected = timeSinceUpdate < 500; // 500ms protection window
|
|
152
|
-
// Add new items always - schedule on UI thread for immediate visibility
|
|
165
|
+
// Add new items always - schedule on UI thread for immediate visibility (or synchronous fallback in tests)
|
|
153
166
|
if (existingIndex === undefined) {
|
|
154
|
-
(
|
|
167
|
+
scheduleOnUIOrImmediate(updateRegistryOnUI, itemIndexRegistry, itemId, index);
|
|
155
168
|
return;
|
|
156
169
|
}
|
|
157
170
|
// Skip update if within protection window (onEnd just updated the registry)
|
|
@@ -161,7 +174,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
161
174
|
}
|
|
162
175
|
// Outside protection window, update if value changed - schedule on UI thread
|
|
163
176
|
if (existingIndex !== index) {
|
|
164
|
-
(
|
|
177
|
+
scheduleOnUIOrImmediate(updateRegistryOnUI, itemIndexRegistry, itemId, index);
|
|
165
178
|
}
|
|
166
179
|
}, [itemIndexRegistry]);
|
|
167
180
|
// Mark registry as recently updated (call this from handleDragEnd)
|
|
@@ -210,7 +223,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
210
223
|
const resetDragState = (0, react_1.useCallback)(() => {
|
|
211
224
|
isDragging.value = false;
|
|
212
225
|
draggedIndex.value = -1;
|
|
213
|
-
draggedItemId.value =
|
|
226
|
+
draggedItemId.value = "";
|
|
214
227
|
currentTranslateY.value = 0;
|
|
215
228
|
draggedScale.value = 1;
|
|
216
229
|
dragStartScrollOffset.value = 0;
|
|
@@ -235,6 +248,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
235
248
|
dragSequence,
|
|
236
249
|
setListRef,
|
|
237
250
|
scrollToOffset,
|
|
251
|
+
prepareForLayoutAnimationRender,
|
|
238
252
|
resetDragState,
|
|
239
253
|
config,
|
|
240
254
|
itemIndexRegistry,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { SharedValue } from
|
|
2
|
-
import type { UseDragAnimatedStyleResult } from
|
|
1
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
2
|
+
import type { UseDragAnimatedStyleResult } from "../../types";
|
|
3
3
|
/**
|
|
4
4
|
* Hook that creates animated styles for drag operations.
|
|
5
5
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDragAnimatedStyle.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragAnimatedStyle.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAI3D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,EAChC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,EAC/B,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,GAC1B,0BAA0B,
|
|
1
|
+
{"version":3,"file":"useDragAnimatedStyle.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragAnimatedStyle.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAI3D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,EAChC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,EAC/B,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,GAC1B,0BAA0B,CAmF5B"}
|
|
@@ -35,7 +35,7 @@ const constants_1 = require("../../constants");
|
|
|
35
35
|
*/
|
|
36
36
|
function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
|
|
37
37
|
// Global drag state for scale, identity check, and scroll compensation
|
|
38
|
-
const { draggedItemId, draggedScale, config, scrollOffset, dragStartScrollOffset,
|
|
38
|
+
const { draggedItemId, draggedScale, config, scrollOffset, dragStartScrollOffset, } = (0, DragStateContext_1.useDragState)();
|
|
39
39
|
// Issue 2 Fix: Track shadow opacity with smooth transitions to prevent "flash"
|
|
40
40
|
// When drag ends, shadow opacity should fade smoothly instead of snapping
|
|
41
41
|
const animatedShadowOpacity = (0, react_native_reanimated_1.useSharedValue)(0.1);
|
|
@@ -44,7 +44,7 @@ function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
|
|
|
44
44
|
isThisItemDragged: draggedItemId.value === itemId,
|
|
45
45
|
scale: draggedScale.value,
|
|
46
46
|
}), (current, previous) => {
|
|
47
|
-
|
|
47
|
+
"worklet";
|
|
48
48
|
if (current.isThisItemDragged) {
|
|
49
49
|
// When dragged, interpolate shadow opacity based on scale
|
|
50
50
|
const targetOpacity = (0, react_native_reanimated_1.interpolate)(current.scale, [1, config.dragScale], [0.1, config.dragShadowOpacity]);
|
|
@@ -67,20 +67,9 @@ function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
|
|
|
67
67
|
const hasSignificantTranslateY = Math.abs(translateY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
|
|
68
68
|
// Calculate scroll delta for position compensation during autoscroll
|
|
69
69
|
const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
// Case 2 handles the FALLBACK scenario where FlashList doesn't re-render
|
|
75
|
-
// after reorder. The item uses translateY to stay at its new position
|
|
76
|
-
// until FlashList eventually re-renders and useDropCompensation runs.
|
|
77
|
-
//
|
|
78
|
-
// CRITICAL: We use isSettling (isDropping && draggedItemId matches) instead
|
|
79
|
-
// of hasSignificantTranslateY. This ensures only the CURRENT dropping item
|
|
80
|
-
// uses its translateY. Other items with stale translateY from interrupted
|
|
81
|
-
// animations won't incorrectly apply their values with a new drag's scrollDelta.
|
|
82
|
-
const isSettling = isDropping.value && draggedItemId.value === itemId;
|
|
83
|
-
const shouldUseDragOffset = (isThisItemDragged && isDragging.value) || isSettling;
|
|
70
|
+
// Use translateY only while actively dragging. Once drop begins we rely on
|
|
71
|
+
// the new base layout (shiftY/FlashList) to avoid any post-drop bounce.
|
|
72
|
+
const shouldUseDragOffset = isThisItemDragged && isDragging.value;
|
|
84
73
|
// Use drag translateY + scroll compensation for dragged/settling item,
|
|
85
74
|
// shiftY for others or for items at rest
|
|
86
75
|
const yOffset = shouldUseDragOffset
|
|
@@ -89,11 +78,8 @@ function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
|
|
|
89
78
|
// Only apply visual effects (scale, elevation, zIndex) when ACTIVELY dragging
|
|
90
79
|
// Not when just settling at new position after drop
|
|
91
80
|
const isActivelyDragging = isThisItemDragged && isDragging.value;
|
|
92
|
-
// Keep elevated zIndex
|
|
93
|
-
|
|
94
|
-
// Note: We use hasSignificantTranslateY alone (not isThisItemDragged) because
|
|
95
|
-
// after FALLBACK completes, draggedItemId is reset to '' but translateY remains
|
|
96
|
-
const shouldBeElevated = isActivelyDragging || hasSignificantTranslateY;
|
|
81
|
+
// Keep elevated zIndex only while actively dragging
|
|
82
|
+
const shouldBeElevated = isActivelyDragging;
|
|
97
83
|
return {
|
|
98
84
|
transform: [
|
|
99
85
|
{ translateY: yOffset },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDragGesture.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragGesture.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useDragGesture.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragGesture.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EACV,oBAAoB,EACpB,uBAAuB,EACvB,oBAAoB,EACrB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,oBAAoB,EAC5B,SAAS,EAAE,uBAAuB,GACjC,oBAAoB,CAybtB"}
|
|
@@ -37,11 +37,13 @@ const DragStateContext_1 = require("../../contexts/DragStateContext");
|
|
|
37
37
|
function useDragGesture(config, callbacks) {
|
|
38
38
|
const { itemId, index, totalItems, enabled, containerRef } = config;
|
|
39
39
|
const { onReorderByDelta, onHapticFeedback } = callbacks;
|
|
40
|
+
// Dev-only guard to warn once if measurement is missing
|
|
41
|
+
const hasWarnedMissingHeight = (0, react_1.useRef)(false);
|
|
40
42
|
// Local drag state for this item's animation
|
|
41
43
|
const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
|
|
42
44
|
const translateY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
43
45
|
// Global drag state for coordinating animations across all items
|
|
44
|
-
const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, draggedScale, scrollOffset, dragStartScrollOffset, contentHeight, visibleHeight, listTopY, measuredItemHeight, isDropping, scrollToOffset, config: dragConfig, itemIndexRegistry, dragStartIndexSnapshot, dragSequence, markRegistryUpdated, } = (0, DragStateContext_1.useDragState)();
|
|
46
|
+
const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, draggedScale, scrollOffset, dragStartScrollOffset, contentHeight, visibleHeight, listTopY, measuredItemHeight, isDropping, scrollToOffset, prepareForLayoutAnimationRender, config: dragConfig, itemIndexRegistry, dragStartIndexSnapshot, dragSequence, markRegistryUpdated, } = (0, DragStateContext_1.useDragState)();
|
|
45
47
|
// Performance optimization: Track last significant Y position and scroll time
|
|
46
48
|
// to avoid updating on every pixel movement
|
|
47
49
|
const lastSignificantY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
@@ -66,7 +68,15 @@ function useDragGesture(config, callbacks) {
|
|
|
66
68
|
currentIndex.value = index;
|
|
67
69
|
setPrevItemId(itemId);
|
|
68
70
|
}
|
|
69
|
-
}, [
|
|
71
|
+
}, [
|
|
72
|
+
itemId,
|
|
73
|
+
prevItemId,
|
|
74
|
+
setPrevItemId,
|
|
75
|
+
translateY,
|
|
76
|
+
isDragging,
|
|
77
|
+
currentIndex,
|
|
78
|
+
index,
|
|
79
|
+
]);
|
|
70
80
|
// Sync index to SharedValue in useEffect (not during render per Reanimated docs)
|
|
71
81
|
(0, react_1.useEffect)(() => {
|
|
72
82
|
currentIndex.value = index;
|
|
@@ -98,6 +108,16 @@ function useDragGesture(config, callbacks) {
|
|
|
98
108
|
const itemHeight = measuredItemHeight.value > 0
|
|
99
109
|
? measuredItemHeight.value
|
|
100
110
|
: dragConfig.itemHeight;
|
|
111
|
+
if (__DEV__ &&
|
|
112
|
+
measuredItemHeight.value <= 0 &&
|
|
113
|
+
!hasWarnedMissingHeight.current) {
|
|
114
|
+
console.warn("[AnimatedFlashList] Falling back to configured itemHeight because measurement was unavailable", {
|
|
115
|
+
itemId,
|
|
116
|
+
index,
|
|
117
|
+
fallbackHeight: dragConfig.itemHeight,
|
|
118
|
+
});
|
|
119
|
+
hasWarnedMissingHeight.current = true;
|
|
120
|
+
}
|
|
101
121
|
// Calculate how many positions to move based on drag offset
|
|
102
122
|
const positionDelta = Math.round(finalTranslateY / itemHeight);
|
|
103
123
|
// Calculate if position actually changes
|
|
@@ -109,7 +129,9 @@ function useDragGesture(config, callbacks) {
|
|
|
109
129
|
// but we also animate here as a fallback since FlashList's virtualization
|
|
110
130
|
// may not trigger re-renders when data changes.
|
|
111
131
|
isDropping.value = true;
|
|
112
|
-
onHapticFeedback?.(
|
|
132
|
+
onHapticFeedback?.("medium");
|
|
133
|
+
// Ask FlashList to prepare cells for layout animation on next render
|
|
134
|
+
prepareForLayoutAnimationRender();
|
|
113
135
|
reorder(currentItemId, positionDelta);
|
|
114
136
|
// Mark registry as recently updated to protect against stale React prop overwrites
|
|
115
137
|
// The onEnd callback already updated the registry on the UI thread - this timestamp
|
|
@@ -118,71 +140,15 @@ function useDragGesture(config, callbacks) {
|
|
|
118
140
|
// Note: Registry is now updated in onEnd on UI thread to prevent race conditions
|
|
119
141
|
// where a new drag starts before this JS callback runs.
|
|
120
142
|
// Reset currentTranslateY to stop shift calculations for other items
|
|
143
|
+
// This triggers all shifted items to animate their shiftY back to 0
|
|
121
144
|
currentTranslateY.value = 0;
|
|
122
|
-
//
|
|
123
|
-
// This
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}, 16);
|
|
130
|
-
// Fallback: If useDropCompensation doesn't run (no re-render from FlashList),
|
|
131
|
-
// we need to animate the item to its new position and reset state.
|
|
132
|
-
//
|
|
133
|
-
// Since FlashList hasn't re-rendered, the item's base position is still at the
|
|
134
|
-
// old index. To make it appear at the new index, we calculate the target translateY
|
|
135
|
-
// that positions the item correctly.
|
|
136
|
-
//
|
|
137
|
-
// When FlashList eventually re-renders (on scroll or interaction), useDropCompensation
|
|
138
|
-
// will see the index change and animate translateY back to 0.
|
|
139
|
-
(0, react_native_reanimated_1.cancelAnimation)(translateY);
|
|
140
|
-
// Calculate the target translateY to position item at its new index
|
|
141
|
-
// Since FlashList hasn't updated base position, we need:
|
|
142
|
-
// targetTranslateY = (newIndex - currentIndex) * itemHeight
|
|
143
|
-
const targetTranslateY = (newIndex - currentIndex) * itemHeight;
|
|
144
|
-
// Capture current sequence to guard callbacks against race conditions
|
|
145
|
-
// If a new drag starts before these animations complete, the sequence
|
|
146
|
-
// will have incremented and we should skip the state reset
|
|
147
|
-
const currentSequence = dragSequence.value;
|
|
148
|
-
// Use a delayed animation to give useDropCompensation a chance to run first
|
|
149
|
-
translateY.value = (0, react_native_reanimated_1.withDelay)(150, // Delay to let React re-render and useDropCompensation run first
|
|
150
|
-
(0, react_native_reanimated_1.withTiming)(targetTranslateY, // Animate to the new position
|
|
151
|
-
{ duration: 150, easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease) }, finished => {
|
|
152
|
-
'worklet';
|
|
153
|
-
// Guard: Only proceed if this is still the same drag operation
|
|
154
|
-
if (dragSequence.value !== currentSequence)
|
|
155
|
-
return;
|
|
156
|
-
// Only reset state if still in dropping state (useDropCompensation didn't handle it)
|
|
157
|
-
if (finished && isDropping.value) {
|
|
158
|
-
// FlashList doesn't re-render visible items after data changes.
|
|
159
|
-
// Since useDropCompensation won't run, we need to animate translateY
|
|
160
|
-
// back to 0 ourselves.
|
|
161
|
-
//
|
|
162
|
-
// CRITICAL: Do NOT reset state here! We must wait until translateY
|
|
163
|
-
// finishes animating to 0. Otherwise, if a new drag starts during
|
|
164
|
-
// this 200ms animation, the item will have stale translateY values
|
|
165
|
-
// that get applied incorrectly with the new drag's scroll delta.
|
|
166
|
-
translateY.value = (0, react_native_reanimated_1.withTiming)(0, {
|
|
167
|
-
duration: 200,
|
|
168
|
-
easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
|
|
169
|
-
}, settleFinished => {
|
|
170
|
-
'worklet';
|
|
171
|
-
// Guard: Check sequence again before resetting state
|
|
172
|
-
if (dragSequence.value !== currentSequence)
|
|
173
|
-
return;
|
|
174
|
-
if (settleFinished) {
|
|
175
|
-
// NOW reset all state - after translateY is fully 0
|
|
176
|
-
globalIsDragging.value = false;
|
|
177
|
-
isDropping.value = false;
|
|
178
|
-
draggedIndex.value = -1;
|
|
179
|
-
draggedItemId.value = '';
|
|
180
|
-
measuredItemHeight.value = 0;
|
|
181
|
-
dragStartScrollOffset.value = 0;
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
}));
|
|
145
|
+
// CRITICAL: Store the current translateY value before any animations start
|
|
146
|
+
// This represents where the item visually is at the moment of drop
|
|
147
|
+
const dropTranslateY = translateY.value;
|
|
148
|
+
// Freeze the visual position at the drop point; do not animate toward 0 here
|
|
149
|
+
// because FlashList has not yet updated base layouts. Animating now would
|
|
150
|
+
// create a visible bounce (the issue you are seeing).
|
|
151
|
+
translateY.value = dropTranslateY;
|
|
186
152
|
}
|
|
187
153
|
else {
|
|
188
154
|
// Same position - animate back and reset state
|
|
@@ -190,15 +156,15 @@ function useDragGesture(config, callbacks) {
|
|
|
190
156
|
const currentSequence = dragSequence.value;
|
|
191
157
|
// Cancel any existing animation before starting the return animation
|
|
192
158
|
(0, react_native_reanimated_1.cancelAnimation)(translateY);
|
|
193
|
-
translateY.value = (0, react_native_reanimated_1.withTiming)(0, { duration: 150, easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease) }, finished => {
|
|
194
|
-
|
|
159
|
+
translateY.value = (0, react_native_reanimated_1.withTiming)(0, { duration: 150, easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease) }, (finished) => {
|
|
160
|
+
"worklet";
|
|
195
161
|
// Guard: Only reset if this is still the same drag operation
|
|
196
162
|
if (dragSequence.value !== currentSequence)
|
|
197
163
|
return;
|
|
198
164
|
if (finished) {
|
|
199
165
|
globalIsDragging.value = false;
|
|
200
166
|
draggedIndex.value = -1;
|
|
201
|
-
draggedItemId.value =
|
|
167
|
+
draggedItemId.value = "";
|
|
202
168
|
currentTranslateY.value = 0;
|
|
203
169
|
dragStartScrollOffset.value = 0;
|
|
204
170
|
measuredItemHeight.value = 0;
|
|
@@ -221,14 +187,14 @@ function useDragGesture(config, callbacks) {
|
|
|
221
187
|
]);
|
|
222
188
|
// Stable haptic callback for drag start
|
|
223
189
|
const triggerLightHaptic = (0, react_1.useCallback)(() => {
|
|
224
|
-
onHapticFeedback?.(
|
|
190
|
+
onHapticFeedback?.("light");
|
|
225
191
|
}, [onHapticFeedback]);
|
|
226
192
|
// Pan gesture for drag-to-reorder
|
|
227
193
|
const panGesture = (0, react_1.useMemo)(() => react_native_gesture_handler_1.Gesture.Pan()
|
|
228
194
|
.activateAfterLongPress(dragConfig.longPressDuration)
|
|
229
195
|
.enabled(enabled)
|
|
230
196
|
.onStart(() => {
|
|
231
|
-
|
|
197
|
+
"worklet";
|
|
232
198
|
// Reset any pending drop state from previous drag
|
|
233
199
|
// This is critical: if a new drag starts before the previous drop's
|
|
234
200
|
// fallback completed, isDropping would still be true, causing shift
|
|
@@ -279,8 +245,8 @@ function useDragGesture(config, callbacks) {
|
|
|
279
245
|
});
|
|
280
246
|
(0, react_native_worklets_1.scheduleOnRN)(triggerLightHaptic);
|
|
281
247
|
})
|
|
282
|
-
.onUpdate(event => {
|
|
283
|
-
|
|
248
|
+
.onUpdate((event) => {
|
|
249
|
+
"worklet";
|
|
284
250
|
// Always update translateY for smooth visual feedback
|
|
285
251
|
translateY.value = event.translationY;
|
|
286
252
|
// Performance optimization: Only update currentTranslateY (which triggers
|
|
@@ -300,7 +266,7 @@ function useDragGesture(config, callbacks) {
|
|
|
300
266
|
const topEdge = dragConfig.edgeThreshold;
|
|
301
267
|
const bottomEdge = visibleHeight.value - dragConfig.edgeThreshold;
|
|
302
268
|
if (fingerInList < topEdge && scrollOffset.value > 0) {
|
|
303
|
-
const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [0, topEdge], [dragConfig.maxScrollSpeed, 0],
|
|
269
|
+
const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [0, topEdge], [dragConfig.maxScrollSpeed, 0], "clamp");
|
|
304
270
|
const newOffset = Math.max(0, scrollOffset.value - speed);
|
|
305
271
|
scrollOffset.value = newOffset;
|
|
306
272
|
// Throttle actual scroll calls to reduce JS thread pressure
|
|
@@ -312,7 +278,7 @@ function useDragGesture(config, callbacks) {
|
|
|
312
278
|
else if (fingerInList > bottomEdge) {
|
|
313
279
|
const maxOffset = Math.max(0, contentHeight.value - visibleHeight.value);
|
|
314
280
|
if (scrollOffset.value < maxOffset) {
|
|
315
|
-
const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [bottomEdge, visibleHeight.value], [0, dragConfig.maxScrollSpeed],
|
|
281
|
+
const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [bottomEdge, visibleHeight.value], [0, dragConfig.maxScrollSpeed], "clamp");
|
|
316
282
|
const newOffset = Math.min(maxOffset, scrollOffset.value + speed);
|
|
317
283
|
scrollOffset.value = newOffset;
|
|
318
284
|
// Throttle actual scroll calls to reduce JS thread pressure
|
|
@@ -323,8 +289,8 @@ function useDragGesture(config, callbacks) {
|
|
|
323
289
|
}
|
|
324
290
|
}
|
|
325
291
|
})
|
|
326
|
-
.onEnd(event => {
|
|
327
|
-
|
|
292
|
+
.onEnd((event) => {
|
|
293
|
+
"worklet";
|
|
328
294
|
isDragging.value = false;
|
|
329
295
|
// Include scroll compensation in final position calculation
|
|
330
296
|
const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
|
|
@@ -344,6 +310,9 @@ function useDragGesture(config, callbacks) {
|
|
|
344
310
|
const positionDelta = Math.round(finalY / itemHeight);
|
|
345
311
|
const newIdx = Math.max(0, Math.min(total - 1, currentIdx + positionDelta));
|
|
346
312
|
if (positionDelta !== 0 && newIdx !== currentIdx) {
|
|
313
|
+
// Mark drop phase immediately on UI thread to prevent non-dragged items
|
|
314
|
+
// from briefly animating back to 0 before the JS drop handler runs.
|
|
315
|
+
isDropping.value = true;
|
|
347
316
|
// Update registry directly on UI thread
|
|
348
317
|
const registry = itemIndexRegistry.value;
|
|
349
318
|
const newRegistry = {};
|
|
@@ -378,14 +347,14 @@ function useDragGesture(config, callbacks) {
|
|
|
378
347
|
(0, react_native_worklets_1.scheduleOnRN)(handleDragEnd, finalY);
|
|
379
348
|
})
|
|
380
349
|
.onFinalize((_event, success) => {
|
|
381
|
-
|
|
350
|
+
"worklet";
|
|
382
351
|
if (!success) {
|
|
383
352
|
isDragging.value = false;
|
|
384
353
|
translateY.value = (0, react_native_reanimated_1.withTiming)(0, { duration: 150 });
|
|
385
354
|
isDropping.value = false;
|
|
386
355
|
globalIsDragging.value = false;
|
|
387
356
|
draggedIndex.value = -1;
|
|
388
|
-
draggedItemId.value =
|
|
357
|
+
draggedItemId.value = "";
|
|
389
358
|
currentTranslateY.value = 0;
|
|
390
359
|
draggedScale.value = 1;
|
|
391
360
|
dragStartScrollOffset.value = 0;
|
|
@@ -393,6 +362,15 @@ function useDragGesture(config, callbacks) {
|
|
|
393
362
|
}
|
|
394
363
|
}),
|
|
395
364
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
396
|
-
[
|
|
365
|
+
[
|
|
366
|
+
enabled,
|
|
367
|
+
containerRef,
|
|
368
|
+
triggerLightHaptic,
|
|
369
|
+
handleDragEnd,
|
|
370
|
+
index,
|
|
371
|
+
itemId,
|
|
372
|
+
dragStartIndexSnapshot,
|
|
373
|
+
itemIndexRegistry,
|
|
374
|
+
]);
|
|
397
375
|
return { panGesture, isDragging, translateY };
|
|
398
376
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDragShift.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragShift.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE1E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,
|
|
1
|
+
{"version":3,"file":"useDragShift.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragShift.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE1E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,CAmP3E"}
|
|
@@ -60,7 +60,7 @@ function useDragShift(config) {
|
|
|
60
60
|
// so we don't need a manual trigger counter. When any read SharedValue changes,
|
|
61
61
|
// Reanimated will re-execute this derived value.
|
|
62
62
|
const targetShiftY = (0, react_native_reanimated_1.useDerivedValue)(() => {
|
|
63
|
-
|
|
63
|
+
"worklet";
|
|
64
64
|
// Read dragSequence to force recalculation when a new drag starts
|
|
65
65
|
// This ensures stale shiftY values from previous drags are recalculated
|
|
66
66
|
const _sequence = dragSequence.value;
|
|
@@ -85,10 +85,9 @@ function useDragShift(config) {
|
|
|
85
85
|
// Dragged item doesn't need shift (it uses translateY)
|
|
86
86
|
if (currentDraggedItemId === itemId)
|
|
87
87
|
return 0;
|
|
88
|
-
// During drop:
|
|
89
|
-
// Let useDropCompensation handle the reset when FlashList re-renders with correct indices
|
|
88
|
+
// During drop: force shift to 0 to avoid any extra wobble when FlashList re-renders
|
|
90
89
|
if (isDropping.value) {
|
|
91
|
-
return
|
|
90
|
+
return 0;
|
|
92
91
|
}
|
|
93
92
|
// Not dragging: return 0 (no shift needed)
|
|
94
93
|
// IMPORTANT: We previously returned flashListMismatch here, but that caused permanent
|
|
@@ -149,7 +148,7 @@ function useDragShift(config) {
|
|
|
149
148
|
target: targetShiftY.value,
|
|
150
149
|
sequence: dragSequence.value,
|
|
151
150
|
}), (state, prev) => {
|
|
152
|
-
|
|
151
|
+
"worklet";
|
|
153
152
|
const targetChanged = state.target !== prev?.target;
|
|
154
153
|
const newDragStarted = prev !== null && state.sequence !== prev.sequence;
|
|
155
154
|
// Cancel any in-flight animation when new drag starts and reset to clean baseline
|
|
@@ -176,6 +175,20 @@ function useDragShift(config) {
|
|
|
176
175
|
});
|
|
177
176
|
}
|
|
178
177
|
});
|
|
178
|
+
// Freeze any in-flight shift animation as soon as drop starts to prevent
|
|
179
|
+
// a “double slide” while isDropping holds the visual spacing.
|
|
180
|
+
(0, react_native_reanimated_1.useAnimatedReaction)(() => isDropping.value, (dropping, prev) => {
|
|
181
|
+
"worklet";
|
|
182
|
+
if (dropping && !prev) {
|
|
183
|
+
(0, react_native_reanimated_1.cancelAnimation)(shiftY);
|
|
184
|
+
shiftY.value = 0;
|
|
185
|
+
}
|
|
186
|
+
// When drop finishes, snap shiftY to 0 (no animation) to avoid a final wobble
|
|
187
|
+
if (!dropping && prev) {
|
|
188
|
+
(0, react_native_reanimated_1.cancelAnimation)(shiftY);
|
|
189
|
+
shiftY.value = 0;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
179
192
|
// CRITICAL: Explicit reset when drag ends (both isDragging and isDropping are false)
|
|
180
193
|
// This serves as a safeguard against race conditions where the target-based animation
|
|
181
194
|
// above doesn't fire properly. When drag fully completes, all items should have shiftY = 0.
|
|
@@ -183,21 +196,17 @@ function useDragShift(config) {
|
|
|
183
196
|
isDragging: globalIsDragging.value,
|
|
184
197
|
isDropping: isDropping.value,
|
|
185
198
|
}), (current, prev) => {
|
|
186
|
-
|
|
199
|
+
"worklet";
|
|
187
200
|
if (prev === null)
|
|
188
201
|
return;
|
|
189
202
|
// Detect when drag fully ends: was dragging/dropping, now neither
|
|
190
203
|
const wasActive = prev.isDragging || prev.isDropping;
|
|
191
204
|
const isNowIdle = !current.isDragging && !current.isDropping;
|
|
192
205
|
if (wasActive && isNowIdle) {
|
|
193
|
-
// Force reset shiftY to 0 when drag fully completes
|
|
194
|
-
// This catches any items that didn't get reset through the normal target animation
|
|
206
|
+
// Force reset shiftY to 0 when drag fully completes (snap, no animation)
|
|
195
207
|
if (Math.abs(shiftY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD) {
|
|
196
208
|
(0, react_native_reanimated_1.cancelAnimation)(shiftY);
|
|
197
|
-
shiftY.value =
|
|
198
|
-
duration: constants_1.ANIMATION_TIMING.SHIFT_DURATION,
|
|
199
|
-
easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
|
|
200
|
-
});
|
|
209
|
+
shiftY.value = 0;
|
|
201
210
|
}
|
|
202
211
|
}
|
|
203
212
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDropCompensation.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDropCompensation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useDropCompensation.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDropCompensation.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,CA6I3E"}
|