@souscheflabs/reanimated-flashlist 0.2.0 → 0.2.3
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 +0 -2
- package/lib/contexts/DragStateContext.d.ts.map +1 -1
- package/lib/contexts/DragStateContext.js +0 -4
- package/lib/hooks/animations/useListExitAnimation.d.ts.map +1 -1
- package/lib/hooks/animations/useListExitAnimation.js +2 -0
- package/lib/hooks/drag/useDragGesture.d.ts.map +1 -1
- package/lib/hooks/drag/useDragGesture.js +33 -8
- package/lib/hooks/drag/useDragShift.d.ts.map +1 -1
- package/lib/hooks/drag/useDragShift.js +17 -19
- package/lib/hooks/drag/useDropCompensation.d.ts.map +1 -1
- package/lib/hooks/drag/useDropCompensation.js +1 -4
- package/package.json +1 -1
- package/src/contexts/DragStateContext.tsx +0 -7
- package/src/hooks/animations/useListExitAnimation.ts +4 -0
- package/src/hooks/drag/useDragGesture.ts +38 -8
- package/src/hooks/drag/useDragShift.ts +18 -20
- package/src/hooks/drag/useDropCompensation.ts +0 -4
|
@@ -33,8 +33,6 @@ export interface DragStateContextValue {
|
|
|
33
33
|
visibleHeight: SharedValue<number>;
|
|
34
34
|
/** Y position of FlashList top on screen (for autoscroll coordinate conversion) */
|
|
35
35
|
listTopY: SharedValue<number>;
|
|
36
|
-
/** Counter incremented on every drag state change to force useDerivedValue re-evaluation */
|
|
37
|
-
dragUpdateTrigger: SharedValue<number>;
|
|
38
36
|
/** Measured height of the dragged item (for dynamic height calculations) */
|
|
39
37
|
measuredItemHeight: SharedValue<number>;
|
|
40
38
|
/** Flag to freeze shift values during drop transition */
|
|
@@ -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,
|
|
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;CACpB;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,CAsG9D,CAAC"}
|
|
@@ -102,8 +102,6 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
102
102
|
const contentHeight = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
103
103
|
const visibleHeight = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
104
104
|
const listTopY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
105
|
-
// Counter to force useDerivedValue re-evaluation on every drag state change
|
|
106
|
-
const dragUpdateTrigger = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
107
105
|
// Measured height of dragged item (0 = use fallback from config)
|
|
108
106
|
const measuredItemHeight = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
109
107
|
// Flag to freeze shift values during drop transition
|
|
@@ -142,7 +140,6 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
142
140
|
contentHeight,
|
|
143
141
|
visibleHeight,
|
|
144
142
|
listTopY,
|
|
145
|
-
dragUpdateTrigger,
|
|
146
143
|
measuredItemHeight,
|
|
147
144
|
isDropping,
|
|
148
145
|
setListRef,
|
|
@@ -160,7 +157,6 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
|
160
157
|
contentHeight,
|
|
161
158
|
visibleHeight,
|
|
162
159
|
listTopY,
|
|
163
|
-
dragUpdateTrigger,
|
|
164
160
|
measuredItemHeight,
|
|
165
161
|
isDropping,
|
|
166
162
|
setListRef,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useListExitAnimation.d.ts","sourceRoot":"","sources":["../../../src/hooks/animations/useListExitAnimation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useListExitAnimation.d.ts","sourceRoot":"","sources":["../../../src/hooks/animations/useListExitAnimation.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAE3E;;GAEG;AACH,UAAU,0BAA0B;IAClC,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAQD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,SAAS,0BAA0B;;;;;;;;;;;6BAyEpB,kBAAkB,cACjB,MAAM,IAAI,WACd,mBAAmB;;CAwDhC,CAAC"}
|
|
@@ -126,6 +126,8 @@ const useListExitAnimation = (itemId, config) => {
|
|
|
126
126
|
// Set animation config SharedValues BEFORE starting animation
|
|
127
127
|
slideDistance.value = animConfig.slide.distance;
|
|
128
128
|
scaleToValue.value = animConfig.scale.toValue;
|
|
129
|
+
// Cancel any existing animation before starting the exit animation
|
|
130
|
+
(0, react_native_reanimated_1.cancelAnimation)(exitDirection);
|
|
129
131
|
// Animate exitDirection.value from 0 to 1 (or -1)
|
|
130
132
|
exitDirection.value = (0, react_native_reanimated_1.withTiming)(direction, { duration: animConfig.slide.duration, easing: animations_1.standardEasing }, finished => {
|
|
131
133
|
'worklet';
|
|
@@ -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":"AAaA,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,CAuPtB"}
|
|
@@ -40,7 +40,11 @@ function useDragGesture(config, callbacks) {
|
|
|
40
40
|
const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
|
|
41
41
|
const translateY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
42
42
|
// Global drag state for coordinating animations across all items
|
|
43
|
-
const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, draggedScale, scrollOffset, dragStartScrollOffset, contentHeight, visibleHeight, listTopY,
|
|
43
|
+
const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, draggedScale, scrollOffset, dragStartScrollOffset, contentHeight, visibleHeight, listTopY, measuredItemHeight, isDropping, scrollToOffset, config: dragConfig, } = (0, DragStateContext_1.useDragState)();
|
|
44
|
+
// Performance optimization: Track last significant Y position and scroll time
|
|
45
|
+
// to avoid updating on every pixel movement
|
|
46
|
+
const lastSignificantY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
47
|
+
const lastScrollTime = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
44
48
|
// Store current values in refs for stable gesture callbacks
|
|
45
49
|
const dragContextRef = (0, react_1.useRef)({
|
|
46
50
|
index,
|
|
@@ -78,6 +82,8 @@ function useDragGesture(config, callbacks) {
|
|
|
78
82
|
}
|
|
79
83
|
else {
|
|
80
84
|
// Same position - animate back and reset state
|
|
85
|
+
// Cancel any existing animation before starting the return animation
|
|
86
|
+
(0, react_native_reanimated_1.cancelAnimation)(translateY);
|
|
81
87
|
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 => {
|
|
82
88
|
'worklet';
|
|
83
89
|
if (finished) {
|
|
@@ -87,7 +93,6 @@ function useDragGesture(config, callbacks) {
|
|
|
87
93
|
currentTranslateY.value = 0;
|
|
88
94
|
dragStartScrollOffset.value = 0;
|
|
89
95
|
measuredItemHeight.value = 0;
|
|
90
|
-
dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
|
|
91
96
|
}
|
|
92
97
|
});
|
|
93
98
|
}
|
|
@@ -100,7 +105,6 @@ function useDragGesture(config, callbacks) {
|
|
|
100
105
|
draggedItemId,
|
|
101
106
|
currentTranslateY,
|
|
102
107
|
dragStartScrollOffset,
|
|
103
|
-
dragUpdateTrigger,
|
|
104
108
|
isDropping,
|
|
105
109
|
onHapticFeedback,
|
|
106
110
|
]);
|
|
@@ -127,19 +131,32 @@ function useDragGesture(config, callbacks) {
|
|
|
127
131
|
draggedItemId.value = itemId;
|
|
128
132
|
dragStartScrollOffset.value = scrollOffset.value;
|
|
129
133
|
currentTranslateY.value = 0;
|
|
134
|
+
// Reset performance tracking values
|
|
135
|
+
lastSignificantY.value = 0;
|
|
136
|
+
lastScrollTime.value = Date.now();
|
|
130
137
|
draggedScale.value = (0, react_native_reanimated_1.withSpring)(dragConfig.dragScale, {
|
|
131
138
|
damping: 15,
|
|
132
139
|
stiffness: 400,
|
|
133
140
|
});
|
|
134
|
-
dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
|
|
135
141
|
(0, react_native_worklets_1.scheduleOnRN)(triggerLightHaptic);
|
|
136
142
|
})
|
|
137
143
|
.onUpdate(event => {
|
|
138
144
|
'worklet';
|
|
145
|
+
// Always update translateY for smooth visual feedback
|
|
139
146
|
translateY.value = event.translationY;
|
|
140
|
-
currentTranslateY
|
|
141
|
-
|
|
147
|
+
// Performance optimization: Only update currentTranslateY (which triggers
|
|
148
|
+
// shift calculations in other items) when movement is significant (>5px)
|
|
149
|
+
const POSITION_THRESHOLD = 5;
|
|
150
|
+
const deltaFromLast = Math.abs(event.translationY - lastSignificantY.value);
|
|
151
|
+
if (deltaFromLast >= POSITION_THRESHOLD) {
|
|
152
|
+
currentTranslateY.value = event.translationY;
|
|
153
|
+
lastSignificantY.value = event.translationY;
|
|
154
|
+
}
|
|
142
155
|
// Autoscroll when dragging near edges
|
|
156
|
+
// Performance optimization: Throttle to ~30fps (33ms interval)
|
|
157
|
+
const SCROLL_THROTTLE_MS = 33;
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
const timeSinceLastScroll = now - lastScrollTime.value;
|
|
143
160
|
const fingerInList = event.absoluteY - listTopY.value;
|
|
144
161
|
const topEdge = dragConfig.edgeThreshold;
|
|
145
162
|
const bottomEdge = visibleHeight.value - dragConfig.edgeThreshold;
|
|
@@ -147,7 +164,11 @@ function useDragGesture(config, callbacks) {
|
|
|
147
164
|
const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [0, topEdge], [dragConfig.maxScrollSpeed, 0], 'clamp');
|
|
148
165
|
const newOffset = Math.max(0, scrollOffset.value - speed);
|
|
149
166
|
scrollOffset.value = newOffset;
|
|
150
|
-
|
|
167
|
+
// Throttle actual scroll calls to reduce JS thread pressure
|
|
168
|
+
if (timeSinceLastScroll >= SCROLL_THROTTLE_MS) {
|
|
169
|
+
lastScrollTime.value = now;
|
|
170
|
+
(0, react_native_worklets_1.scheduleOnRN)(scrollToOffset, newOffset);
|
|
171
|
+
}
|
|
151
172
|
}
|
|
152
173
|
else if (fingerInList > bottomEdge) {
|
|
153
174
|
const maxOffset = Math.max(0, contentHeight.value - visibleHeight.value);
|
|
@@ -155,7 +176,11 @@ function useDragGesture(config, callbacks) {
|
|
|
155
176
|
const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [bottomEdge, visibleHeight.value], [0, dragConfig.maxScrollSpeed], 'clamp');
|
|
156
177
|
const newOffset = Math.min(maxOffset, scrollOffset.value + speed);
|
|
157
178
|
scrollOffset.value = newOffset;
|
|
158
|
-
|
|
179
|
+
// Throttle actual scroll calls to reduce JS thread pressure
|
|
180
|
+
if (timeSinceLastScroll >= SCROLL_THROTTLE_MS) {
|
|
181
|
+
lastScrollTime.value = now;
|
|
182
|
+
(0, react_native_worklets_1.scheduleOnRN)(scrollToOffset, newOffset);
|
|
183
|
+
}
|
|
159
184
|
}
|
|
160
185
|
}
|
|
161
186
|
})
|
|
@@ -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":"AAUA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE1E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,CA2G3E"}
|
|
@@ -25,29 +25,15 @@ const constants_1 = require("../../constants");
|
|
|
25
25
|
function useDragShift(config) {
|
|
26
26
|
const { itemId, index } = config;
|
|
27
27
|
// Global drag state for coordinating animations across all items
|
|
28
|
-
const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, scrollOffset, dragStartScrollOffset,
|
|
28
|
+
const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, scrollOffset, dragStartScrollOffset, measuredItemHeight, isDropping, config: dragConfig, } = (0, DragStateContext_1.useDragState)();
|
|
29
29
|
// Shift animation SharedValue
|
|
30
30
|
const shiftY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
31
31
|
// Calculate target shift using useDerivedValue
|
|
32
|
+
// Note: useDerivedValue automatically tracks SharedValue dependencies on UI thread,
|
|
33
|
+
// so we don't need a manual trigger counter. When any read SharedValue changes,
|
|
34
|
+
// Reanimated will re-execute this derived value.
|
|
32
35
|
const targetShiftY = (0, react_native_reanimated_1.useDerivedValue)(() => {
|
|
33
36
|
'worklet';
|
|
34
|
-
/**
|
|
35
|
-
* FORCE RE-EVALUATION PATTERN:
|
|
36
|
-
* useDerivedValue only re-runs when its dependencies change. However, with
|
|
37
|
-
* SharedValues from context, React can't track changes automatically.
|
|
38
|
-
*
|
|
39
|
-
* By reading dragUpdateTrigger.value (which is incremented on every drag
|
|
40
|
-
* state change), we force this derived value to recompute. This pattern
|
|
41
|
-
* is necessary because:
|
|
42
|
-
* 1. SharedValue changes don't trigger React re-renders
|
|
43
|
-
* 2. useDerivedValue caches its result based on tracked values
|
|
44
|
-
* 3. We need fresh calculations on every drag position update
|
|
45
|
-
*
|
|
46
|
-
* The ESLint disable is required because the value appears "unused" but
|
|
47
|
-
* reading it creates a dependency that Reanimated tracks.
|
|
48
|
-
*/
|
|
49
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
50
|
-
dragUpdateTrigger.value;
|
|
51
37
|
// During drop transition, freeze shift values
|
|
52
38
|
if (isDropping.value) {
|
|
53
39
|
return shiftY.value;
|
|
@@ -77,7 +63,16 @@ function useDragShift(config) {
|
|
|
77
63
|
: effectiveTranslateY < 0
|
|
78
64
|
? -constants_1.DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR
|
|
79
65
|
: 0;
|
|
80
|
-
const hoveredIndex = currentDraggedIndex +
|
|
66
|
+
const hoveredIndex = currentDraggedIndex +
|
|
67
|
+
Math.round(effectiveTranslateY / itemHeight + offset);
|
|
68
|
+
// Performance optimization: Spatial filtering
|
|
69
|
+
// Only items in the affected range need to calculate shift
|
|
70
|
+
// Items outside [min(dragIdx, hoverIdx), max(dragIdx, hoverIdx)] can bail early
|
|
71
|
+
const minAffectedIndex = Math.min(currentDraggedIndex, hoveredIndex);
|
|
72
|
+
const maxAffectedIndex = Math.max(currentDraggedIndex, hoveredIndex);
|
|
73
|
+
if (index < minAffectedIndex || index > maxAffectedIndex) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
81
76
|
// Moving DOWN: items between original and hovered positions shift UP
|
|
82
77
|
if (hoveredIndex > currentDraggedIndex) {
|
|
83
78
|
if (index > currentDraggedIndex && index <= hoveredIndex) {
|
|
@@ -96,6 +91,9 @@ function useDragShift(config) {
|
|
|
96
91
|
(0, react_native_reanimated_1.useAnimatedReaction)(() => targetShiftY.value, (target, prev) => {
|
|
97
92
|
'worklet';
|
|
98
93
|
if (target !== prev) {
|
|
94
|
+
// Cancel any in-flight animation before starting a new one
|
|
95
|
+
// This prevents competing animations and visual glitches
|
|
96
|
+
(0, react_native_reanimated_1.cancelAnimation)(shiftY);
|
|
99
97
|
shiftY.value = (0, react_native_reanimated_1.withTiming)(target, {
|
|
100
98
|
duration: constants_1.ANIMATION_TIMING.SHIFT_DURATION,
|
|
101
99
|
easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDropCompensation.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDropCompensation.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"useDropCompensation.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDropCompensation.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,CAqG3E"}
|
|
@@ -35,7 +35,7 @@ function useDropCompensation(config) {
|
|
|
35
35
|
// Track index on UI thread for synchronized shift reset
|
|
36
36
|
const indexShared = (0, react_native_reanimated_1.useSharedValue)(index);
|
|
37
37
|
// Global drag state
|
|
38
|
-
const { isDragging: globalIsDragging, draggedIndex, draggedItemId, measuredItemHeight, isDropping,
|
|
38
|
+
const { isDragging: globalIsDragging, draggedIndex, draggedItemId, measuredItemHeight, isDropping, config: dragConfig, } = (0, DragStateContext_1.useDragState)();
|
|
39
39
|
// Use FlashList's useRecyclingState for automatic reset on view recycling
|
|
40
40
|
const [prevIndex, setPrevIndex] = (0, flash_list_1.useRecyclingState)(index, [itemId]);
|
|
41
41
|
// Handle index changes after data updates (drop compensation)
|
|
@@ -74,9 +74,6 @@ function useDropCompensation(config) {
|
|
|
74
74
|
draggedIndex.value = -1;
|
|
75
75
|
draggedItemId.value = '';
|
|
76
76
|
measuredItemHeight.value = 0;
|
|
77
|
-
// Issue 5 Fix: Use direct assignment instead of withTiming for counter
|
|
78
|
-
// withTiming on a counter can cause timing issues and potential double-increment
|
|
79
|
-
dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
|
|
80
77
|
}
|
|
81
78
|
});
|
|
82
79
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@souscheflabs/reanimated-flashlist",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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",
|
|
@@ -42,8 +42,6 @@ export interface DragStateContextValue {
|
|
|
42
42
|
visibleHeight: SharedValue<number>;
|
|
43
43
|
/** Y position of FlashList top on screen (for autoscroll coordinate conversion) */
|
|
44
44
|
listTopY: SharedValue<number>;
|
|
45
|
-
/** Counter incremented on every drag state change to force useDerivedValue re-evaluation */
|
|
46
|
-
dragUpdateTrigger: SharedValue<number>;
|
|
47
45
|
/** Measured height of the dragged item (for dynamic height calculations) */
|
|
48
46
|
measuredItemHeight: SharedValue<number>;
|
|
49
47
|
/** Flag to freeze shift values during drop transition */
|
|
@@ -152,9 +150,6 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
|
|
|
152
150
|
const visibleHeight = useSharedValue(0);
|
|
153
151
|
const listTopY = useSharedValue(0);
|
|
154
152
|
|
|
155
|
-
// Counter to force useDerivedValue re-evaluation on every drag state change
|
|
156
|
-
const dragUpdateTrigger = useSharedValue(0);
|
|
157
|
-
|
|
158
153
|
// Measured height of dragged item (0 = use fallback from config)
|
|
159
154
|
const measuredItemHeight = useSharedValue(0);
|
|
160
155
|
|
|
@@ -200,7 +195,6 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
|
|
|
200
195
|
contentHeight,
|
|
201
196
|
visibleHeight,
|
|
202
197
|
listTopY,
|
|
203
|
-
dragUpdateTrigger,
|
|
204
198
|
measuredItemHeight,
|
|
205
199
|
isDropping,
|
|
206
200
|
setListRef,
|
|
@@ -219,7 +213,6 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
|
|
|
219
213
|
contentHeight,
|
|
220
214
|
visibleHeight,
|
|
221
215
|
listTopY,
|
|
222
|
-
dragUpdateTrigger,
|
|
223
216
|
measuredItemHeight,
|
|
224
217
|
isDropping,
|
|
225
218
|
setListRef,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
useAnimatedStyle,
|
|
4
4
|
useSharedValue,
|
|
5
5
|
withTiming,
|
|
6
|
+
cancelAnimation,
|
|
6
7
|
} from 'react-native-reanimated';
|
|
7
8
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
8
9
|
import {
|
|
@@ -171,6 +172,9 @@ export const useListExitAnimation = (
|
|
|
171
172
|
slideDistance.value = animConfig.slide.distance;
|
|
172
173
|
scaleToValue.value = animConfig.scale.toValue;
|
|
173
174
|
|
|
175
|
+
// Cancel any existing animation before starting the exit animation
|
|
176
|
+
cancelAnimation(exitDirection);
|
|
177
|
+
|
|
174
178
|
// Animate exitDirection.value from 0 to 1 (or -1)
|
|
175
179
|
exitDirection.value = withTiming(
|
|
176
180
|
direction,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
measure,
|
|
5
5
|
withSpring,
|
|
6
6
|
withTiming,
|
|
7
|
+
cancelAnimation,
|
|
7
8
|
Easing,
|
|
8
9
|
interpolate,
|
|
9
10
|
} from 'react-native-reanimated';
|
|
@@ -66,13 +67,17 @@ export function useDragGesture(
|
|
|
66
67
|
contentHeight,
|
|
67
68
|
visibleHeight,
|
|
68
69
|
listTopY,
|
|
69
|
-
dragUpdateTrigger,
|
|
70
70
|
measuredItemHeight,
|
|
71
71
|
isDropping,
|
|
72
72
|
scrollToOffset,
|
|
73
73
|
config: dragConfig,
|
|
74
74
|
} = useDragState();
|
|
75
75
|
|
|
76
|
+
// Performance optimization: Track last significant Y position and scroll time
|
|
77
|
+
// to avoid updating on every pixel movement
|
|
78
|
+
const lastSignificantY = useSharedValue(0);
|
|
79
|
+
const lastScrollTime = useSharedValue(0);
|
|
80
|
+
|
|
76
81
|
// Store current values in refs for stable gesture callbacks
|
|
77
82
|
const dragContextRef = useRef({
|
|
78
83
|
index,
|
|
@@ -127,6 +132,8 @@ export function useDragGesture(
|
|
|
127
132
|
dragStartScrollOffset.value = 0;
|
|
128
133
|
} else {
|
|
129
134
|
// Same position - animate back and reset state
|
|
135
|
+
// Cancel any existing animation before starting the return animation
|
|
136
|
+
cancelAnimation(translateY);
|
|
130
137
|
translateY.value = withTiming(
|
|
131
138
|
0,
|
|
132
139
|
{ duration: 150, easing: Easing.out(Easing.ease) },
|
|
@@ -139,7 +146,6 @@ export function useDragGesture(
|
|
|
139
146
|
currentTranslateY.value = 0;
|
|
140
147
|
dragStartScrollOffset.value = 0;
|
|
141
148
|
measuredItemHeight.value = 0;
|
|
142
|
-
dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
|
|
143
149
|
}
|
|
144
150
|
},
|
|
145
151
|
);
|
|
@@ -154,7 +160,6 @@ export function useDragGesture(
|
|
|
154
160
|
draggedItemId,
|
|
155
161
|
currentTranslateY,
|
|
156
162
|
dragStartScrollOffset,
|
|
157
|
-
dragUpdateTrigger,
|
|
158
163
|
isDropping,
|
|
159
164
|
onHapticFeedback,
|
|
160
165
|
],
|
|
@@ -187,20 +192,37 @@ export function useDragGesture(
|
|
|
187
192
|
draggedItemId.value = itemId;
|
|
188
193
|
dragStartScrollOffset.value = scrollOffset.value;
|
|
189
194
|
currentTranslateY.value = 0;
|
|
195
|
+
// Reset performance tracking values
|
|
196
|
+
lastSignificantY.value = 0;
|
|
197
|
+
lastScrollTime.value = Date.now();
|
|
190
198
|
draggedScale.value = withSpring(dragConfig.dragScale, {
|
|
191
199
|
damping: 15,
|
|
192
200
|
stiffness: 400,
|
|
193
201
|
});
|
|
194
|
-
dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
|
|
195
202
|
scheduleOnRN(triggerLightHaptic);
|
|
196
203
|
})
|
|
197
204
|
.onUpdate(event => {
|
|
198
205
|
'worklet';
|
|
206
|
+
// Always update translateY for smooth visual feedback
|
|
199
207
|
translateY.value = event.translationY;
|
|
200
|
-
|
|
201
|
-
|
|
208
|
+
|
|
209
|
+
// Performance optimization: Only update currentTranslateY (which triggers
|
|
210
|
+
// shift calculations in other items) when movement is significant (>5px)
|
|
211
|
+
const POSITION_THRESHOLD = 5;
|
|
212
|
+
const deltaFromLast = Math.abs(
|
|
213
|
+
event.translationY - lastSignificantY.value,
|
|
214
|
+
);
|
|
215
|
+
if (deltaFromLast >= POSITION_THRESHOLD) {
|
|
216
|
+
currentTranslateY.value = event.translationY;
|
|
217
|
+
lastSignificantY.value = event.translationY;
|
|
218
|
+
}
|
|
202
219
|
|
|
203
220
|
// Autoscroll when dragging near edges
|
|
221
|
+
// Performance optimization: Throttle to ~30fps (33ms interval)
|
|
222
|
+
const SCROLL_THROTTLE_MS = 33;
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
const timeSinceLastScroll = now - lastScrollTime.value;
|
|
225
|
+
|
|
204
226
|
const fingerInList = event.absoluteY - listTopY.value;
|
|
205
227
|
const topEdge = dragConfig.edgeThreshold;
|
|
206
228
|
const bottomEdge = visibleHeight.value - dragConfig.edgeThreshold;
|
|
@@ -214,7 +236,11 @@ export function useDragGesture(
|
|
|
214
236
|
);
|
|
215
237
|
const newOffset = Math.max(0, scrollOffset.value - speed);
|
|
216
238
|
scrollOffset.value = newOffset;
|
|
217
|
-
|
|
239
|
+
// Throttle actual scroll calls to reduce JS thread pressure
|
|
240
|
+
if (timeSinceLastScroll >= SCROLL_THROTTLE_MS) {
|
|
241
|
+
lastScrollTime.value = now;
|
|
242
|
+
scheduleOnRN(scrollToOffset, newOffset);
|
|
243
|
+
}
|
|
218
244
|
} else if (fingerInList > bottomEdge) {
|
|
219
245
|
const maxOffset = Math.max(
|
|
220
246
|
0,
|
|
@@ -229,7 +255,11 @@ export function useDragGesture(
|
|
|
229
255
|
);
|
|
230
256
|
const newOffset = Math.min(maxOffset, scrollOffset.value + speed);
|
|
231
257
|
scrollOffset.value = newOffset;
|
|
232
|
-
|
|
258
|
+
// Throttle actual scroll calls to reduce JS thread pressure
|
|
259
|
+
if (timeSinceLastScroll >= SCROLL_THROTTLE_MS) {
|
|
260
|
+
lastScrollTime.value = now;
|
|
261
|
+
scheduleOnRN(scrollToOffset, newOffset);
|
|
262
|
+
}
|
|
233
263
|
}
|
|
234
264
|
}
|
|
235
265
|
})
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
useDerivedValue,
|
|
4
4
|
useAnimatedReaction,
|
|
5
5
|
withTiming,
|
|
6
|
+
cancelAnimation,
|
|
6
7
|
Easing,
|
|
7
8
|
} from 'react-native-reanimated';
|
|
8
9
|
import { useDragState } from '../../contexts/DragStateContext';
|
|
@@ -38,7 +39,6 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
|
|
|
38
39
|
currentTranslateY,
|
|
39
40
|
scrollOffset,
|
|
40
41
|
dragStartScrollOffset,
|
|
41
|
-
dragUpdateTrigger,
|
|
42
42
|
measuredItemHeight,
|
|
43
43
|
isDropping,
|
|
44
44
|
config: dragConfig,
|
|
@@ -48,26 +48,11 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
|
|
|
48
48
|
const shiftY = useSharedValue(0);
|
|
49
49
|
|
|
50
50
|
// Calculate target shift using useDerivedValue
|
|
51
|
+
// Note: useDerivedValue automatically tracks SharedValue dependencies on UI thread,
|
|
52
|
+
// so we don't need a manual trigger counter. When any read SharedValue changes,
|
|
53
|
+
// Reanimated will re-execute this derived value.
|
|
51
54
|
const targetShiftY = useDerivedValue(() => {
|
|
52
55
|
'worklet';
|
|
53
|
-
/**
|
|
54
|
-
* FORCE RE-EVALUATION PATTERN:
|
|
55
|
-
* useDerivedValue only re-runs when its dependencies change. However, with
|
|
56
|
-
* SharedValues from context, React can't track changes automatically.
|
|
57
|
-
*
|
|
58
|
-
* By reading dragUpdateTrigger.value (which is incremented on every drag
|
|
59
|
-
* state change), we force this derived value to recompute. This pattern
|
|
60
|
-
* is necessary because:
|
|
61
|
-
* 1. SharedValue changes don't trigger React re-renders
|
|
62
|
-
* 2. useDerivedValue caches its result based on tracked values
|
|
63
|
-
* 3. We need fresh calculations on every drag position update
|
|
64
|
-
*
|
|
65
|
-
* The ESLint disable is required because the value appears "unused" but
|
|
66
|
-
* reading it creates a dependency that Reanimated tracks.
|
|
67
|
-
*/
|
|
68
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
69
|
-
dragUpdateTrigger.value;
|
|
70
|
-
|
|
71
56
|
// During drop transition, freeze shift values
|
|
72
57
|
if (isDropping.value) {
|
|
73
58
|
return shiftY.value;
|
|
@@ -104,7 +89,17 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
|
|
|
104
89
|
? -DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR
|
|
105
90
|
: 0;
|
|
106
91
|
const hoveredIndex =
|
|
107
|
-
currentDraggedIndex +
|
|
92
|
+
currentDraggedIndex +
|
|
93
|
+
Math.round(effectiveTranslateY / itemHeight + offset);
|
|
94
|
+
|
|
95
|
+
// Performance optimization: Spatial filtering
|
|
96
|
+
// Only items in the affected range need to calculate shift
|
|
97
|
+
// Items outside [min(dragIdx, hoverIdx), max(dragIdx, hoverIdx)] can bail early
|
|
98
|
+
const minAffectedIndex = Math.min(currentDraggedIndex, hoveredIndex);
|
|
99
|
+
const maxAffectedIndex = Math.max(currentDraggedIndex, hoveredIndex);
|
|
100
|
+
if (index < minAffectedIndex || index > maxAffectedIndex) {
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
108
103
|
|
|
109
104
|
// Moving DOWN: items between original and hovered positions shift UP
|
|
110
105
|
if (hoveredIndex > currentDraggedIndex) {
|
|
@@ -128,6 +123,9 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
|
|
|
128
123
|
(target, prev) => {
|
|
129
124
|
'worklet';
|
|
130
125
|
if (target !== prev) {
|
|
126
|
+
// Cancel any in-flight animation before starting a new one
|
|
127
|
+
// This prevents competing animations and visual glitches
|
|
128
|
+
cancelAnimation(shiftY);
|
|
131
129
|
shiftY.value = withTiming(target, {
|
|
132
130
|
duration: ANIMATION_TIMING.SHIFT_DURATION,
|
|
133
131
|
easing: Easing.out(Easing.ease),
|
|
@@ -47,7 +47,6 @@ export function useDropCompensation(config: UseDropCompensationConfig): void {
|
|
|
47
47
|
draggedItemId,
|
|
48
48
|
measuredItemHeight,
|
|
49
49
|
isDropping,
|
|
50
|
-
dragUpdateTrigger,
|
|
51
50
|
config: dragConfig,
|
|
52
51
|
} = useDragState();
|
|
53
52
|
|
|
@@ -99,9 +98,6 @@ export function useDropCompensation(config: UseDropCompensationConfig): void {
|
|
|
99
98
|
draggedIndex.value = -1;
|
|
100
99
|
draggedItemId.value = '';
|
|
101
100
|
measuredItemHeight.value = 0;
|
|
102
|
-
// Issue 5 Fix: Use direct assignment instead of withTiming for counter
|
|
103
|
-
// withTiming on a counter can cause timing issues and potential double-increment
|
|
104
|
-
dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
|
|
105
101
|
}
|
|
106
102
|
},
|
|
107
103
|
);
|