@souscheflabs/reanimated-flashlist 0.2.8 → 0.3.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/contexts/DragStateContext.d.ts +6 -4
- package/lib/contexts/DragStateContext.d.ts.map +1 -1
- package/lib/contexts/DragStateContext.js +14 -9
- 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 +52 -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 +44 -31
- package/package.json +1 -1
- package/src/contexts/DragStateContext.tsx +28 -16
- package/src/hooks/drag/useDragAnimatedStyle.ts +21 -30
- package/src/hooks/drag/useDragGesture.ts +72 -101
- package/src/hooks/drag/useDragShift.ts +35 -21
- package/src/hooks/drag/useDropCompensation.ts +63 -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;AAoB3C;;;;;;;;;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,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,
|
|
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;AAoB3C;;;;;;;;;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,CA+N9D,CAAC"}
|
|
@@ -44,7 +44,7 @@ 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 };
|
|
@@ -58,7 +58,7 @@ const DragStateContext = (0, react_1.createContext)(null);
|
|
|
58
58
|
const useDragState = () => {
|
|
59
59
|
const context = (0, react_1.useContext)(DragStateContext);
|
|
60
60
|
if (!context) {
|
|
61
|
-
throw new Error(
|
|
61
|
+
throw new Error("useDragState must be used within DragStateProvider");
|
|
62
62
|
}
|
|
63
63
|
return context;
|
|
64
64
|
};
|
|
@@ -80,19 +80,19 @@ exports.useDragState = useDragState;
|
|
|
80
80
|
function validateConfig(config) {
|
|
81
81
|
if (__DEV__) {
|
|
82
82
|
if (config.itemHeight <= 0) {
|
|
83
|
-
console.warn(
|
|
83
|
+
console.warn("[AnimatedFlashList] Invalid itemHeight: must be positive. Got:", config.itemHeight);
|
|
84
84
|
}
|
|
85
85
|
if (config.longPressDuration <= 0) {
|
|
86
|
-
console.warn(
|
|
86
|
+
console.warn("[AnimatedFlashList] Invalid longPressDuration: must be positive. Got:", config.longPressDuration);
|
|
87
87
|
}
|
|
88
88
|
if (config.dragScale <= 0) {
|
|
89
|
-
console.warn(
|
|
89
|
+
console.warn("[AnimatedFlashList] Invalid dragScale: must be positive. Got:", config.dragScale);
|
|
90
90
|
}
|
|
91
91
|
if (config.edgeThreshold < 0) {
|
|
92
|
-
console.warn(
|
|
92
|
+
console.warn("[AnimatedFlashList] Invalid edgeThreshold: must be non-negative. Got:", config.edgeThreshold);
|
|
93
93
|
}
|
|
94
94
|
if (config.maxScrollSpeed <= 0) {
|
|
95
|
-
console.warn(
|
|
95
|
+
console.warn("[AnimatedFlashList] Invalid maxScrollSpeed: must be positive. Got:", config.maxScrollSpeed);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
}
|
|
@@ -106,7 +106,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
106
106
|
// Shared values are created once and persist for the lifetime of the provider
|
|
107
107
|
const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
|
|
108
108
|
const draggedIndex = (0, react_native_reanimated_1.useSharedValue)(-1);
|
|
109
|
-
const draggedItemId = (0, react_native_reanimated_1.useSharedValue)(
|
|
109
|
+
const draggedItemId = (0, react_native_reanimated_1.useSharedValue)("");
|
|
110
110
|
const currentTranslateY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
111
111
|
const draggedScale = (0, react_native_reanimated_1.useSharedValue)(1);
|
|
112
112
|
// Scroll state for viewport-aware calculations and autoscroll
|
|
@@ -139,6 +139,10 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
139
139
|
const scrollToOffset = (0, react_1.useCallback)((offset, animated = false) => {
|
|
140
140
|
listRef.current?.scrollToOffset({ offset, animated });
|
|
141
141
|
}, []);
|
|
142
|
+
// Request FlashList to prepare cells for layout animation on next render
|
|
143
|
+
const prepareForLayoutAnimationRender = (0, react_1.useCallback)(() => {
|
|
144
|
+
listRef.current?.prepareForLayoutAnimationRender?.();
|
|
145
|
+
}, []);
|
|
142
146
|
// Update an item's index in the registry
|
|
143
147
|
// Uses scheduleOnUI to ensure SharedValue modifications are immediately visible
|
|
144
148
|
// to UI thread worklets (fixing the registry sync bug).
|
|
@@ -210,7 +214,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
210
214
|
const resetDragState = (0, react_1.useCallback)(() => {
|
|
211
215
|
isDragging.value = false;
|
|
212
216
|
draggedIndex.value = -1;
|
|
213
|
-
draggedItemId.value =
|
|
217
|
+
draggedItemId.value = "";
|
|
214
218
|
currentTranslateY.value = 0;
|
|
215
219
|
draggedScale.value = 1;
|
|
216
220
|
dragStartScrollOffset.value = 0;
|
|
@@ -235,6 +239,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
235
239
|
dragSequence,
|
|
236
240
|
setListRef,
|
|
237
241
|
scrollToOffset,
|
|
242
|
+
prepareForLayoutAnimationRender,
|
|
238
243
|
resetDragState,
|
|
239
244
|
config,
|
|
240
245
|
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":"AAqBA,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,CA2atB"}
|
|
@@ -41,7 +41,7 @@ function useDragGesture(config, callbacks) {
|
|
|
41
41
|
const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
|
|
42
42
|
const translateY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
43
43
|
// 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)();
|
|
44
|
+
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
45
|
// Performance optimization: Track last significant Y position and scroll time
|
|
46
46
|
// to avoid updating on every pixel movement
|
|
47
47
|
const lastSignificantY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
@@ -66,7 +66,15 @@ function useDragGesture(config, callbacks) {
|
|
|
66
66
|
currentIndex.value = index;
|
|
67
67
|
setPrevItemId(itemId);
|
|
68
68
|
}
|
|
69
|
-
}, [
|
|
69
|
+
}, [
|
|
70
|
+
itemId,
|
|
71
|
+
prevItemId,
|
|
72
|
+
setPrevItemId,
|
|
73
|
+
translateY,
|
|
74
|
+
isDragging,
|
|
75
|
+
currentIndex,
|
|
76
|
+
index,
|
|
77
|
+
]);
|
|
70
78
|
// Sync index to SharedValue in useEffect (not during render per Reanimated docs)
|
|
71
79
|
(0, react_1.useEffect)(() => {
|
|
72
80
|
currentIndex.value = index;
|
|
@@ -109,7 +117,9 @@ function useDragGesture(config, callbacks) {
|
|
|
109
117
|
// but we also animate here as a fallback since FlashList's virtualization
|
|
110
118
|
// may not trigger re-renders when data changes.
|
|
111
119
|
isDropping.value = true;
|
|
112
|
-
onHapticFeedback?.(
|
|
120
|
+
onHapticFeedback?.("medium");
|
|
121
|
+
// Ask FlashList to prepare cells for layout animation on next render
|
|
122
|
+
prepareForLayoutAnimationRender();
|
|
113
123
|
reorder(currentItemId, positionDelta);
|
|
114
124
|
// Mark registry as recently updated to protect against stale React prop overwrites
|
|
115
125
|
// The onEnd callback already updated the registry on the UI thread - this timestamp
|
|
@@ -118,71 +128,20 @@ function useDragGesture(config, callbacks) {
|
|
|
118
128
|
// Note: Registry is now updated in onEnd on UI thread to prevent race conditions
|
|
119
129
|
// where a new drag starts before this JS callback runs.
|
|
120
130
|
// Reset currentTranslateY to stop shift calculations for other items
|
|
131
|
+
// This triggers all shifted items to animate their shiftY back to 0
|
|
121
132
|
currentTranslateY.value = 0;
|
|
122
|
-
//
|
|
123
|
-
// This
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
}));
|
|
133
|
+
// CRITICAL: Store the current translateY value before any animations start
|
|
134
|
+
// This represents where the item visually is at the moment of drop
|
|
135
|
+
const dropTranslateY = translateY.value;
|
|
136
|
+
// Freeze the visual position at the drop point; do not animate toward 0 here
|
|
137
|
+
// because FlashList has not yet updated base layouts. Animating now would
|
|
138
|
+
// create a visible bounce (the issue you are seeing).
|
|
139
|
+
translateY.value = dropTranslateY;
|
|
140
|
+
console.log("[DROP] Captured translateY at drop:", {
|
|
141
|
+
dropTranslateY,
|
|
142
|
+
currentIndex,
|
|
143
|
+
newIndex,
|
|
144
|
+
});
|
|
186
145
|
}
|
|
187
146
|
else {
|
|
188
147
|
// Same position - animate back and reset state
|
|
@@ -190,15 +149,15 @@ function useDragGesture(config, callbacks) {
|
|
|
190
149
|
const currentSequence = dragSequence.value;
|
|
191
150
|
// Cancel any existing animation before starting the return animation
|
|
192
151
|
(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
|
-
|
|
152
|
+
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) => {
|
|
153
|
+
"worklet";
|
|
195
154
|
// Guard: Only reset if this is still the same drag operation
|
|
196
155
|
if (dragSequence.value !== currentSequence)
|
|
197
156
|
return;
|
|
198
157
|
if (finished) {
|
|
199
158
|
globalIsDragging.value = false;
|
|
200
159
|
draggedIndex.value = -1;
|
|
201
|
-
draggedItemId.value =
|
|
160
|
+
draggedItemId.value = "";
|
|
202
161
|
currentTranslateY.value = 0;
|
|
203
162
|
dragStartScrollOffset.value = 0;
|
|
204
163
|
measuredItemHeight.value = 0;
|
|
@@ -221,14 +180,14 @@ function useDragGesture(config, callbacks) {
|
|
|
221
180
|
]);
|
|
222
181
|
// Stable haptic callback for drag start
|
|
223
182
|
const triggerLightHaptic = (0, react_1.useCallback)(() => {
|
|
224
|
-
onHapticFeedback?.(
|
|
183
|
+
onHapticFeedback?.("light");
|
|
225
184
|
}, [onHapticFeedback]);
|
|
226
185
|
// Pan gesture for drag-to-reorder
|
|
227
186
|
const panGesture = (0, react_1.useMemo)(() => react_native_gesture_handler_1.Gesture.Pan()
|
|
228
187
|
.activateAfterLongPress(dragConfig.longPressDuration)
|
|
229
188
|
.enabled(enabled)
|
|
230
189
|
.onStart(() => {
|
|
231
|
-
|
|
190
|
+
"worklet";
|
|
232
191
|
// Reset any pending drop state from previous drag
|
|
233
192
|
// This is critical: if a new drag starts before the previous drop's
|
|
234
193
|
// fallback completed, isDropping would still be true, causing shift
|
|
@@ -279,8 +238,8 @@ function useDragGesture(config, callbacks) {
|
|
|
279
238
|
});
|
|
280
239
|
(0, react_native_worklets_1.scheduleOnRN)(triggerLightHaptic);
|
|
281
240
|
})
|
|
282
|
-
.onUpdate(event => {
|
|
283
|
-
|
|
241
|
+
.onUpdate((event) => {
|
|
242
|
+
"worklet";
|
|
284
243
|
// Always update translateY for smooth visual feedback
|
|
285
244
|
translateY.value = event.translationY;
|
|
286
245
|
// Performance optimization: Only update currentTranslateY (which triggers
|
|
@@ -300,7 +259,7 @@ function useDragGesture(config, callbacks) {
|
|
|
300
259
|
const topEdge = dragConfig.edgeThreshold;
|
|
301
260
|
const bottomEdge = visibleHeight.value - dragConfig.edgeThreshold;
|
|
302
261
|
if (fingerInList < topEdge && scrollOffset.value > 0) {
|
|
303
|
-
const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [0, topEdge], [dragConfig.maxScrollSpeed, 0],
|
|
262
|
+
const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [0, topEdge], [dragConfig.maxScrollSpeed, 0], "clamp");
|
|
304
263
|
const newOffset = Math.max(0, scrollOffset.value - speed);
|
|
305
264
|
scrollOffset.value = newOffset;
|
|
306
265
|
// Throttle actual scroll calls to reduce JS thread pressure
|
|
@@ -312,7 +271,7 @@ function useDragGesture(config, callbacks) {
|
|
|
312
271
|
else if (fingerInList > bottomEdge) {
|
|
313
272
|
const maxOffset = Math.max(0, contentHeight.value - visibleHeight.value);
|
|
314
273
|
if (scrollOffset.value < maxOffset) {
|
|
315
|
-
const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [bottomEdge, visibleHeight.value], [0, dragConfig.maxScrollSpeed],
|
|
274
|
+
const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [bottomEdge, visibleHeight.value], [0, dragConfig.maxScrollSpeed], "clamp");
|
|
316
275
|
const newOffset = Math.min(maxOffset, scrollOffset.value + speed);
|
|
317
276
|
scrollOffset.value = newOffset;
|
|
318
277
|
// Throttle actual scroll calls to reduce JS thread pressure
|
|
@@ -323,8 +282,8 @@ function useDragGesture(config, callbacks) {
|
|
|
323
282
|
}
|
|
324
283
|
}
|
|
325
284
|
})
|
|
326
|
-
.onEnd(event => {
|
|
327
|
-
|
|
285
|
+
.onEnd((event) => {
|
|
286
|
+
"worklet";
|
|
328
287
|
isDragging.value = false;
|
|
329
288
|
// Include scroll compensation in final position calculation
|
|
330
289
|
const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
|
|
@@ -344,6 +303,9 @@ function useDragGesture(config, callbacks) {
|
|
|
344
303
|
const positionDelta = Math.round(finalY / itemHeight);
|
|
345
304
|
const newIdx = Math.max(0, Math.min(total - 1, currentIdx + positionDelta));
|
|
346
305
|
if (positionDelta !== 0 && newIdx !== currentIdx) {
|
|
306
|
+
// Mark drop phase immediately on UI thread to prevent non-dragged items
|
|
307
|
+
// from briefly animating back to 0 before the JS drop handler runs.
|
|
308
|
+
isDropping.value = true;
|
|
347
309
|
// Update registry directly on UI thread
|
|
348
310
|
const registry = itemIndexRegistry.value;
|
|
349
311
|
const newRegistry = {};
|
|
@@ -378,14 +340,14 @@ function useDragGesture(config, callbacks) {
|
|
|
378
340
|
(0, react_native_worklets_1.scheduleOnRN)(handleDragEnd, finalY);
|
|
379
341
|
})
|
|
380
342
|
.onFinalize((_event, success) => {
|
|
381
|
-
|
|
343
|
+
"worklet";
|
|
382
344
|
if (!success) {
|
|
383
345
|
isDragging.value = false;
|
|
384
346
|
translateY.value = (0, react_native_reanimated_1.withTiming)(0, { duration: 150 });
|
|
385
347
|
isDropping.value = false;
|
|
386
348
|
globalIsDragging.value = false;
|
|
387
349
|
draggedIndex.value = -1;
|
|
388
|
-
draggedItemId.value =
|
|
350
|
+
draggedItemId.value = "";
|
|
389
351
|
currentTranslateY.value = 0;
|
|
390
352
|
draggedScale.value = 1;
|
|
391
353
|
dragStartScrollOffset.value = 0;
|
|
@@ -393,6 +355,15 @@ function useDragGesture(config, callbacks) {
|
|
|
393
355
|
}
|
|
394
356
|
}),
|
|
395
357
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
396
|
-
[
|
|
358
|
+
[
|
|
359
|
+
enabled,
|
|
360
|
+
containerRef,
|
|
361
|
+
triggerLightHaptic,
|
|
362
|
+
handleDragEnd,
|
|
363
|
+
index,
|
|
364
|
+
itemId,
|
|
365
|
+
dragStartIndexSnapshot,
|
|
366
|
+
itemIndexRegistry,
|
|
367
|
+
]);
|
|
397
368
|
return { panGesture, isDragging, translateY };
|
|
398
369
|
}
|
|
@@ -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,CAgK3E"}
|