@souscheflabs/reanimated-flashlist 0.2.6 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/assets/example-screenshot.png +0 -0
- package/lib/AnimatedFlashList.d.ts.map +1 -1
- package/lib/AnimatedFlashList.js +13 -5
- package/lib/AnimatedFlashListItem.d.ts.map +1 -1
- package/lib/AnimatedFlashListItem.js +11 -0
- package/lib/contexts/DragStateContext.d.ts +23 -0
- package/lib/contexts/DragStateContext.d.ts.map +1 -1
- package/lib/contexts/DragStateContext.js +41 -0
- package/lib/hooks/drag/useDragAnimatedStyle.d.ts.map +1 -1
- package/lib/hooks/drag/useDragAnimatedStyle.js +23 -6
- package/lib/hooks/drag/useDragGesture.d.ts.map +1 -1
- package/lib/hooks/drag/useDragGesture.js +109 -8
- package/lib/hooks/drag/useDragShift.d.ts.map +1 -1
- package/lib/hooks/drag/useDragShift.js +55 -9
- package/lib/types/list.d.ts +5 -0
- package/lib/types/list.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/AnimatedFlashList.tsx +14 -5
- package/src/AnimatedFlashListItem.tsx +14 -1
- package/src/contexts/DragStateContext.tsx +76 -0
- package/src/hooks/drag/useDragAnimatedStyle.ts +27 -8
- package/src/hooks/drag/useDragGesture.ts +131 -8
- package/src/hooks/drag/useDragShift.ts +67 -10
- package/src/types/list.ts +6 -0
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
A high-performance animated FlashList with drag-to-reorder and entry/exit animations for React Native.
|
|
4
4
|
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="assets/example-screenshot.png" width="300" alt="Example screenshot showing drag-to-reorder list" />
|
|
7
|
+
</p>
|
|
8
|
+
|
|
5
9
|
## Features
|
|
6
10
|
|
|
7
11
|
- **Drag-to-reorder** with smooth animations and autoscroll
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnimatedFlashList.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAC;AAaf,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EAErB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"AnimatedFlashList.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAC;AAaf,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EAErB,MAAM,SAAS,CAAC;AAsYjB,eAAO,MAAM,iBAAiB,EAAyC,CACrE,CAAC,SAAS,gBAAgB,EAE1B,KAAK,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,oBAAoB,CAAC,CAAA;CAAE,KAClF,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC"}
|
package/lib/AnimatedFlashList.js
CHANGED
|
@@ -120,12 +120,20 @@ function InnerFlashList({ data, totalItemsRef, flashListRef, renderItem, keyExtr
|
|
|
120
120
|
* ```
|
|
121
121
|
*/
|
|
122
122
|
function AnimatedFlashListInner(props, ref) {
|
|
123
|
-
const { data, keyExtractor, renderItem, dragEnabled = false, onReorder, onReorderByNeighbors, canDrag, onHapticFeedback, config, onPrepareLayoutAnimation, ListFooterComponent, onRefresh, refreshing = false, onEndReached, onEndReachedThreshold = 0.5, contentContainerStyle, ...flashListProps } = props;
|
|
123
|
+
const { data, keyExtractor, renderItem, dragEnabled = false, onReorder, onReorderByNeighbors, canDrag, onHapticFeedback, config, onPrepareLayoutAnimation, ListFooterComponent, onRefresh, refreshing = false, onEndReached, onEndReachedThreshold = 0.5, contentContainerStyle, estimatedItemSize, ...flashListProps } = props;
|
|
124
124
|
// Merge config with defaults
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
// Use estimatedItemSize as default itemHeight if not explicitly configured
|
|
126
|
+
const dragConfig = (0, react_1.useMemo)(() => {
|
|
127
|
+
const mergedConfig = {
|
|
128
|
+
...constants_1.DEFAULT_DRAG_CONFIG,
|
|
129
|
+
...config?.drag,
|
|
130
|
+
itemHeight: config?.drag?.itemHeight ?? estimatedItemSize ?? constants_1.DEFAULT_DRAG_CONFIG.itemHeight,
|
|
131
|
+
};
|
|
132
|
+
if (__DEV__) {
|
|
133
|
+
console.log(`[AnimatedFlashList] dragConfig.itemHeight=${mergedConfig.itemHeight}, estimatedItemSize=${estimatedItemSize}, config.drag?.itemHeight=${config?.drag?.itemHeight}`);
|
|
134
|
+
}
|
|
135
|
+
return mergedConfig;
|
|
136
|
+
}, [config?.drag, estimatedItemSize]);
|
|
129
137
|
// Ref to FlashList
|
|
130
138
|
const flashListRef = (0, react_1.useRef)(null);
|
|
131
139
|
// Expose methods to parent via ref
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnimatedFlashListItem.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashListItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwD,MAAM,OAAO,CAAC;AAY7E,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,
|
|
1
|
+
{"version":3,"file":"AnimatedFlashListItem.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashListItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwD,MAAM,OAAO,CAAC;AAY7E,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,CA+J3D;AAGD,eAAO,MAAM,qBAAqB,EAE7B,OAAO,0BAA0B,CAAC"}
|
|
@@ -50,6 +50,17 @@ const contexts_1 = require("./contexts");
|
|
|
50
50
|
* @internal
|
|
51
51
|
*/
|
|
52
52
|
function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, renderItem, onReorderByDelta, onHapticFeedback, }) {
|
|
53
|
+
// DEBUG: Log renders
|
|
54
|
+
if (__DEV__) {
|
|
55
|
+
console.log(`[AnimatedFlashListItem] RENDER ${item.id} at index=${index}`);
|
|
56
|
+
}
|
|
57
|
+
// Register this item's index in the central registry
|
|
58
|
+
// This handles FlashList's recycling behavior where components may not re-render
|
|
59
|
+
// after data changes, leaving their index props stale
|
|
60
|
+
const { updateItemIndex } = (0, contexts_1.useDragState)();
|
|
61
|
+
// Call updateItemIndex synchronously during render to ensure the registry
|
|
62
|
+
// is always up-to-date when any item renders
|
|
63
|
+
updateItemIndex(item.id, index);
|
|
53
64
|
// Animated ref for measuring item height on drag start
|
|
54
65
|
const containerRef = (0, react_native_reanimated_1.useAnimatedRef)();
|
|
55
66
|
// Track measured height for layout compensation
|
|
@@ -37,6 +37,8 @@ export interface DragStateContextValue {
|
|
|
37
37
|
measuredItemHeight: SharedValue<number>;
|
|
38
38
|
/** Flag to freeze shift values during drop transition */
|
|
39
39
|
isDropping: SharedValue<boolean>;
|
|
40
|
+
/** Counter incremented on each drag start, used to force shift recalculation */
|
|
41
|
+
dragSequence: SharedValue<number>;
|
|
40
42
|
/** Register the FlashList ref for autoscroll operations */
|
|
41
43
|
setListRef: (ref: FlashListRef<unknown> | null) => void;
|
|
42
44
|
/** Scroll the list to a specific offset (for autoscroll during drag) */
|
|
@@ -45,6 +47,27 @@ export interface DragStateContextValue {
|
|
|
45
47
|
resetDragState: () => void;
|
|
46
48
|
/** Current drag configuration */
|
|
47
49
|
config: DragConfig;
|
|
50
|
+
/**
|
|
51
|
+
* Index registry for tracking itemId -> index mapping.
|
|
52
|
+
* Updated on every item render to handle FlashList recycling.
|
|
53
|
+
* Stored on UI thread as SharedValue for worklet access.
|
|
54
|
+
*/
|
|
55
|
+
itemIndexRegistry: SharedValue<Record<string, number>>;
|
|
56
|
+
/** Update an item's index in the registry (call from JS thread) */
|
|
57
|
+
updateItemIndex: (itemId: string, index: number) => void;
|
|
58
|
+
/** Get an item's index from the registry */
|
|
59
|
+
getItemIndex: (itemId: string) => number | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Snapshot of itemIndexRegistry taken at drag start.
|
|
62
|
+
* Used during drag to ensure consistent index lookup regardless of
|
|
63
|
+
* React re-renders or FlashList recycling.
|
|
64
|
+
*/
|
|
65
|
+
dragStartIndexSnapshot: SharedValue<Record<string, number>>;
|
|
66
|
+
/**
|
|
67
|
+
* Capture a snapshot of the current itemIndexRegistry for use during drag.
|
|
68
|
+
* Call this at the start of a drag operation.
|
|
69
|
+
*/
|
|
70
|
+
snapshotRegistryForDrag: () => void;
|
|
48
71
|
}
|
|
49
72
|
/**
|
|
50
73
|
* Hook to access shared drag state from context.
|
|
@@ -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;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAG3C;;;;;;;;;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,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;
|
|
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;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAG3C;;;;;;;;;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;CACrC;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,CA2J9D,CAAC"}
|
|
@@ -106,6 +106,14 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
106
106
|
const measuredItemHeight = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
107
107
|
// Flag to freeze shift values during drop transition
|
|
108
108
|
const isDropping = (0, react_native_reanimated_1.useSharedValue)(false);
|
|
109
|
+
// Counter incremented on each drag start, used to force shift recalculation
|
|
110
|
+
const dragSequence = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
111
|
+
// Index registry for tracking itemId -> index mapping
|
|
112
|
+
// This handles FlashList's recycling behavior where components may not re-render
|
|
113
|
+
// after data changes, leaving their index props stale
|
|
114
|
+
const itemIndexRegistry = (0, react_native_reanimated_1.useSharedValue)({});
|
|
115
|
+
// Snapshot of registry taken at drag start for consistent index lookup during drag
|
|
116
|
+
const dragStartIndexSnapshot = (0, react_native_reanimated_1.useSharedValue)({});
|
|
109
117
|
// Ref to FlashList for autoscroll operations
|
|
110
118
|
const listRef = (0, react_1.useRef)(null);
|
|
111
119
|
// Register the FlashList ref
|
|
@@ -116,6 +124,27 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
116
124
|
const scrollToOffset = (0, react_1.useCallback)((offset, animated = false) => {
|
|
117
125
|
listRef.current?.scrollToOffset({ offset, animated });
|
|
118
126
|
}, []);
|
|
127
|
+
// Update an item's index in the registry
|
|
128
|
+
// Called on every item render to keep indices fresh
|
|
129
|
+
const updateItemIndex = (0, react_1.useCallback)((itemId, index) => {
|
|
130
|
+
// Only update if the value has changed to avoid unnecessary SharedValue updates
|
|
131
|
+
if (itemIndexRegistry.value[itemId] !== index) {
|
|
132
|
+
itemIndexRegistry.value = {
|
|
133
|
+
...itemIndexRegistry.value,
|
|
134
|
+
[itemId]: index,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}, [itemIndexRegistry]);
|
|
138
|
+
// Get an item's index from the registry
|
|
139
|
+
const getItemIndex = (0, react_1.useCallback)((itemId) => {
|
|
140
|
+
return itemIndexRegistry.value[itemId];
|
|
141
|
+
}, [itemIndexRegistry]);
|
|
142
|
+
// Capture a snapshot of the registry at drag start
|
|
143
|
+
// This ensures consistent index lookup during the entire drag operation,
|
|
144
|
+
// regardless of React re-renders or FlashList recycling
|
|
145
|
+
const snapshotRegistryForDrag = (0, react_1.useCallback)(() => {
|
|
146
|
+
dragStartIndexSnapshot.value = { ...itemIndexRegistry.value };
|
|
147
|
+
}, [dragStartIndexSnapshot, itemIndexRegistry]);
|
|
119
148
|
// Reset drag state after drop
|
|
120
149
|
const resetDragState = (0, react_1.useCallback)(() => {
|
|
121
150
|
isDragging.value = false;
|
|
@@ -142,10 +171,16 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
142
171
|
listTopY,
|
|
143
172
|
measuredItemHeight,
|
|
144
173
|
isDropping,
|
|
174
|
+
dragSequence,
|
|
145
175
|
setListRef,
|
|
146
176
|
scrollToOffset,
|
|
147
177
|
resetDragState,
|
|
148
178
|
config,
|
|
179
|
+
itemIndexRegistry,
|
|
180
|
+
updateItemIndex,
|
|
181
|
+
getItemIndex,
|
|
182
|
+
dragStartIndexSnapshot,
|
|
183
|
+
snapshotRegistryForDrag,
|
|
149
184
|
}), [
|
|
150
185
|
isDragging,
|
|
151
186
|
draggedIndex,
|
|
@@ -159,10 +194,16 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
159
194
|
listTopY,
|
|
160
195
|
measuredItemHeight,
|
|
161
196
|
isDropping,
|
|
197
|
+
dragSequence,
|
|
162
198
|
setListRef,
|
|
163
199
|
scrollToOffset,
|
|
164
200
|
resetDragState,
|
|
165
201
|
config,
|
|
202
|
+
itemIndexRegistry,
|
|
203
|
+
updateItemIndex,
|
|
204
|
+
getItemIndex,
|
|
205
|
+
dragStartIndexSnapshot,
|
|
206
|
+
snapshotRegistryForDrag,
|
|
166
207
|
]);
|
|
167
208
|
return (<DragStateContext.Provider value={value}>
|
|
168
209
|
{children}
|
|
@@ -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,CAsF5B"}
|
|
@@ -64,20 +64,37 @@ function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
|
|
|
64
64
|
// Animated style for drag offset with scale and shadow
|
|
65
65
|
const dragAnimatedStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => {
|
|
66
66
|
const isThisItemDragged = draggedItemId.value === itemId;
|
|
67
|
-
|
|
68
|
-
const shouldBeElevated = isDragging.value ||
|
|
69
|
-
Math.abs(translateY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
|
|
67
|
+
const hasSignificantTranslateY = Math.abs(translateY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
|
|
70
68
|
// Calculate scroll delta for position compensation during autoscroll
|
|
71
69
|
const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
|
|
72
|
-
//
|
|
73
|
-
|
|
70
|
+
// Determine when to use translateY for positioning:
|
|
71
|
+
// 1. Actively being dragged (isThisItemDragged && isDragging)
|
|
72
|
+
// 2. Animating/settling after drop (hasSignificantTranslateY)
|
|
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
|
+
const shouldUseDragOffset = (isThisItemDragged && isDragging.value) || hasSignificantTranslateY;
|
|
78
|
+
// Use drag translateY + scroll compensation for dragged/settling item,
|
|
79
|
+
// shiftY for others or for items at rest
|
|
80
|
+
const yOffset = shouldUseDragOffset
|
|
74
81
|
? translateY.value + scrollDelta
|
|
75
82
|
: shiftY.value;
|
|
83
|
+
// Only apply visual effects (scale, elevation, zIndex) when ACTIVELY dragging
|
|
84
|
+
// Not when just settling at new position after drop
|
|
85
|
+
const isActivelyDragging = isThisItemDragged && isDragging.value;
|
|
86
|
+
// Keep elevated zIndex when actively dragging OR when settling at new position
|
|
87
|
+
// This prevents the dropped item from appearing behind other items
|
|
88
|
+
// Note: We use hasSignificantTranslateY alone (not isThisItemDragged) because
|
|
89
|
+
// after FALLBACK completes, draggedItemId is reset to '' but translateY remains
|
|
90
|
+
const shouldBeElevated = isActivelyDragging || hasSignificantTranslateY;
|
|
76
91
|
return {
|
|
77
92
|
transform: [
|
|
78
93
|
{ translateY: yOffset },
|
|
79
|
-
|
|
94
|
+
// Only apply drag scale when actively dragging
|
|
95
|
+
{ scale: isActivelyDragging ? draggedScale.value : 1 },
|
|
80
96
|
],
|
|
97
|
+
// Elevate zIndex when dragging or settling at new position
|
|
81
98
|
zIndex: shouldBeElevated ? constants_1.DRAG_THRESHOLDS.ELEVATED_Z_INDEX : 0,
|
|
82
99
|
// Use animated shadow opacity for smooth transitions
|
|
83
100
|
shadowOpacity: animatedShadowOpacity.value,
|
|
@@ -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":"AAeA,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,CA8WtB"}
|
|
@@ -5,6 +5,7 @@ const react_1 = require("react");
|
|
|
5
5
|
const react_native_reanimated_1 = require("react-native-reanimated");
|
|
6
6
|
const react_native_gesture_handler_1 = require("react-native-gesture-handler");
|
|
7
7
|
const react_native_worklets_1 = require("react-native-worklets");
|
|
8
|
+
const flash_list_1 = require("@shopify/flash-list");
|
|
8
9
|
const DragStateContext_1 = require("../../contexts/DragStateContext");
|
|
9
10
|
/**
|
|
10
11
|
* Hook that encapsulates all drag gesture logic.
|
|
@@ -40,28 +41,58 @@ function useDragGesture(config, callbacks) {
|
|
|
40
41
|
const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
|
|
41
42
|
const translateY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
42
43
|
// Global drag state for coordinating animations across all items
|
|
43
|
-
const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, draggedScale, scrollOffset, dragStartScrollOffset, contentHeight, visibleHeight, listTopY, measuredItemHeight, isDropping, scrollToOffset, config: dragConfig, } = (0, DragStateContext_1.useDragState)();
|
|
44
|
+
const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, draggedScale, scrollOffset, dragStartScrollOffset, contentHeight, visibleHeight, listTopY, measuredItemHeight, isDropping, scrollToOffset, config: dragConfig, itemIndexRegistry, snapshotRegistryForDrag, dragSequence, } = (0, DragStateContext_1.useDragState)();
|
|
44
45
|
// Performance optimization: Track last significant Y position and scroll time
|
|
45
46
|
// to avoid updating on every pixel movement
|
|
46
47
|
const lastSignificantY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
47
48
|
const lastScrollTime = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
49
|
+
// Track current index in SharedValue for worklet access
|
|
50
|
+
// This is critical because FlashList may not re-render items after reorder,
|
|
51
|
+
// leaving the closure-captured `index` stale
|
|
52
|
+
const currentIndex = (0, react_native_reanimated_1.useSharedValue)(index);
|
|
53
|
+
// Use FlashList's useRecyclingState to detect when view is recycled
|
|
54
|
+
// This tracks the previous itemId so we can reset state when the view
|
|
55
|
+
// is reused for a different item during autoscroll
|
|
56
|
+
const [prevItemId, setPrevItemId] = (0, flash_list_1.useRecyclingState)(itemId, [itemId]);
|
|
57
|
+
// Reset translateY and currentIndex when view is recycled
|
|
58
|
+
// Using useLayoutEffect to run before paint and prevent visual glitches
|
|
59
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
60
|
+
if (prevItemId !== itemId) {
|
|
61
|
+
// View was recycled - reset local drag state to prevent stale values
|
|
62
|
+
// from the previous item affecting this item's position
|
|
63
|
+
(0, react_native_reanimated_1.cancelAnimation)(translateY);
|
|
64
|
+
translateY.value = 0;
|
|
65
|
+
isDragging.value = false;
|
|
66
|
+
currentIndex.value = index;
|
|
67
|
+
setPrevItemId(itemId);
|
|
68
|
+
}
|
|
69
|
+
}, [itemId, prevItemId, setPrevItemId, translateY, isDragging, currentIndex, index]);
|
|
70
|
+
// Sync index to SharedValue in useEffect (not during render per Reanimated docs)
|
|
71
|
+
(0, react_1.useEffect)(() => {
|
|
72
|
+
currentIndex.value = index;
|
|
73
|
+
}, [index, currentIndex]);
|
|
48
74
|
// Store current values in refs for stable gesture callbacks
|
|
49
75
|
const dragContextRef = (0, react_1.useRef)({
|
|
50
|
-
index,
|
|
51
76
|
totalItems,
|
|
52
77
|
itemId,
|
|
53
78
|
onReorderByDelta,
|
|
54
79
|
});
|
|
55
80
|
// Keep ref in sync with current values
|
|
56
81
|
dragContextRef.current = {
|
|
57
|
-
index,
|
|
58
82
|
totalItems,
|
|
59
83
|
itemId,
|
|
60
84
|
onReorderByDelta,
|
|
61
85
|
};
|
|
62
86
|
// Calculate new position and call reorder callback
|
|
63
87
|
const handleDragEnd = (0, react_1.useCallback)((finalTranslateY) => {
|
|
64
|
-
const {
|
|
88
|
+
const { totalItems: total, itemId: currentItemId, onReorderByDelta: reorder, } = dragContextRef.current;
|
|
89
|
+
// Use draggedIndex from context, which was set in onStart using the registry
|
|
90
|
+
// This handles FlashList's recycling where closure-captured index may be stale
|
|
91
|
+
const currentIndex = draggedIndex.value;
|
|
92
|
+
if (currentIndex === -1) {
|
|
93
|
+
// Drag was cancelled or reset
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
65
96
|
// Use configured itemHeight for consistent position calculations
|
|
66
97
|
const itemHeight = dragConfig.itemHeight;
|
|
67
98
|
// Calculate how many positions to move based on drag offset
|
|
@@ -70,14 +101,67 @@ function useDragGesture(config, callbacks) {
|
|
|
70
101
|
const newIndex = Math.max(0, Math.min(total - 1, currentIndex + positionDelta));
|
|
71
102
|
const positionChanged = reorder && positionDelta !== 0 && newIndex !== currentIndex;
|
|
72
103
|
if (positionChanged) {
|
|
73
|
-
// Position changes - call reorder
|
|
104
|
+
// Position changes - call reorder and animate back
|
|
105
|
+
// useDropCompensation handles state reset if re-renders happen,
|
|
106
|
+
// but we also animate here as a fallback since FlashList's virtualization
|
|
107
|
+
// may not trigger re-renders when data changes.
|
|
74
108
|
isDropping.value = true;
|
|
75
109
|
onHapticFeedback?.('medium');
|
|
76
110
|
reorder(currentItemId, positionDelta);
|
|
111
|
+
// Note: We no longer call applyReorderToRegistry here.
|
|
112
|
+
// The snapshot-based approach means shift calculations use the frozen
|
|
113
|
+
// snapshot taken at drag start. After drag ends, React re-renders
|
|
114
|
+
// will naturally update the registry via updateItemIndex calls.
|
|
77
115
|
// Reset currentTranslateY to stop shift calculations for other items
|
|
78
|
-
// Note: dragStartScrollOffset is NOT reset here to prevent visual jump
|
|
79
|
-
// It will be reset after the settle animation completes in useDropCompensation
|
|
80
116
|
currentTranslateY.value = 0;
|
|
117
|
+
// Trigger a micro-scroll to force FlashList to refresh its views
|
|
118
|
+
// This helps FlashList notice the data change and re-render
|
|
119
|
+
const currentOffset = scrollOffset.value;
|
|
120
|
+
(0, react_native_worklets_1.scheduleOnRN)(scrollToOffset, currentOffset + 1, false);
|
|
121
|
+
// Scroll back after a short delay
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
scrollToOffset(currentOffset, false);
|
|
124
|
+
}, 16);
|
|
125
|
+
// Fallback: If useDropCompensation doesn't run (no re-render from FlashList),
|
|
126
|
+
// we need to animate the item to its new position and reset state.
|
|
127
|
+
//
|
|
128
|
+
// Since FlashList hasn't re-rendered, the item's base position is still at the
|
|
129
|
+
// old index. To make it appear at the new index, we calculate the target translateY
|
|
130
|
+
// that positions the item correctly.
|
|
131
|
+
//
|
|
132
|
+
// When FlashList eventually re-renders (on scroll or interaction), useDropCompensation
|
|
133
|
+
// will see the index change and animate translateY back to 0.
|
|
134
|
+
(0, react_native_reanimated_1.cancelAnimation)(translateY);
|
|
135
|
+
// Calculate the target translateY to position item at its new index
|
|
136
|
+
// Since FlashList hasn't updated base position, we need:
|
|
137
|
+
// targetTranslateY = (newIndex - currentIndex) * itemHeight
|
|
138
|
+
const targetTranslateY = (newIndex - currentIndex) * itemHeight;
|
|
139
|
+
// Use a delayed animation to give useDropCompensation a chance to run first
|
|
140
|
+
translateY.value = (0, react_native_reanimated_1.withDelay)(150, // Delay to let React re-render and useDropCompensation run first
|
|
141
|
+
(0, react_native_reanimated_1.withTiming)(targetTranslateY, // Animate to the new position
|
|
142
|
+
{ duration: 150, easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease) }, finished => {
|
|
143
|
+
'worklet';
|
|
144
|
+
// Only reset state if still in dropping state (useDropCompensation didn't handle it)
|
|
145
|
+
if (finished && isDropping.value) {
|
|
146
|
+
// Reset all drag state - the item is now visually at its new position
|
|
147
|
+
// via translateY offset. When FlashList re-renders, useDropCompensation
|
|
148
|
+
// will compensate and animate to 0.
|
|
149
|
+
globalIsDragging.value = false;
|
|
150
|
+
isDropping.value = false;
|
|
151
|
+
draggedIndex.value = -1;
|
|
152
|
+
draggedItemId.value = '';
|
|
153
|
+
measuredItemHeight.value = 0;
|
|
154
|
+
dragStartScrollOffset.value = 0;
|
|
155
|
+
// FlashList doesn't re-render visible items after data changes.
|
|
156
|
+
// Since useDropCompensation won't run, we need to animate translateY
|
|
157
|
+
// back to 0 ourselves. This causes a visual "snap" but prevents the
|
|
158
|
+
// item from being stuck floating forever.
|
|
159
|
+
translateY.value = (0, react_native_reanimated_1.withTiming)(0, {
|
|
160
|
+
duration: 200,
|
|
161
|
+
easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}));
|
|
81
165
|
}
|
|
82
166
|
else {
|
|
83
167
|
// Same position - animate back and reset state
|
|
@@ -117,6 +201,17 @@ function useDragGesture(config, callbacks) {
|
|
|
117
201
|
.enabled(enabled)
|
|
118
202
|
.onStart(() => {
|
|
119
203
|
'worklet';
|
|
204
|
+
// Reset any pending drop state from previous drag
|
|
205
|
+
// This is critical: if a new drag starts before the previous drop's
|
|
206
|
+
// fallback completed, isDropping would still be true, causing shift
|
|
207
|
+
// calculations to freeze at their previous values
|
|
208
|
+
if (isDropping.value) {
|
|
209
|
+
isDropping.value = false;
|
|
210
|
+
}
|
|
211
|
+
// Increment drag sequence to force all items to recalculate their shifts
|
|
212
|
+
// This handles the case where items had non-zero shiftY from a previous
|
|
213
|
+
// drag that didn't complete its animations
|
|
214
|
+
dragSequence.value = dragSequence.value + 1;
|
|
120
215
|
// Measure actual item height for accurate drag calculations
|
|
121
216
|
const measured = (0, react_native_reanimated_1.measure)(containerRef);
|
|
122
217
|
if (measured) {
|
|
@@ -126,7 +221,13 @@ function useDragGesture(config, callbacks) {
|
|
|
126
221
|
isDragging.value = true;
|
|
127
222
|
// Global drag state for shift animations
|
|
128
223
|
globalIsDragging.value = true;
|
|
129
|
-
|
|
224
|
+
// Use index from the central registry, which is updated on every item render
|
|
225
|
+
// This handles FlashList's recycling behavior where currentIndex.value might be stale
|
|
226
|
+
const registryIndex = itemIndexRegistry.value[itemId];
|
|
227
|
+
const actualIndex = registryIndex !== undefined ? registryIndex : currentIndex.value;
|
|
228
|
+
draggedIndex.value = actualIndex;
|
|
229
|
+
// Also update currentIndex to stay in sync
|
|
230
|
+
currentIndex.value = actualIndex;
|
|
130
231
|
draggedItemId.value = itemId;
|
|
131
232
|
dragStartScrollOffset.value = scrollOffset.value;
|
|
132
233
|
currentTranslateY.value = 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDragShift.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragShift.ts"],"names":[],"mappings":"
|
|
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,CAkK3E"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.useDragShift = useDragShift;
|
|
4
|
+
const react_1 = require("react");
|
|
4
5
|
const react_native_reanimated_1 = require("react-native-reanimated");
|
|
6
|
+
const flash_list_1 = require("@shopify/flash-list");
|
|
5
7
|
const DragStateContext_1 = require("../../contexts/DragStateContext");
|
|
6
8
|
const constants_1 = require("../../constants");
|
|
7
9
|
/**
|
|
@@ -25,15 +27,43 @@ const constants_1 = require("../../constants");
|
|
|
25
27
|
function useDragShift(config) {
|
|
26
28
|
const { itemId, index } = config;
|
|
27
29
|
// Global drag state for coordinating animations across all items
|
|
28
|
-
const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, scrollOffset, dragStartScrollOffset,
|
|
30
|
+
const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, scrollOffset, dragStartScrollOffset, isDropping, config: dragConfig, itemIndexRegistry, dragSequence, } = (0, DragStateContext_1.useDragState)();
|
|
29
31
|
// Shift animation SharedValue
|
|
30
32
|
const shiftY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
33
|
+
// Track current index in SharedValue for worklet access
|
|
34
|
+
// This is critical because FlashList may not re-render items after reorder,
|
|
35
|
+
// leaving the closure-captured `index` stale
|
|
36
|
+
const currentIndex = (0, react_native_reanimated_1.useSharedValue)(index);
|
|
37
|
+
// Use FlashList's useRecyclingState to detect when view is recycled
|
|
38
|
+
// This tracks the previous itemId so we can reset state when the view
|
|
39
|
+
// is reused for a different item during autoscroll
|
|
40
|
+
const [prevItemId, setPrevItemId] = (0, flash_list_1.useRecyclingState)(itemId, [itemId]);
|
|
41
|
+
// Reset shiftY and update currentIndex immediately when view is recycled
|
|
42
|
+
// Using useLayoutEffect to run before paint and prevent visual glitches
|
|
43
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
44
|
+
if (prevItemId !== itemId) {
|
|
45
|
+
// View was recycled - reset shiftY to prevent stale shift values
|
|
46
|
+
// from the previous item affecting this item's position
|
|
47
|
+
(0, react_native_reanimated_1.cancelAnimation)(shiftY);
|
|
48
|
+
shiftY.value = 0;
|
|
49
|
+
// Update currentIndex immediately for the new item
|
|
50
|
+
currentIndex.value = index;
|
|
51
|
+
setPrevItemId(itemId);
|
|
52
|
+
}
|
|
53
|
+
}, [itemId, prevItemId, setPrevItemId, shiftY, currentIndex, index]);
|
|
54
|
+
// Sync index to SharedValue in useEffect (not during render per Reanimated docs)
|
|
55
|
+
(0, react_1.useEffect)(() => {
|
|
56
|
+
currentIndex.value = index;
|
|
57
|
+
}, [index, currentIndex]);
|
|
31
58
|
// Calculate target shift using useDerivedValue
|
|
32
59
|
// Note: useDerivedValue automatically tracks SharedValue dependencies on UI thread,
|
|
33
60
|
// so we don't need a manual trigger counter. When any read SharedValue changes,
|
|
34
61
|
// Reanimated will re-execute this derived value.
|
|
35
62
|
const targetShiftY = (0, react_native_reanimated_1.useDerivedValue)(() => {
|
|
36
63
|
'worklet';
|
|
64
|
+
// Read dragSequence to force recalculation when a new drag starts
|
|
65
|
+
// This ensures stale shiftY values from previous drags are recalculated
|
|
66
|
+
const _sequence = dragSequence.value;
|
|
37
67
|
// During drop transition, freeze shift values
|
|
38
68
|
if (isDropping.value) {
|
|
39
69
|
return shiftY.value;
|
|
@@ -42,6 +72,10 @@ function useDragShift(config) {
|
|
|
42
72
|
const isDraggingNow = globalIsDragging.value;
|
|
43
73
|
const translateYNow = currentTranslateY.value;
|
|
44
74
|
const currentDraggedItemId = draggedItemId.value;
|
|
75
|
+
// Use index from the central registry if available, which is updated on every item render
|
|
76
|
+
// This handles FlashList's recycling behavior where currentIndex.value might be stale
|
|
77
|
+
const registryIndex = itemIndexRegistry.value[itemId];
|
|
78
|
+
const myIndex = registryIndex !== undefined ? registryIndex : currentIndex.value;
|
|
45
79
|
// If I'm the dragged item, no shift needed
|
|
46
80
|
if (currentDraggedItemId === itemId)
|
|
47
81
|
return 0;
|
|
@@ -71,31 +105,43 @@ function useDragShift(config) {
|
|
|
71
105
|
// Items outside [min(dragIdx, hoverIdx), max(dragIdx, hoverIdx)] can bail early
|
|
72
106
|
const minAffectedIndex = Math.min(currentDraggedIndex, hoveredIndex);
|
|
73
107
|
const maxAffectedIndex = Math.max(currentDraggedIndex, hoveredIndex);
|
|
74
|
-
if (
|
|
108
|
+
if (myIndex < minAffectedIndex || myIndex > maxAffectedIndex) {
|
|
75
109
|
return 0;
|
|
76
110
|
}
|
|
77
111
|
// Moving DOWN: items between original and hovered positions shift UP
|
|
78
112
|
if (hoveredIndex > currentDraggedIndex) {
|
|
79
|
-
if (
|
|
113
|
+
if (myIndex > currentDraggedIndex && myIndex <= hoveredIndex) {
|
|
80
114
|
return -itemHeight;
|
|
81
115
|
}
|
|
82
116
|
}
|
|
83
117
|
// Moving UP: items between hovered and original positions shift DOWN
|
|
84
118
|
else if (hoveredIndex < currentDraggedIndex) {
|
|
85
|
-
if (
|
|
119
|
+
if (myIndex < currentDraggedIndex && myIndex >= hoveredIndex) {
|
|
86
120
|
return itemHeight;
|
|
87
121
|
}
|
|
88
122
|
}
|
|
89
123
|
return 0;
|
|
90
|
-
}, [
|
|
91
|
-
// Animate shift when target changes
|
|
92
|
-
(0, react_native_reanimated_1.useAnimatedReaction)(() =>
|
|
124
|
+
}, [itemId]);
|
|
125
|
+
// Animate shift when target changes or when a new drag starts
|
|
126
|
+
(0, react_native_reanimated_1.useAnimatedReaction)(() => ({
|
|
127
|
+
target: targetShiftY.value,
|
|
128
|
+
sequence: dragSequence.value,
|
|
129
|
+
}), (state, prev) => {
|
|
93
130
|
'worklet';
|
|
94
|
-
|
|
131
|
+
const targetChanged = state.target !== prev?.target;
|
|
132
|
+
const newDragStarted = state.sequence !== prev?.sequence;
|
|
133
|
+
// Animate to target if:
|
|
134
|
+
// 1. The target changed, OR
|
|
135
|
+
// 2. A new drag started and we're not already at the target
|
|
136
|
+
const needsAnimation = targetChanged ||
|
|
137
|
+
(newDragStarted &&
|
|
138
|
+
Math.abs(shiftY.value - state.target) >
|
|
139
|
+
constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD);
|
|
140
|
+
if (needsAnimation) {
|
|
95
141
|
// Cancel any in-flight animation before starting a new one
|
|
96
142
|
// This prevents competing animations and visual glitches
|
|
97
143
|
(0, react_native_reanimated_1.cancelAnimation)(shiftY);
|
|
98
|
-
shiftY.value = (0, react_native_reanimated_1.withTiming)(target, {
|
|
144
|
+
shiftY.value = (0, react_native_reanimated_1.withTiming)(state.target, {
|
|
99
145
|
duration: constants_1.ANIMATION_TIMING.SHIFT_DURATION,
|
|
100
146
|
easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
|
|
101
147
|
});
|
package/lib/types/list.d.ts
CHANGED
|
@@ -115,6 +115,11 @@ export interface AnimatedFlashListProps<T extends AnimatedListItem> extends Omit
|
|
|
115
115
|
onEndReached?: () => void;
|
|
116
116
|
/** Pagination threshold */
|
|
117
117
|
onEndReachedThreshold?: number;
|
|
118
|
+
/**
|
|
119
|
+
* Estimated size of each item (deprecated in FlashList v2, but used as default for drag itemHeight)
|
|
120
|
+
* @deprecated Use config.drag.itemHeight instead
|
|
121
|
+
*/
|
|
122
|
+
estimatedItemSize?: number;
|
|
118
123
|
}
|
|
119
124
|
/**
|
|
120
125
|
* Ref handle for AnimatedFlashList
|
package/lib/types/list.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/types/list.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/types/list.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@souscheflabs/reanimated-flashlist",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "A high-performance animated FlashList with drag-to-reorder and entry/exit animations (New Architecture)",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.js",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"source": "src/index.ts",
|
|
9
9
|
"files": [
|
|
10
10
|
"lib",
|
|
11
|
-
"src"
|
|
11
|
+
"src",
|
|
12
|
+
"assets"
|
|
12
13
|
],
|
|
13
14
|
"sideEffects": false,
|
|
14
15
|
"scripts": {
|
|
@@ -274,16 +274,25 @@ function AnimatedFlashListInner<T extends AnimatedListItem>(
|
|
|
274
274
|
onEndReached,
|
|
275
275
|
onEndReachedThreshold = 0.5,
|
|
276
276
|
contentContainerStyle,
|
|
277
|
+
estimatedItemSize,
|
|
277
278
|
...flashListProps
|
|
278
279
|
} = props;
|
|
279
280
|
|
|
280
281
|
// Merge config with defaults
|
|
282
|
+
// Use estimatedItemSize as default itemHeight if not explicitly configured
|
|
281
283
|
const dragConfig = useMemo(
|
|
282
|
-
() =>
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
284
|
+
() => {
|
|
285
|
+
const mergedConfig = {
|
|
286
|
+
...DEFAULT_DRAG_CONFIG,
|
|
287
|
+
...config?.drag,
|
|
288
|
+
itemHeight: config?.drag?.itemHeight ?? estimatedItemSize ?? DEFAULT_DRAG_CONFIG.itemHeight,
|
|
289
|
+
};
|
|
290
|
+
if (__DEV__) {
|
|
291
|
+
console.log(`[AnimatedFlashList] dragConfig.itemHeight=${mergedConfig.itemHeight}, estimatedItemSize=${estimatedItemSize}, config.drag?.itemHeight=${config?.drag?.itemHeight}`);
|
|
292
|
+
}
|
|
293
|
+
return mergedConfig;
|
|
294
|
+
},
|
|
295
|
+
[config?.drag, estimatedItemSize],
|
|
287
296
|
);
|
|
288
297
|
|
|
289
298
|
// Ref to FlashList
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
useListExitAnimation,
|
|
10
10
|
useListEntryAnimation,
|
|
11
11
|
} from './hooks';
|
|
12
|
-
import { useListAnimationOptional } from './contexts';
|
|
12
|
+
import { useListAnimationOptional, useDragState } from './contexts';
|
|
13
13
|
import type {
|
|
14
14
|
AnimatedListItem,
|
|
15
15
|
AnimatedRenderItemInfo,
|
|
@@ -53,6 +53,19 @@ function AnimatedFlashListItemInner<T extends AnimatedListItem>({
|
|
|
53
53
|
onReorderByDelta,
|
|
54
54
|
onHapticFeedback,
|
|
55
55
|
}: AnimatedFlashListItemProps<T>): React.ReactElement | null {
|
|
56
|
+
// DEBUG: Log renders
|
|
57
|
+
if (__DEV__) {
|
|
58
|
+
console.log(`[AnimatedFlashListItem] RENDER ${item.id} at index=${index}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Register this item's index in the central registry
|
|
62
|
+
// This handles FlashList's recycling behavior where components may not re-render
|
|
63
|
+
// after data changes, leaving their index props stale
|
|
64
|
+
const { updateItemIndex } = useDragState();
|
|
65
|
+
// Call updateItemIndex synchronously during render to ensure the registry
|
|
66
|
+
// is always up-to-date when any item renders
|
|
67
|
+
updateItemIndex(item.id, index);
|
|
68
|
+
|
|
56
69
|
// Animated ref for measuring item height on drag start
|
|
57
70
|
const containerRef = useAnimatedRef<Animated.View>();
|
|
58
71
|
|
|
@@ -46,6 +46,8 @@ export interface DragStateContextValue {
|
|
|
46
46
|
measuredItemHeight: SharedValue<number>;
|
|
47
47
|
/** Flag to freeze shift values during drop transition */
|
|
48
48
|
isDropping: SharedValue<boolean>;
|
|
49
|
+
/** Counter incremented on each drag start, used to force shift recalculation */
|
|
50
|
+
dragSequence: SharedValue<number>;
|
|
49
51
|
/** Register the FlashList ref for autoscroll operations */
|
|
50
52
|
setListRef: (ref: FlashListRef<unknown> | null) => void;
|
|
51
53
|
/** Scroll the list to a specific offset (for autoscroll during drag) */
|
|
@@ -54,6 +56,27 @@ export interface DragStateContextValue {
|
|
|
54
56
|
resetDragState: () => void;
|
|
55
57
|
/** Current drag configuration */
|
|
56
58
|
config: DragConfig;
|
|
59
|
+
/**
|
|
60
|
+
* Index registry for tracking itemId -> index mapping.
|
|
61
|
+
* Updated on every item render to handle FlashList recycling.
|
|
62
|
+
* Stored on UI thread as SharedValue for worklet access.
|
|
63
|
+
*/
|
|
64
|
+
itemIndexRegistry: SharedValue<Record<string, number>>;
|
|
65
|
+
/** Update an item's index in the registry (call from JS thread) */
|
|
66
|
+
updateItemIndex: (itemId: string, index: number) => void;
|
|
67
|
+
/** Get an item's index from the registry */
|
|
68
|
+
getItemIndex: (itemId: string) => number | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Snapshot of itemIndexRegistry taken at drag start.
|
|
71
|
+
* Used during drag to ensure consistent index lookup regardless of
|
|
72
|
+
* React re-renders or FlashList recycling.
|
|
73
|
+
*/
|
|
74
|
+
dragStartIndexSnapshot: SharedValue<Record<string, number>>;
|
|
75
|
+
/**
|
|
76
|
+
* Capture a snapshot of the current itemIndexRegistry for use during drag.
|
|
77
|
+
* Call this at the start of a drag operation.
|
|
78
|
+
*/
|
|
79
|
+
snapshotRegistryForDrag: () => void;
|
|
57
80
|
}
|
|
58
81
|
|
|
59
82
|
const DragStateContext = createContext<DragStateContextValue | null>(null);
|
|
@@ -156,6 +179,17 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
|
|
|
156
179
|
// Flag to freeze shift values during drop transition
|
|
157
180
|
const isDropping = useSharedValue(false);
|
|
158
181
|
|
|
182
|
+
// Counter incremented on each drag start, used to force shift recalculation
|
|
183
|
+
const dragSequence = useSharedValue(0);
|
|
184
|
+
|
|
185
|
+
// Index registry for tracking itemId -> index mapping
|
|
186
|
+
// This handles FlashList's recycling behavior where components may not re-render
|
|
187
|
+
// after data changes, leaving their index props stale
|
|
188
|
+
const itemIndexRegistry = useSharedValue<Record<string, number>>({});
|
|
189
|
+
|
|
190
|
+
// Snapshot of registry taken at drag start for consistent index lookup during drag
|
|
191
|
+
const dragStartIndexSnapshot = useSharedValue<Record<string, number>>({});
|
|
192
|
+
|
|
159
193
|
// Ref to FlashList for autoscroll operations
|
|
160
194
|
const listRef = useRef<FlashListRef<unknown> | null>(null);
|
|
161
195
|
|
|
@@ -169,6 +203,36 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
|
|
|
169
203
|
listRef.current?.scrollToOffset({ offset, animated });
|
|
170
204
|
}, []);
|
|
171
205
|
|
|
206
|
+
// Update an item's index in the registry
|
|
207
|
+
// Called on every item render to keep indices fresh
|
|
208
|
+
const updateItemIndex = useCallback(
|
|
209
|
+
(itemId: string, index: number) => {
|
|
210
|
+
// Only update if the value has changed to avoid unnecessary SharedValue updates
|
|
211
|
+
if (itemIndexRegistry.value[itemId] !== index) {
|
|
212
|
+
itemIndexRegistry.value = {
|
|
213
|
+
...itemIndexRegistry.value,
|
|
214
|
+
[itemId]: index,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
[itemIndexRegistry],
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Get an item's index from the registry
|
|
222
|
+
const getItemIndex = useCallback(
|
|
223
|
+
(itemId: string): number | undefined => {
|
|
224
|
+
return itemIndexRegistry.value[itemId];
|
|
225
|
+
},
|
|
226
|
+
[itemIndexRegistry],
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Capture a snapshot of the registry at drag start
|
|
230
|
+
// This ensures consistent index lookup during the entire drag operation,
|
|
231
|
+
// regardless of React re-renders or FlashList recycling
|
|
232
|
+
const snapshotRegistryForDrag = useCallback(() => {
|
|
233
|
+
dragStartIndexSnapshot.value = { ...itemIndexRegistry.value };
|
|
234
|
+
}, [dragStartIndexSnapshot, itemIndexRegistry]);
|
|
235
|
+
|
|
172
236
|
// Reset drag state after drop
|
|
173
237
|
const resetDragState = useCallback(() => {
|
|
174
238
|
isDragging.value = false;
|
|
@@ -197,10 +261,16 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
|
|
|
197
261
|
listTopY,
|
|
198
262
|
measuredItemHeight,
|
|
199
263
|
isDropping,
|
|
264
|
+
dragSequence,
|
|
200
265
|
setListRef,
|
|
201
266
|
scrollToOffset,
|
|
202
267
|
resetDragState,
|
|
203
268
|
config,
|
|
269
|
+
itemIndexRegistry,
|
|
270
|
+
updateItemIndex,
|
|
271
|
+
getItemIndex,
|
|
272
|
+
dragStartIndexSnapshot,
|
|
273
|
+
snapshotRegistryForDrag,
|
|
204
274
|
}),
|
|
205
275
|
[
|
|
206
276
|
isDragging,
|
|
@@ -215,10 +285,16 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
|
|
|
215
285
|
listTopY,
|
|
216
286
|
measuredItemHeight,
|
|
217
287
|
isDropping,
|
|
288
|
+
dragSequence,
|
|
218
289
|
setListRef,
|
|
219
290
|
scrollToOffset,
|
|
220
291
|
resetDragState,
|
|
221
292
|
config,
|
|
293
|
+
itemIndexRegistry,
|
|
294
|
+
updateItemIndex,
|
|
295
|
+
getItemIndex,
|
|
296
|
+
dragStartIndexSnapshot,
|
|
297
|
+
snapshotRegistryForDrag,
|
|
222
298
|
],
|
|
223
299
|
);
|
|
224
300
|
|
|
@@ -85,25 +85,44 @@ export function useDragAnimatedStyle(
|
|
|
85
85
|
// Animated style for drag offset with scale and shadow
|
|
86
86
|
const dragAnimatedStyle = useAnimatedStyle(() => {
|
|
87
87
|
const isThisItemDragged = draggedItemId.value === itemId;
|
|
88
|
-
|
|
89
|
-
// Keep elevated if: actively dragging OR has offset (animating back)
|
|
90
|
-
const shouldBeElevated =
|
|
91
|
-
isDragging.value ||
|
|
92
|
-
Math.abs(translateY.value) > DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
|
|
88
|
+
const hasSignificantTranslateY = Math.abs(translateY.value) > DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
|
|
93
89
|
|
|
94
90
|
// Calculate scroll delta for position compensation during autoscroll
|
|
95
91
|
const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
|
|
96
92
|
|
|
97
|
-
//
|
|
98
|
-
|
|
93
|
+
// Determine when to use translateY for positioning:
|
|
94
|
+
// 1. Actively being dragged (isThisItemDragged && isDragging)
|
|
95
|
+
// 2. Animating/settling after drop (hasSignificantTranslateY)
|
|
96
|
+
//
|
|
97
|
+
// Case 2 handles the FALLBACK scenario where FlashList doesn't re-render
|
|
98
|
+
// after reorder. The item uses translateY to stay at its new position
|
|
99
|
+
// until FlashList eventually re-renders and useDropCompensation runs.
|
|
100
|
+
const shouldUseDragOffset =
|
|
101
|
+
(isThisItemDragged && isDragging.value) || hasSignificantTranslateY;
|
|
102
|
+
|
|
103
|
+
// Use drag translateY + scroll compensation for dragged/settling item,
|
|
104
|
+
// shiftY for others or for items at rest
|
|
105
|
+
const yOffset = shouldUseDragOffset
|
|
99
106
|
? translateY.value + scrollDelta
|
|
100
107
|
: shiftY.value;
|
|
101
108
|
|
|
109
|
+
// Only apply visual effects (scale, elevation, zIndex) when ACTIVELY dragging
|
|
110
|
+
// Not when just settling at new position after drop
|
|
111
|
+
const isActivelyDragging = isThisItemDragged && isDragging.value;
|
|
112
|
+
|
|
113
|
+
// Keep elevated zIndex when actively dragging OR when settling at new position
|
|
114
|
+
// This prevents the dropped item from appearing behind other items
|
|
115
|
+
// Note: We use hasSignificantTranslateY alone (not isThisItemDragged) because
|
|
116
|
+
// after FALLBACK completes, draggedItemId is reset to '' but translateY remains
|
|
117
|
+
const shouldBeElevated = isActivelyDragging || hasSignificantTranslateY;
|
|
118
|
+
|
|
102
119
|
return {
|
|
103
120
|
transform: [
|
|
104
121
|
{ translateY: yOffset },
|
|
105
|
-
|
|
122
|
+
// Only apply drag scale when actively dragging
|
|
123
|
+
{ scale: isActivelyDragging ? draggedScale.value : 1 },
|
|
106
124
|
],
|
|
125
|
+
// Elevate zIndex when dragging or settling at new position
|
|
107
126
|
zIndex: shouldBeElevated ? DRAG_THRESHOLDS.ELEVATED_Z_INDEX : 0,
|
|
108
127
|
// Use animated shadow opacity for smooth transitions
|
|
109
128
|
shadowOpacity: animatedShadowOpacity.value,
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { useCallback, useRef, useMemo } from 'react';
|
|
1
|
+
import { useCallback, useRef, useMemo, useEffect, useLayoutEffect } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
useSharedValue,
|
|
4
4
|
measure,
|
|
5
5
|
withSpring,
|
|
6
6
|
withTiming,
|
|
7
|
+
withDelay,
|
|
7
8
|
cancelAnimation,
|
|
8
9
|
Easing,
|
|
9
10
|
interpolate,
|
|
10
11
|
} from 'react-native-reanimated';
|
|
11
12
|
import { Gesture } from 'react-native-gesture-handler';
|
|
12
13
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
14
|
+
import { useRecyclingState } from '@shopify/flash-list';
|
|
13
15
|
import { useDragState } from '../../contexts/DragStateContext';
|
|
14
16
|
import type {
|
|
15
17
|
UseDragGestureConfig,
|
|
@@ -71,6 +73,9 @@ export function useDragGesture(
|
|
|
71
73
|
isDropping,
|
|
72
74
|
scrollToOffset,
|
|
73
75
|
config: dragConfig,
|
|
76
|
+
itemIndexRegistry,
|
|
77
|
+
snapshotRegistryForDrag,
|
|
78
|
+
dragSequence,
|
|
74
79
|
} = useDragState();
|
|
75
80
|
|
|
76
81
|
// Performance optimization: Track last significant Y position and scroll time
|
|
@@ -78,9 +83,37 @@ export function useDragGesture(
|
|
|
78
83
|
const lastSignificantY = useSharedValue(0);
|
|
79
84
|
const lastScrollTime = useSharedValue(0);
|
|
80
85
|
|
|
86
|
+
// Track current index in SharedValue for worklet access
|
|
87
|
+
// This is critical because FlashList may not re-render items after reorder,
|
|
88
|
+
// leaving the closure-captured `index` stale
|
|
89
|
+
const currentIndex = useSharedValue(index);
|
|
90
|
+
|
|
91
|
+
// Use FlashList's useRecyclingState to detect when view is recycled
|
|
92
|
+
// This tracks the previous itemId so we can reset state when the view
|
|
93
|
+
// is reused for a different item during autoscroll
|
|
94
|
+
const [prevItemId, setPrevItemId] = useRecyclingState(itemId, [itemId]);
|
|
95
|
+
|
|
96
|
+
// Reset translateY and currentIndex when view is recycled
|
|
97
|
+
// Using useLayoutEffect to run before paint and prevent visual glitches
|
|
98
|
+
useLayoutEffect(() => {
|
|
99
|
+
if (prevItemId !== itemId) {
|
|
100
|
+
// View was recycled - reset local drag state to prevent stale values
|
|
101
|
+
// from the previous item affecting this item's position
|
|
102
|
+
cancelAnimation(translateY);
|
|
103
|
+
translateY.value = 0;
|
|
104
|
+
isDragging.value = false;
|
|
105
|
+
currentIndex.value = index;
|
|
106
|
+
setPrevItemId(itemId);
|
|
107
|
+
}
|
|
108
|
+
}, [itemId, prevItemId, setPrevItemId, translateY, isDragging, currentIndex, index]);
|
|
109
|
+
|
|
110
|
+
// Sync index to SharedValue in useEffect (not during render per Reanimated docs)
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
currentIndex.value = index;
|
|
113
|
+
}, [index, currentIndex]);
|
|
114
|
+
|
|
81
115
|
// Store current values in refs for stable gesture callbacks
|
|
82
116
|
const dragContextRef = useRef({
|
|
83
|
-
index,
|
|
84
117
|
totalItems,
|
|
85
118
|
itemId,
|
|
86
119
|
onReorderByDelta,
|
|
@@ -88,7 +121,6 @@ export function useDragGesture(
|
|
|
88
121
|
|
|
89
122
|
// Keep ref in sync with current values
|
|
90
123
|
dragContextRef.current = {
|
|
91
|
-
index,
|
|
92
124
|
totalItems,
|
|
93
125
|
itemId,
|
|
94
126
|
onReorderByDelta,
|
|
@@ -98,12 +130,19 @@ export function useDragGesture(
|
|
|
98
130
|
const handleDragEnd = useCallback(
|
|
99
131
|
(finalTranslateY: number) => {
|
|
100
132
|
const {
|
|
101
|
-
index: currentIndex,
|
|
102
133
|
totalItems: total,
|
|
103
134
|
itemId: currentItemId,
|
|
104
135
|
onReorderByDelta: reorder,
|
|
105
136
|
} = dragContextRef.current;
|
|
106
137
|
|
|
138
|
+
// Use draggedIndex from context, which was set in onStart using the registry
|
|
139
|
+
// This handles FlashList's recycling where closure-captured index may be stale
|
|
140
|
+
const currentIndex = draggedIndex.value;
|
|
141
|
+
if (currentIndex === -1) {
|
|
142
|
+
// Drag was cancelled or reset
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
107
146
|
// Use configured itemHeight for consistent position calculations
|
|
108
147
|
const itemHeight = dragConfig.itemHeight;
|
|
109
148
|
|
|
@@ -119,15 +158,79 @@ export function useDragGesture(
|
|
|
119
158
|
reorder && positionDelta !== 0 && newIndex !== currentIndex;
|
|
120
159
|
|
|
121
160
|
if (positionChanged) {
|
|
122
|
-
// Position changes - call reorder
|
|
161
|
+
// Position changes - call reorder and animate back
|
|
162
|
+
// useDropCompensation handles state reset if re-renders happen,
|
|
163
|
+
// but we also animate here as a fallback since FlashList's virtualization
|
|
164
|
+
// may not trigger re-renders when data changes.
|
|
123
165
|
isDropping.value = true;
|
|
124
166
|
onHapticFeedback?.('medium');
|
|
125
167
|
reorder(currentItemId, positionDelta);
|
|
126
168
|
|
|
169
|
+
// Note: We no longer call applyReorderToRegistry here.
|
|
170
|
+
// The snapshot-based approach means shift calculations use the frozen
|
|
171
|
+
// snapshot taken at drag start. After drag ends, React re-renders
|
|
172
|
+
// will naturally update the registry via updateItemIndex calls.
|
|
173
|
+
|
|
127
174
|
// Reset currentTranslateY to stop shift calculations for other items
|
|
128
|
-
// Note: dragStartScrollOffset is NOT reset here to prevent visual jump
|
|
129
|
-
// It will be reset after the settle animation completes in useDropCompensation
|
|
130
175
|
currentTranslateY.value = 0;
|
|
176
|
+
|
|
177
|
+
// Trigger a micro-scroll to force FlashList to refresh its views
|
|
178
|
+
// This helps FlashList notice the data change and re-render
|
|
179
|
+
const currentOffset = scrollOffset.value;
|
|
180
|
+
scheduleOnRN(scrollToOffset, currentOffset + 1, false);
|
|
181
|
+
// Scroll back after a short delay
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
scrollToOffset(currentOffset, false);
|
|
184
|
+
}, 16);
|
|
185
|
+
|
|
186
|
+
// Fallback: If useDropCompensation doesn't run (no re-render from FlashList),
|
|
187
|
+
// we need to animate the item to its new position and reset state.
|
|
188
|
+
//
|
|
189
|
+
// Since FlashList hasn't re-rendered, the item's base position is still at the
|
|
190
|
+
// old index. To make it appear at the new index, we calculate the target translateY
|
|
191
|
+
// that positions the item correctly.
|
|
192
|
+
//
|
|
193
|
+
// When FlashList eventually re-renders (on scroll or interaction), useDropCompensation
|
|
194
|
+
// will see the index change and animate translateY back to 0.
|
|
195
|
+
cancelAnimation(translateY);
|
|
196
|
+
|
|
197
|
+
// Calculate the target translateY to position item at its new index
|
|
198
|
+
// Since FlashList hasn't updated base position, we need:
|
|
199
|
+
// targetTranslateY = (newIndex - currentIndex) * itemHeight
|
|
200
|
+
const targetTranslateY = (newIndex - currentIndex) * itemHeight;
|
|
201
|
+
|
|
202
|
+
// Use a delayed animation to give useDropCompensation a chance to run first
|
|
203
|
+
translateY.value = withDelay(
|
|
204
|
+
150, // Delay to let React re-render and useDropCompensation run first
|
|
205
|
+
withTiming(
|
|
206
|
+
targetTranslateY, // Animate to the new position
|
|
207
|
+
{ duration: 150, easing: Easing.out(Easing.ease) },
|
|
208
|
+
finished => {
|
|
209
|
+
'worklet';
|
|
210
|
+
// Only reset state if still in dropping state (useDropCompensation didn't handle it)
|
|
211
|
+
if (finished && isDropping.value) {
|
|
212
|
+
// Reset all drag state - the item is now visually at its new position
|
|
213
|
+
// via translateY offset. When FlashList re-renders, useDropCompensation
|
|
214
|
+
// will compensate and animate to 0.
|
|
215
|
+
globalIsDragging.value = false;
|
|
216
|
+
isDropping.value = false;
|
|
217
|
+
draggedIndex.value = -1;
|
|
218
|
+
draggedItemId.value = '';
|
|
219
|
+
measuredItemHeight.value = 0;
|
|
220
|
+
dragStartScrollOffset.value = 0;
|
|
221
|
+
|
|
222
|
+
// FlashList doesn't re-render visible items after data changes.
|
|
223
|
+
// Since useDropCompensation won't run, we need to animate translateY
|
|
224
|
+
// back to 0 ourselves. This causes a visual "snap" but prevents the
|
|
225
|
+
// item from being stuck floating forever.
|
|
226
|
+
translateY.value = withTiming(0, {
|
|
227
|
+
duration: 200,
|
|
228
|
+
easing: Easing.out(Easing.ease),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
),
|
|
233
|
+
);
|
|
131
234
|
} else {
|
|
132
235
|
// Same position - animate back and reset state
|
|
133
236
|
// Cancel any existing animation before starting the return animation
|
|
@@ -176,6 +279,19 @@ export function useDragGesture(
|
|
|
176
279
|
.enabled(enabled)
|
|
177
280
|
.onStart(() => {
|
|
178
281
|
'worklet';
|
|
282
|
+
// Reset any pending drop state from previous drag
|
|
283
|
+
// This is critical: if a new drag starts before the previous drop's
|
|
284
|
+
// fallback completed, isDropping would still be true, causing shift
|
|
285
|
+
// calculations to freeze at their previous values
|
|
286
|
+
if (isDropping.value) {
|
|
287
|
+
isDropping.value = false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Increment drag sequence to force all items to recalculate their shifts
|
|
291
|
+
// This handles the case where items had non-zero shiftY from a previous
|
|
292
|
+
// drag that didn't complete its animations
|
|
293
|
+
dragSequence.value = dragSequence.value + 1;
|
|
294
|
+
|
|
179
295
|
// Measure actual item height for accurate drag calculations
|
|
180
296
|
const measured = measure(containerRef);
|
|
181
297
|
if (measured) {
|
|
@@ -186,7 +302,14 @@ export function useDragGesture(
|
|
|
186
302
|
isDragging.value = true;
|
|
187
303
|
// Global drag state for shift animations
|
|
188
304
|
globalIsDragging.value = true;
|
|
189
|
-
|
|
305
|
+
// Use index from the central registry, which is updated on every item render
|
|
306
|
+
// This handles FlashList's recycling behavior where currentIndex.value might be stale
|
|
307
|
+
const registryIndex = itemIndexRegistry.value[itemId];
|
|
308
|
+
const actualIndex =
|
|
309
|
+
registryIndex !== undefined ? registryIndex : currentIndex.value;
|
|
310
|
+
draggedIndex.value = actualIndex;
|
|
311
|
+
// Also update currentIndex to stay in sync
|
|
312
|
+
currentIndex.value = actualIndex;
|
|
190
313
|
draggedItemId.value = itemId;
|
|
191
314
|
dragStartScrollOffset.value = scrollOffset.value;
|
|
192
315
|
currentTranslateY.value = 0;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect } from 'react';
|
|
1
2
|
import {
|
|
2
3
|
useSharedValue,
|
|
3
4
|
useDerivedValue,
|
|
@@ -6,6 +7,7 @@ import {
|
|
|
6
7
|
cancelAnimation,
|
|
7
8
|
Easing,
|
|
8
9
|
} from 'react-native-reanimated';
|
|
10
|
+
import { useRecyclingState } from '@shopify/flash-list';
|
|
9
11
|
import { useDragState } from '../../contexts/DragStateContext';
|
|
10
12
|
import { ANIMATION_TIMING, DRAG_THRESHOLDS } from '../../constants';
|
|
11
13
|
import type { UseDragShiftConfig, UseDragShiftResult } from '../../types';
|
|
@@ -39,20 +41,54 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
|
|
|
39
41
|
currentTranslateY,
|
|
40
42
|
scrollOffset,
|
|
41
43
|
dragStartScrollOffset,
|
|
42
|
-
measuredItemHeight,
|
|
43
44
|
isDropping,
|
|
44
45
|
config: dragConfig,
|
|
46
|
+
itemIndexRegistry,
|
|
47
|
+
dragSequence,
|
|
45
48
|
} = useDragState();
|
|
46
49
|
|
|
47
50
|
// Shift animation SharedValue
|
|
48
51
|
const shiftY = useSharedValue(0);
|
|
49
52
|
|
|
53
|
+
// Track current index in SharedValue for worklet access
|
|
54
|
+
// This is critical because FlashList may not re-render items after reorder,
|
|
55
|
+
// leaving the closure-captured `index` stale
|
|
56
|
+
const currentIndex = useSharedValue(index);
|
|
57
|
+
|
|
58
|
+
// Use FlashList's useRecyclingState to detect when view is recycled
|
|
59
|
+
// This tracks the previous itemId so we can reset state when the view
|
|
60
|
+
// is reused for a different item during autoscroll
|
|
61
|
+
const [prevItemId, setPrevItemId] = useRecyclingState(itemId, [itemId]);
|
|
62
|
+
|
|
63
|
+
// Reset shiftY and update currentIndex immediately when view is recycled
|
|
64
|
+
// Using useLayoutEffect to run before paint and prevent visual glitches
|
|
65
|
+
useLayoutEffect(() => {
|
|
66
|
+
if (prevItemId !== itemId) {
|
|
67
|
+
// View was recycled - reset shiftY to prevent stale shift values
|
|
68
|
+
// from the previous item affecting this item's position
|
|
69
|
+
cancelAnimation(shiftY);
|
|
70
|
+
shiftY.value = 0;
|
|
71
|
+
// Update currentIndex immediately for the new item
|
|
72
|
+
currentIndex.value = index;
|
|
73
|
+
setPrevItemId(itemId);
|
|
74
|
+
}
|
|
75
|
+
}, [itemId, prevItemId, setPrevItemId, shiftY, currentIndex, index]);
|
|
76
|
+
|
|
77
|
+
// Sync index to SharedValue in useEffect (not during render per Reanimated docs)
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
currentIndex.value = index;
|
|
80
|
+
}, [index, currentIndex]);
|
|
81
|
+
|
|
50
82
|
// Calculate target shift using useDerivedValue
|
|
51
83
|
// Note: useDerivedValue automatically tracks SharedValue dependencies on UI thread,
|
|
52
84
|
// so we don't need a manual trigger counter. When any read SharedValue changes,
|
|
53
85
|
// Reanimated will re-execute this derived value.
|
|
54
86
|
const targetShiftY = useDerivedValue(() => {
|
|
55
87
|
'worklet';
|
|
88
|
+
// Read dragSequence to force recalculation when a new drag starts
|
|
89
|
+
// This ensures stale shiftY values from previous drags are recalculated
|
|
90
|
+
const _sequence = dragSequence.value;
|
|
91
|
+
|
|
56
92
|
// During drop transition, freeze shift values
|
|
57
93
|
if (isDropping.value) {
|
|
58
94
|
return shiftY.value;
|
|
@@ -62,6 +98,11 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
|
|
|
62
98
|
const isDraggingNow = globalIsDragging.value;
|
|
63
99
|
const translateYNow = currentTranslateY.value;
|
|
64
100
|
const currentDraggedItemId = draggedItemId.value;
|
|
101
|
+
// Use index from the central registry if available, which is updated on every item render
|
|
102
|
+
// This handles FlashList's recycling behavior where currentIndex.value might be stale
|
|
103
|
+
const registryIndex = itemIndexRegistry.value[itemId];
|
|
104
|
+
const myIndex =
|
|
105
|
+
registryIndex !== undefined ? registryIndex : currentIndex.value;
|
|
65
106
|
|
|
66
107
|
// If I'm the dragged item, no shift needed
|
|
67
108
|
if (currentDraggedItemId === itemId) return 0;
|
|
@@ -75,6 +116,7 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
|
|
|
75
116
|
// The configured itemHeight (content + margins) provides reliable, predictable behavior.
|
|
76
117
|
const itemHeight = dragConfig.itemHeight;
|
|
77
118
|
|
|
119
|
+
|
|
78
120
|
// Calculate effective translateY including scroll delta
|
|
79
121
|
const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
|
|
80
122
|
const effectiveTranslateY = translateYNow + scrollDelta;
|
|
@@ -97,36 +139,51 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
|
|
|
97
139
|
// Items outside [min(dragIdx, hoverIdx), max(dragIdx, hoverIdx)] can bail early
|
|
98
140
|
const minAffectedIndex = Math.min(currentDraggedIndex, hoveredIndex);
|
|
99
141
|
const maxAffectedIndex = Math.max(currentDraggedIndex, hoveredIndex);
|
|
100
|
-
if (
|
|
142
|
+
if (myIndex < minAffectedIndex || myIndex > maxAffectedIndex) {
|
|
101
143
|
return 0;
|
|
102
144
|
}
|
|
103
145
|
|
|
104
146
|
// Moving DOWN: items between original and hovered positions shift UP
|
|
105
147
|
if (hoveredIndex > currentDraggedIndex) {
|
|
106
|
-
if (
|
|
148
|
+
if (myIndex > currentDraggedIndex && myIndex <= hoveredIndex) {
|
|
107
149
|
return -itemHeight;
|
|
108
150
|
}
|
|
109
151
|
}
|
|
110
152
|
// Moving UP: items between hovered and original positions shift DOWN
|
|
111
153
|
else if (hoveredIndex < currentDraggedIndex) {
|
|
112
|
-
if (
|
|
154
|
+
if (myIndex < currentDraggedIndex && myIndex >= hoveredIndex) {
|
|
113
155
|
return itemHeight;
|
|
114
156
|
}
|
|
115
157
|
}
|
|
116
158
|
|
|
117
159
|
return 0;
|
|
118
|
-
}, [
|
|
160
|
+
}, [itemId]);
|
|
119
161
|
|
|
120
|
-
// Animate shift when target changes
|
|
162
|
+
// Animate shift when target changes or when a new drag starts
|
|
121
163
|
useAnimatedReaction(
|
|
122
|
-
() =>
|
|
123
|
-
|
|
164
|
+
() => ({
|
|
165
|
+
target: targetShiftY.value,
|
|
166
|
+
sequence: dragSequence.value,
|
|
167
|
+
}),
|
|
168
|
+
(state, prev) => {
|
|
124
169
|
'worklet';
|
|
125
|
-
|
|
170
|
+
const targetChanged = state.target !== prev?.target;
|
|
171
|
+
const newDragStarted = state.sequence !== prev?.sequence;
|
|
172
|
+
|
|
173
|
+
// Animate to target if:
|
|
174
|
+
// 1. The target changed, OR
|
|
175
|
+
// 2. A new drag started and we're not already at the target
|
|
176
|
+
const needsAnimation =
|
|
177
|
+
targetChanged ||
|
|
178
|
+
(newDragStarted &&
|
|
179
|
+
Math.abs(shiftY.value - state.target) >
|
|
180
|
+
DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD);
|
|
181
|
+
|
|
182
|
+
if (needsAnimation) {
|
|
126
183
|
// Cancel any in-flight animation before starting a new one
|
|
127
184
|
// This prevents competing animations and visual glitches
|
|
128
185
|
cancelAnimation(shiftY);
|
|
129
|
-
shiftY.value = withTiming(target, {
|
|
186
|
+
shiftY.value = withTiming(state.target, {
|
|
130
187
|
duration: ANIMATION_TIMING.SHIFT_DURATION,
|
|
131
188
|
easing: Easing.out(Easing.ease),
|
|
132
189
|
});
|
package/src/types/list.ts
CHANGED
|
@@ -155,6 +155,12 @@ export interface AnimatedFlashListProps<T extends AnimatedListItem>
|
|
|
155
155
|
|
|
156
156
|
/** Pagination threshold */
|
|
157
157
|
onEndReachedThreshold?: number;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Estimated size of each item (deprecated in FlashList v2, but used as default for drag itemHeight)
|
|
161
|
+
* @deprecated Use config.drag.itemHeight instead
|
|
162
|
+
*/
|
|
163
|
+
estimatedItemSize?: number;
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
/**
|