@souscheflabs/reanimated-flashlist 0.1.7 → 0.1.10
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 +6 -6
- package/lib/AnimatedFlashListItem.d.ts.map +1 -1
- package/lib/AnimatedFlashListItem.js +3 -8
- package/lib/constants/animations.d.ts.map +1 -1
- package/lib/constants/animations.js +4 -2
- package/lib/constants/drag.d.ts +38 -0
- package/lib/constants/drag.d.ts.map +1 -1
- package/lib/constants/drag.js +48 -1
- package/lib/contexts/DragStateContext.d.ts +0 -10
- package/lib/contexts/DragStateContext.d.ts.map +1 -1
- package/lib/contexts/DragStateContext.js +29 -2
- package/lib/contexts/ListAnimationContext.d.ts +5 -2
- package/lib/contexts/ListAnimationContext.d.ts.map +1 -1
- package/lib/contexts/ListAnimationContext.js +8 -9
- package/lib/hooks/animations/useListEntryAnimation.d.ts.map +1 -1
- package/lib/hooks/animations/useListEntryAnimation.js +3 -3
- package/lib/hooks/animations/useListExitAnimation.d.ts.map +1 -1
- package/lib/hooks/animations/useListExitAnimation.js +5 -1
- package/lib/hooks/drag/useDragAnimatedStyle.d.ts.map +1 -1
- package/lib/hooks/drag/useDragAnimatedStyle.js +31 -4
- package/lib/hooks/drag/useDragShift.d.ts.map +1 -1
- package/lib/hooks/drag/useDragShift.js +24 -3
- package/lib/hooks/drag/useDropCompensation.d.ts.map +1 -1
- package/lib/hooks/drag/useDropCompensation.js +28 -9
- package/lib/index.d.ts +2 -2
- package/lib/index.js +2 -2
- package/package.json +1 -1
- package/src/AnimatedFlashListItem.tsx +4 -10
- package/src/__tests__/contexts/ListAnimationContext.test.tsx +3 -3
- package/src/constants/animations.ts +4 -2
- package/src/constants/drag.ts +52 -0
- package/src/contexts/DragStateContext.tsx +45 -5
- package/src/contexts/ListAnimationContext.tsx +18 -11
- package/src/hooks/animations/useListEntryAnimation.ts +9 -6
- package/src/hooks/animations/useListExitAnimation.ts +6 -1
- package/src/hooks/drag/useDragAnimatedStyle.ts +45 -10
- package/src/hooks/drag/useDragShift.ts +24 -3
- package/src/hooks/drag/useDropCompensation.ts +31 -9
- package/src/index.ts +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @souscheflabs/reanimated-flashlist
|
|
2
2
|
|
|
3
3
|
A high-performance animated FlashList with drag-to-reorder and entry/exit animations for React Native.
|
|
4
4
|
|
|
@@ -15,7 +15,7 @@ A high-performance animated FlashList with drag-to-reorder and entry/exit animat
|
|
|
15
15
|
|
|
16
16
|
### From npm (when published)
|
|
17
17
|
```bash
|
|
18
|
-
npm install @
|
|
18
|
+
npm install @souscheflabs/reanimated-flashlist
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
### From GitHub (private repo)
|
|
@@ -34,7 +34,7 @@ Or add to `package.json`:
|
|
|
34
34
|
```json
|
|
35
35
|
{
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@
|
|
37
|
+
"@souscheflabs/reanimated-flashlist": "github:SousChefLabs/reanimated-flashlist#v0.1.1"
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
```
|
|
@@ -57,7 +57,7 @@ npm install @shopify/flash-list react-native-reanimated react-native-gesture-han
|
|
|
57
57
|
## Quick Start
|
|
58
58
|
|
|
59
59
|
```tsx
|
|
60
|
-
import { AnimatedFlashList, type AnimatedListItem } from '@
|
|
60
|
+
import { AnimatedFlashList, type AnimatedListItem } from '@souscheflabs/reanimated-flashlist';
|
|
61
61
|
import { GestureDetector } from 'react-native-gesture-handler';
|
|
62
62
|
|
|
63
63
|
interface MyItem extends AnimatedListItem {
|
|
@@ -261,7 +261,7 @@ import {
|
|
|
261
261
|
useDragAnimatedStyle,
|
|
262
262
|
useListExitAnimation,
|
|
263
263
|
useListEntryAnimation,
|
|
264
|
-
} from '@
|
|
264
|
+
} from '@souscheflabs/reanimated-flashlist';
|
|
265
265
|
```
|
|
266
266
|
|
|
267
267
|
### Context Providers
|
|
@@ -274,7 +274,7 @@ import {
|
|
|
274
274
|
ListAnimationProvider,
|
|
275
275
|
useDragState,
|
|
276
276
|
useListAnimation,
|
|
277
|
-
} from '@
|
|
277
|
+
} from '@souscheflabs/reanimated-flashlist';
|
|
278
278
|
```
|
|
279
279
|
|
|
280
280
|
## License
|
|
@@ -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,CAkJ3D;AAGD,eAAO,MAAM,qBAAqB,EAE7B,OAAO,0BAA0B,CAAC"}
|
|
@@ -102,12 +102,6 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
|
|
|
102
102
|
const handleLayout = (0, react_1.useCallback)((event) => {
|
|
103
103
|
measuredHeightRef.current = event.nativeEvent.layout.height;
|
|
104
104
|
}, []);
|
|
105
|
-
// Create combined animated style
|
|
106
|
-
const combinedAnimatedStyle = (0, react_1.useMemo)(() => {
|
|
107
|
-
// We can't directly combine animated styles here since they're worklet-based
|
|
108
|
-
// Instead, we'll let the consumer apply them via the render prop
|
|
109
|
-
return {};
|
|
110
|
-
}, []);
|
|
111
105
|
// Create drag handle props
|
|
112
106
|
const dragHandleProps = (0, react_1.useMemo)(() => isDragEnabled
|
|
113
107
|
? {
|
|
@@ -120,11 +114,13 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
|
|
|
120
114
|
triggerExit(direction, onComplete, preset);
|
|
121
115
|
}, [triggerExit]);
|
|
122
116
|
// Create render info
|
|
117
|
+
// Note: animatedStyle is empty because animations are applied to the wrapper View automatically.
|
|
118
|
+
// This field is kept for backward compatibility with the AnimatedRenderItemInfo interface.
|
|
123
119
|
const renderInfo = (0, react_1.useMemo)(() => ({
|
|
124
120
|
item,
|
|
125
121
|
index,
|
|
126
122
|
totalItems,
|
|
127
|
-
animatedStyle:
|
|
123
|
+
animatedStyle: {},
|
|
128
124
|
dragHandleProps,
|
|
129
125
|
isDragging: false, // This is a SharedValue, consumer should use dragHandleProps.isDragging
|
|
130
126
|
isDragEnabled,
|
|
@@ -134,7 +130,6 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
|
|
|
134
130
|
item,
|
|
135
131
|
index,
|
|
136
132
|
totalItems,
|
|
137
|
-
combinedAnimatedStyle,
|
|
138
133
|
dragHandleProps,
|
|
139
134
|
isDragEnabled,
|
|
140
135
|
triggerExitAnimation,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"animations.d.ts","sourceRoot":"","sources":["../../src/constants/animations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAE1E;;;GAGG;AACH,eAAO,MAAM,cAAc,yDAAoC,CAAC;AAEhE;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,EAAE,mBAkBpC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,
|
|
1
|
+
{"version":3,"file":"animations.d.ts","sourceRoot":"","sources":["../../src/constants/animations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAE1E;;;GAGG;AACH,eAAO,MAAM,cAAc,yDAAoC,CAAC;AAEhE;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,EAAE,mBAkBpC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,mBAoBjC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,EAAE,oBAGrC,CAAC;AAEF;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,GAAE,SAAS,GAAG,MAAe,EACnC,SAAS,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,GACvC,mBAAmB,CAWrB;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,GACxC,oBAAoB,CAOtB"}
|
|
@@ -34,7 +34,7 @@ exports.DEFAULT_EXIT_ANIMATION = {
|
|
|
34
34
|
},
|
|
35
35
|
removalDelay: 300,
|
|
36
36
|
layoutAnimation: {
|
|
37
|
-
duration:
|
|
37
|
+
duration: 300,
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
40
|
/**
|
|
@@ -61,7 +61,9 @@ exports.FAST_EXIT_ANIMATION = {
|
|
|
61
61
|
},
|
|
62
62
|
removalDelay: 200,
|
|
63
63
|
layoutAnimation: {
|
|
64
|
-
|
|
64
|
+
// Slightly longer than the exit animation for a smoother overall effect
|
|
65
|
+
// Items below start moving up as the exiting item is sliding out
|
|
66
|
+
duration: 250,
|
|
65
67
|
},
|
|
66
68
|
};
|
|
67
69
|
/**
|
package/lib/constants/drag.d.ts
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
import type { DragConfig } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Unified animation timing configuration for consistency across all drag hooks.
|
|
4
|
+
* Using consistent timing prevents visual "mismatches" during complex interactions.
|
|
5
|
+
*/
|
|
6
|
+
export declare const ANIMATION_TIMING: {
|
|
7
|
+
/** Duration for item shift animations (when items move to make room) */
|
|
8
|
+
readonly SHIFT_DURATION: 100;
|
|
9
|
+
/** Duration for drop settle animation (when dragged item animates to final position) */
|
|
10
|
+
readonly DROP_SETTLE_DURATION: 150;
|
|
11
|
+
/** Duration for shadow opacity fade transition */
|
|
12
|
+
readonly SHADOW_FADE_DURATION: 100;
|
|
13
|
+
/** Duration for non-dragged item compensation animations */
|
|
14
|
+
readonly COMPENSATION_DURATION: 100;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Configurable thresholds for drag calculations.
|
|
18
|
+
* These values control sensitivity and visual behavior.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DRAG_THRESHOLDS: {
|
|
21
|
+
/**
|
|
22
|
+
* Offset factor for hover position calculation.
|
|
23
|
+
* This adds a bias when determining which index is being hovered over,
|
|
24
|
+
* making it slightly easier to "cross" into the next/prev position.
|
|
25
|
+
* Range: 0-0.5, where 0 = exact center, 0.5 = edge of item
|
|
26
|
+
*/
|
|
27
|
+
readonly HOVER_OFFSET_FACTOR: 0.2;
|
|
28
|
+
/**
|
|
29
|
+
* Z-index for elevated (dragged) items.
|
|
30
|
+
* Should be high enough to appear above other UI but not conflict
|
|
31
|
+
* with modals or overlays (which typically use 1000+).
|
|
32
|
+
*/
|
|
33
|
+
readonly ELEVATED_Z_INDEX: 999;
|
|
34
|
+
/**
|
|
35
|
+
* Minimum translateY/shiftY value to consider "significant".
|
|
36
|
+
* Values below this are treated as zero for optimization.
|
|
37
|
+
*/
|
|
38
|
+
readonly SHIFT_SIGNIFICANCE_THRESHOLD: 1;
|
|
39
|
+
};
|
|
2
40
|
/**
|
|
3
41
|
* Default drag configuration
|
|
4
42
|
* All values can be overridden via AnimatedFlashList config prop
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drag.d.ts","sourceRoot":"","sources":["../../src/constants/drag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"drag.d.ts","sourceRoot":"","sources":["../../src/constants/drag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAM3C;;;GAGG;AACH,eAAO,MAAM,gBAAgB;IAC3B,wEAAwE;;IAExE,wFAAwF;;IAExF,kDAAkD;;IAElD,4DAA4D;;CAEpD,CAAC;AAMX;;;GAGG;AACH,eAAO,MAAM,eAAe;IAC1B;;;;;OAKG;;IAEH;;;;OAIG;;IAEH;;;OAGG;;CAEK,CAAC;AAMX;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,UAoCjC,CAAC;AAEF;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,CAG5E"}
|
package/lib/constants/drag.js
CHANGED
|
@@ -1,7 +1,54 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_DRAG_CONFIG = void 0;
|
|
3
|
+
exports.DEFAULT_DRAG_CONFIG = exports.DRAG_THRESHOLDS = exports.ANIMATION_TIMING = void 0;
|
|
4
4
|
exports.createDragConfig = createDragConfig;
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Animation Timing & Easing Constants
|
|
7
|
+
// ============================================================================
|
|
8
|
+
/**
|
|
9
|
+
* Unified animation timing configuration for consistency across all drag hooks.
|
|
10
|
+
* Using consistent timing prevents visual "mismatches" during complex interactions.
|
|
11
|
+
*/
|
|
12
|
+
exports.ANIMATION_TIMING = {
|
|
13
|
+
/** Duration for item shift animations (when items move to make room) */
|
|
14
|
+
SHIFT_DURATION: 100,
|
|
15
|
+
/** Duration for drop settle animation (when dragged item animates to final position) */
|
|
16
|
+
DROP_SETTLE_DURATION: 150,
|
|
17
|
+
/** Duration for shadow opacity fade transition */
|
|
18
|
+
SHADOW_FADE_DURATION: 100,
|
|
19
|
+
/** Duration for non-dragged item compensation animations */
|
|
20
|
+
COMPENSATION_DURATION: 100,
|
|
21
|
+
};
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Threshold Constants
|
|
24
|
+
// ============================================================================
|
|
25
|
+
/**
|
|
26
|
+
* Configurable thresholds for drag calculations.
|
|
27
|
+
* These values control sensitivity and visual behavior.
|
|
28
|
+
*/
|
|
29
|
+
exports.DRAG_THRESHOLDS = {
|
|
30
|
+
/**
|
|
31
|
+
* Offset factor for hover position calculation.
|
|
32
|
+
* This adds a bias when determining which index is being hovered over,
|
|
33
|
+
* making it slightly easier to "cross" into the next/prev position.
|
|
34
|
+
* Range: 0-0.5, where 0 = exact center, 0.5 = edge of item
|
|
35
|
+
*/
|
|
36
|
+
HOVER_OFFSET_FACTOR: 0.2,
|
|
37
|
+
/**
|
|
38
|
+
* Z-index for elevated (dragged) items.
|
|
39
|
+
* Should be high enough to appear above other UI but not conflict
|
|
40
|
+
* with modals or overlays (which typically use 1000+).
|
|
41
|
+
*/
|
|
42
|
+
ELEVATED_Z_INDEX: 999,
|
|
43
|
+
/**
|
|
44
|
+
* Minimum translateY/shiftY value to consider "significant".
|
|
45
|
+
* Values below this are treated as zero for optimization.
|
|
46
|
+
*/
|
|
47
|
+
SHIFT_SIGNIFICANCE_THRESHOLD: 1,
|
|
48
|
+
};
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Default Drag Configuration
|
|
51
|
+
// ============================================================================
|
|
5
52
|
/**
|
|
6
53
|
* Default drag configuration
|
|
7
54
|
* All values can be overridden via AnimatedFlashList config prop
|
|
@@ -58,16 +58,6 @@ interface DragStateProviderProps {
|
|
|
58
58
|
/** Optional drag configuration overrides */
|
|
59
59
|
config?: Partial<DragConfig>;
|
|
60
60
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Provider that creates shared Reanimated values for drag state.
|
|
63
|
-
*
|
|
64
|
-
* These values are shared across all list items:
|
|
65
|
-
* - The dragged item writes to them during drag gestures
|
|
66
|
-
* - Non-dragged items read them to calculate their shift offset
|
|
67
|
-
* - Scroll state enables viewport-aware hover calculations
|
|
68
|
-
*
|
|
69
|
-
* Using SharedValues ensures animations run on the UI thread at 60fps.
|
|
70
|
-
*/
|
|
71
61
|
export declare const DragStateProvider: React.FC<DragStateProviderProps>;
|
|
72
62
|
export {};
|
|
73
63
|
//# sourceMappingURL=DragStateContext.d.ts.map
|
|
@@ -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,4FAA4F;IAC5F,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,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;
|
|
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,4FAA4F;IAC5F,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,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,CA2G9D,CAAC"}
|
|
@@ -60,9 +60,36 @@ exports.useDragState = useDragState;
|
|
|
60
60
|
*
|
|
61
61
|
* Using SharedValues ensures animations run on the UI thread at 60fps.
|
|
62
62
|
*/
|
|
63
|
+
/**
|
|
64
|
+
* Validates drag configuration values to prevent runtime errors.
|
|
65
|
+
* Logs warnings in development for invalid configurations.
|
|
66
|
+
*/
|
|
67
|
+
function validateConfig(config) {
|
|
68
|
+
if (__DEV__) {
|
|
69
|
+
if (config.itemHeight <= 0) {
|
|
70
|
+
console.warn('[AnimatedFlashList] Invalid itemHeight: must be positive. Got:', config.itemHeight);
|
|
71
|
+
}
|
|
72
|
+
if (config.longPressDuration <= 0) {
|
|
73
|
+
console.warn('[AnimatedFlashList] Invalid longPressDuration: must be positive. Got:', config.longPressDuration);
|
|
74
|
+
}
|
|
75
|
+
if (config.dragScale <= 0) {
|
|
76
|
+
console.warn('[AnimatedFlashList] Invalid dragScale: must be positive. Got:', config.dragScale);
|
|
77
|
+
}
|
|
78
|
+
if (config.edgeThreshold < 0) {
|
|
79
|
+
console.warn('[AnimatedFlashList] Invalid edgeThreshold: must be non-negative. Got:', config.edgeThreshold);
|
|
80
|
+
}
|
|
81
|
+
if (config.maxScrollSpeed <= 0) {
|
|
82
|
+
console.warn('[AnimatedFlashList] Invalid maxScrollSpeed: must be positive. Got:', config.maxScrollSpeed);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
63
86
|
const DragStateProvider = ({ children, config: configOverrides, }) => {
|
|
64
|
-
// Merge config with defaults
|
|
65
|
-
const config = (0, react_1.useMemo)(() =>
|
|
87
|
+
// Merge config with defaults and validate
|
|
88
|
+
const config = (0, react_1.useMemo)(() => {
|
|
89
|
+
const mergedConfig = { ...constants_1.DEFAULT_DRAG_CONFIG, ...configOverrides };
|
|
90
|
+
validateConfig(mergedConfig);
|
|
91
|
+
return mergedConfig;
|
|
92
|
+
}, [configOverrides]);
|
|
66
93
|
// Shared values are created once and persist for the lifetime of the provider
|
|
67
94
|
const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
|
|
68
95
|
const draggedIndex = (0, react_native_reanimated_1.useSharedValue)(-1);
|
|
@@ -80,8 +80,11 @@ interface ListAnimationProviderProps {
|
|
|
80
80
|
*/
|
|
81
81
|
entryAnimationTimeout?: number;
|
|
82
82
|
/**
|
|
83
|
-
* Duration for layout animations when items are removed (ms)
|
|
84
|
-
*
|
|
83
|
+
* Duration for layout animations when items are removed (ms).
|
|
84
|
+
* This controls how fast remaining items animate into their new positions
|
|
85
|
+
* after an item is removed from the list.
|
|
86
|
+
* Lower values = faster/snappier, higher values = smoother/more gradual
|
|
87
|
+
* @default 300
|
|
85
88
|
*/
|
|
86
89
|
layoutAnimationDuration?: number;
|
|
87
90
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ListAnimationContext.d.ts","sourceRoot":"","sources":["../../src/contexts/ListAnimationContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EAEtB,MAAM,UAAU,CAAC;AAElB;;;;;;;;GAQG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,wBAAwB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAElF;;;OAGG;IACH,0BAA0B,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAErD;;;OAGG;IACH,oBAAoB,EAAE,CACpB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,kBAAkB,EAC7B,UAAU,EAAE,MAAM,IAAI,KACnB,OAAO,CAAC;IAEb;;;OAGG;IACH,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAE7E;;;;OAIG;IACH,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,qBAAqB,GAAG,IAAI,CAAC;IAEtE;;;OAGG;IACH,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAE7E;;OAEG;IACH,qBAAqB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhD;;;OAGG;IACH,qBAAqB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAEjD;;;OAGG;IACH,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAEzC;;OAEG;IACH,uBAAuB,EAAE,MAAM,CAAC;CACjC;AAID;;;GAGG;AACH,eAAO,MAAM,gBAAgB,QAAO,yBAMnC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,QAAO,yBAAyB,GAAG,IAEvE,CAAC;AAEF,UAAU,0BAA0B;IAClC,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B
|
|
1
|
+
{"version":3,"file":"ListAnimationContext.d.ts","sourceRoot":"","sources":["../../src/contexts/ListAnimationContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EAEtB,MAAM,UAAU,CAAC;AAElB;;;;;;;;GAQG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,wBAAwB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAElF;;;OAGG;IACH,0BAA0B,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAErD;;;OAGG;IACH,oBAAoB,EAAE,CACpB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,kBAAkB,EAC7B,UAAU,EAAE,MAAM,IAAI,KACnB,OAAO,CAAC;IAEb;;;OAGG;IACH,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAE7E;;;;OAIG;IACH,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,qBAAqB,GAAG,IAAI,CAAC;IAEtE;;;OAGG;IACH,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAE7E;;OAEG;IACH,qBAAqB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhD;;;OAGG;IACH,qBAAqB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAEjD;;;OAGG;IACH,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAEzC;;OAEG;IACH,uBAAuB,EAAE,MAAM,CAAC;CACjC;AAID;;;GAGG;AACH,eAAO,MAAM,gBAAgB,QAAO,yBAMnC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,QAAO,yBAAyB,GAAG,IAEvE,CAAC;AAEF,UAAU,0BAA0B;IAClC,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAmKtE,CAAC"}
|
|
@@ -68,7 +68,7 @@ exports.useListAnimationOptional = useListAnimationOptional;
|
|
|
68
68
|
* - Layout animation coordination when items are removed
|
|
69
69
|
* - Decoupled from React render cycle for performance
|
|
70
70
|
*/
|
|
71
|
-
const ListAnimationProvider = ({ children, entryAnimationTimeout = 5000, layoutAnimationDuration =
|
|
71
|
+
const ListAnimationProvider = ({ children, entryAnimationTimeout = 5000, layoutAnimationDuration = 300, enableLayoutAnimation = true, }) => {
|
|
72
72
|
// Map of itemId -> exit animation trigger function
|
|
73
73
|
const animationTriggersRef = (0, react_1.useRef)(new Map());
|
|
74
74
|
// Map of itemId -> pending entry animation
|
|
@@ -129,14 +129,13 @@ const ListAnimationProvider = ({ children, entryAnimationTimeout = 5000, layoutA
|
|
|
129
129
|
exitingItemsVersion.value = exitingItemsVersion.value + 1;
|
|
130
130
|
// Configure native LayoutAnimation for remaining items
|
|
131
131
|
if (enableLayoutAnimation) {
|
|
132
|
-
// Use
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
});
|
|
132
|
+
// Use spring animation for smoother, more natural item transitions
|
|
133
|
+
// Spring animations better handle position changes than timed animations
|
|
134
|
+
// and provide a more "organic" feel to remaining items moving up
|
|
135
|
+
react_native_1.LayoutAnimation.configureNext(react_native_1.LayoutAnimation.create(layoutAnimationDuration, react_native_1.LayoutAnimation.Types.easeOut,
|
|
136
|
+
// Use opacity as the trigger property - this works better for
|
|
137
|
+
// position-based animations than scaleY which can cause visual artifacts
|
|
138
|
+
react_native_1.LayoutAnimation.Properties.opacity));
|
|
140
139
|
}
|
|
141
140
|
}, [enableLayoutAnimation, layoutAnimationDuration, exitingItemsVersion]);
|
|
142
141
|
// Unregister an exiting item
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useListEntryAnimation.d.ts","sourceRoot":"","sources":["../../../src/hooks/animations/useListEntryAnimation.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,MAAM,EACd,kBAAkB,OAAO,CAAC,oBAAoB,CAAC;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"useListEntryAnimation.d.ts","sourceRoot":"","sources":["../../../src/hooks/animations/useListEntryAnimation.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,MAAM,EACd,kBAAkB,OAAO,CAAC,oBAAoB,CAAC;;;;;;;;;;CAsEhD,CAAC"}
|
|
@@ -32,11 +32,11 @@ const ListAnimationContext_1 = require("../../contexts/ListAnimationContext");
|
|
|
32
32
|
*/
|
|
33
33
|
const useListEntryAnimation = (itemId, configOverrides) => {
|
|
34
34
|
const animationContext = (0, ListAnimationContext_1.useListAnimationOptional)();
|
|
35
|
-
//
|
|
36
|
-
const config = {
|
|
35
|
+
// Memoize merged config to prevent unnecessary re-renders and effect re-runs
|
|
36
|
+
const config = (0, react_1.useMemo)(() => ({
|
|
37
37
|
fade: { ...animations_1.DEFAULT_ENTRY_ANIMATION.fade, ...configOverrides?.fade },
|
|
38
38
|
slide: { ...animations_1.DEFAULT_ENTRY_ANIMATION.slide, ...configOverrides?.slide },
|
|
39
|
-
};
|
|
39
|
+
}), [configOverrides]);
|
|
40
40
|
// Shared values for entry animation - start at final position (no animation by default)
|
|
41
41
|
const translateX = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
42
42
|
const opacity = (0, react_native_reanimated_1.useSharedValue)(1);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useListExitAnimation.d.ts","sourceRoot":"","sources":["../../../src/hooks/animations/useListExitAnimation.ts"],"names":[],"mappings":"AAYA,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;;
|
|
1
|
+
{"version":3,"file":"useListExitAnimation.d.ts","sourceRoot":"","sources":["../../../src/hooks/animations/useListExitAnimation.ts"],"names":[],"mappings":"AAYA,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;;CAqDhC,CAAC"}
|
|
@@ -116,7 +116,11 @@ const useListExitAnimation = (itemId, config) => {
|
|
|
116
116
|
const animConfig = animationConfigs[preset];
|
|
117
117
|
// Register exiting item for layout compensation (if configured)
|
|
118
118
|
if (config?.onExitStart && config.index !== undefined) {
|
|
119
|
-
|
|
119
|
+
// Use measured height if available and valid (> 0), otherwise use animation distance
|
|
120
|
+
// This handles the case where layout hasn't been measured yet (height = 0)
|
|
121
|
+
const height = config.measuredHeight && config.measuredHeight > 0
|
|
122
|
+
? config.measuredHeight
|
|
123
|
+
: animConfig.slide.distance;
|
|
120
124
|
config.onExitStart(config.index, height);
|
|
121
125
|
}
|
|
122
126
|
// Set animation config SharedValues BEFORE starting animation
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDragAnimatedStyle.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragAnimatedStyle.ts"],"names":[],"mappings":"
|
|
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,CAmE5B"}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.useDragAnimatedStyle = useDragAnimatedStyle;
|
|
4
4
|
const react_native_reanimated_1 = require("react-native-reanimated");
|
|
5
5
|
const DragStateContext_1 = require("../../contexts/DragStateContext");
|
|
6
|
+
const constants_1 = require("../../constants");
|
|
6
7
|
/**
|
|
7
8
|
* Hook that creates animated styles for drag operations.
|
|
8
9
|
*
|
|
@@ -35,12 +36,37 @@ const DragStateContext_1 = require("../../contexts/DragStateContext");
|
|
|
35
36
|
function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
|
|
36
37
|
// Global drag state for scale, identity check, and scroll compensation
|
|
37
38
|
const { draggedItemId, draggedScale, config, scrollOffset, dragStartScrollOffset } = (0, DragStateContext_1.useDragState)();
|
|
39
|
+
// Issue 2 Fix: Track shadow opacity with smooth transitions to prevent "flash"
|
|
40
|
+
// When drag ends, shadow opacity should fade smoothly instead of snapping
|
|
41
|
+
const animatedShadowOpacity = (0, react_native_reanimated_1.useSharedValue)(0.1);
|
|
42
|
+
// React to drag state changes and animate shadow opacity
|
|
43
|
+
(0, react_native_reanimated_1.useAnimatedReaction)(() => ({
|
|
44
|
+
isThisItemDragged: draggedItemId.value === itemId,
|
|
45
|
+
scale: draggedScale.value,
|
|
46
|
+
}), (current, previous) => {
|
|
47
|
+
'worklet';
|
|
48
|
+
if (current.isThisItemDragged) {
|
|
49
|
+
// When dragged, interpolate shadow opacity based on scale
|
|
50
|
+
const targetOpacity = (0, react_native_reanimated_1.interpolate)(current.scale, [1, config.dragScale], [0.1, config.dragShadowOpacity]);
|
|
51
|
+
animatedShadowOpacity.value = targetOpacity;
|
|
52
|
+
}
|
|
53
|
+
else if (previous?.isThisItemDragged && !current.isThisItemDragged) {
|
|
54
|
+
// Transition from dragged to not-dragged: fade shadow smoothly
|
|
55
|
+
animatedShadowOpacity.value = (0, react_native_reanimated_1.withTiming)(0.1, {
|
|
56
|
+
duration: constants_1.ANIMATION_TIMING.SHADOW_FADE_DURATION,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Non-dragged item: keep at base opacity
|
|
61
|
+
animatedShadowOpacity.value = 0.1;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
38
64
|
// Animated style for drag offset with scale and shadow
|
|
39
65
|
const dragAnimatedStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => {
|
|
40
66
|
const isThisItemDragged = draggedItemId.value === itemId;
|
|
41
67
|
// Keep elevated if: actively dragging OR has offset (animating back)
|
|
42
|
-
const shouldBeElevated = isDragging.value ||
|
|
43
|
-
|
|
68
|
+
const shouldBeElevated = isDragging.value ||
|
|
69
|
+
Math.abs(translateY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
|
|
44
70
|
// Calculate scroll delta for position compensation during autoscroll
|
|
45
71
|
const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
|
|
46
72
|
// Use drag translateY + scroll compensation for dragged item, shiftY for others
|
|
@@ -52,8 +78,9 @@ function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
|
|
|
52
78
|
{ translateY: yOffset },
|
|
53
79
|
{ scale: isThisItemDragged ? draggedScale.value : 1 },
|
|
54
80
|
],
|
|
55
|
-
zIndex: shouldBeElevated ?
|
|
56
|
-
|
|
81
|
+
zIndex: shouldBeElevated ? constants_1.DRAG_THRESHOLDS.ELEVATED_Z_INDEX : 0,
|
|
82
|
+
// Use animated shadow opacity for smooth transitions
|
|
83
|
+
shadowOpacity: animatedShadowOpacity.value,
|
|
57
84
|
elevation: isDragging.value ? 12 : 4,
|
|
58
85
|
};
|
|
59
86
|
});
|
|
@@ -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":"AASA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE1E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,CA8G3E"}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.useDragShift = useDragShift;
|
|
4
4
|
const react_native_reanimated_1 = require("react-native-reanimated");
|
|
5
5
|
const DragStateContext_1 = require("../../contexts/DragStateContext");
|
|
6
|
+
const constants_1 = require("../../constants");
|
|
6
7
|
/**
|
|
7
8
|
* Hook that calculates shift animation for non-dragged items.
|
|
8
9
|
*
|
|
@@ -30,7 +31,21 @@ function useDragShift(config) {
|
|
|
30
31
|
// Calculate target shift using useDerivedValue
|
|
31
32
|
const targetShiftY = (0, react_native_reanimated_1.useDerivedValue)(() => {
|
|
32
33
|
'worklet';
|
|
33
|
-
|
|
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
|
+
*/
|
|
34
49
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
35
50
|
dragUpdateTrigger.value;
|
|
36
51
|
// During drop transition, freeze shift values
|
|
@@ -55,7 +70,13 @@ function useDragShift(config) {
|
|
|
55
70
|
const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
|
|
56
71
|
const effectiveTranslateY = translateYNow + scrollDelta;
|
|
57
72
|
// Calculate which index the dragged item is hovering over
|
|
58
|
-
|
|
73
|
+
// The HOVER_OFFSET_FACTOR adds a small bias to make crossing into
|
|
74
|
+
// the next position slightly easier (prevents "sticky" hover positions)
|
|
75
|
+
const offset = effectiveTranslateY > 0
|
|
76
|
+
? constants_1.DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR
|
|
77
|
+
: effectiveTranslateY < 0
|
|
78
|
+
? -constants_1.DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR
|
|
79
|
+
: 0;
|
|
59
80
|
const hoveredIndex = currentDraggedIndex + Math.round(effectiveTranslateY / itemHeight + offset);
|
|
60
81
|
// Moving DOWN: items between original and hovered positions shift UP
|
|
61
82
|
if (hoveredIndex > currentDraggedIndex) {
|
|
@@ -76,7 +97,7 @@ function useDragShift(config) {
|
|
|
76
97
|
'worklet';
|
|
77
98
|
if (target !== prev) {
|
|
78
99
|
shiftY.value = (0, react_native_reanimated_1.withTiming)(target, {
|
|
79
|
-
duration:
|
|
100
|
+
duration: constants_1.ANIMATION_TIMING.SHIFT_DURATION,
|
|
80
101
|
easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
|
|
81
102
|
});
|
|
82
103
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDropCompensation.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDropCompensation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useDropCompensation.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDropCompensation.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,CAyG3E"}
|
|
@@ -5,6 +5,7 @@ const react_1 = require("react");
|
|
|
5
5
|
const react_native_reanimated_1 = require("react-native-reanimated");
|
|
6
6
|
const flash_list_1 = require("@shopify/flash-list");
|
|
7
7
|
const DragStateContext_1 = require("../../contexts/DragStateContext");
|
|
8
|
+
const constants_1 = require("../../constants");
|
|
8
9
|
/**
|
|
9
10
|
* Hook that handles index change compensation after drag reorder.
|
|
10
11
|
*
|
|
@@ -40,9 +41,20 @@ function useDropCompensation(config) {
|
|
|
40
41
|
// Handle index changes after data updates (drop compensation)
|
|
41
42
|
(0, react_1.useLayoutEffect)(() => {
|
|
42
43
|
if (index !== prevIndex) {
|
|
44
|
+
// Guard: Skip compensation during active drop transition to prevent double-compensation
|
|
45
|
+
// The isDropping flag is set when drop begins and cleared after settle animation
|
|
46
|
+
if (isDropping.value && draggedItemId.value !== itemId) {
|
|
47
|
+
// Non-dragged items should wait for drop to complete
|
|
48
|
+
setPrevIndex(index);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Issue 1 Fix: Use dynamically measured height when available, matching useDragShift behavior
|
|
52
|
+
const itemHeight = measuredItemHeight.value > 0
|
|
53
|
+
? measuredItemHeight.value + dragConfig.itemVerticalMargin
|
|
54
|
+
: dragConfig.itemHeight;
|
|
43
55
|
// Compensate for index change by adjusting translateY
|
|
44
56
|
const indexDelta = index - prevIndex;
|
|
45
|
-
const heightDelta = indexDelta *
|
|
57
|
+
const heightDelta = indexDelta * itemHeight;
|
|
46
58
|
// Check if this is the dragged item completing its drop
|
|
47
59
|
const isTheDraggedItem = draggedItemId.value === itemId;
|
|
48
60
|
// Compensate translateY for position change
|
|
@@ -50,7 +62,10 @@ function useDropCompensation(config) {
|
|
|
50
62
|
if (isTheDraggedItem) {
|
|
51
63
|
// Dragged item: animate smoothly to 0 from compensated position
|
|
52
64
|
translateY.value = compensatedY;
|
|
53
|
-
translateY.value = (0, react_native_reanimated_1.withTiming)(0, {
|
|
65
|
+
translateY.value = (0, react_native_reanimated_1.withTiming)(0, {
|
|
66
|
+
duration: constants_1.ANIMATION_TIMING.DROP_SETTLE_DURATION,
|
|
67
|
+
easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
|
|
68
|
+
}, finished => {
|
|
54
69
|
'worklet';
|
|
55
70
|
if (finished) {
|
|
56
71
|
// Reset ALL global state after settle animation
|
|
@@ -59,15 +74,19 @@ function useDropCompensation(config) {
|
|
|
59
74
|
draggedIndex.value = -1;
|
|
60
75
|
draggedItemId.value = '';
|
|
61
76
|
measuredItemHeight.value = 0;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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;
|
|
65
80
|
}
|
|
66
81
|
});
|
|
67
82
|
}
|
|
68
|
-
else if (Math.abs(translateY.value) >
|
|
69
|
-
// Non-dragged
|
|
70
|
-
|
|
83
|
+
else if (Math.abs(translateY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD) {
|
|
84
|
+
// Issue 3 Fix: Non-dragged items now animate their compensation for visual consistency
|
|
85
|
+
// This prevents the jarring "jump" when only the dragged item animates smoothly
|
|
86
|
+
translateY.value = (0, react_native_reanimated_1.withTiming)(compensatedY, {
|
|
87
|
+
duration: constants_1.ANIMATION_TIMING.COMPENSATION_DURATION,
|
|
88
|
+
easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
|
|
89
|
+
});
|
|
71
90
|
}
|
|
72
91
|
// Update tracked index
|
|
73
92
|
setPrevIndex(index);
|
|
@@ -83,7 +102,7 @@ function useDropCompensation(config) {
|
|
|
83
102
|
'worklet';
|
|
84
103
|
if (prevIdx !== null &&
|
|
85
104
|
currentIndex !== prevIdx &&
|
|
86
|
-
Math.abs(shiftY.value) >
|
|
105
|
+
Math.abs(shiftY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD) {
|
|
87
106
|
shiftY.value = 0;
|
|
88
107
|
}
|
|
89
108
|
});
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
2
|
+
* @souscheflabs/reanimated-flashlist
|
|
3
3
|
*
|
|
4
4
|
* A high-performance animated FlashList with drag-to-reorder and entry/exit animations.
|
|
5
5
|
*
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* import {
|
|
9
9
|
* AnimatedFlashList,
|
|
10
10
|
* type AnimatedListItem,
|
|
11
|
-
* } from '@
|
|
11
|
+
* } from '@souscheflabs/reanimated-flashlist';
|
|
12
12
|
*
|
|
13
13
|
* interface MyItem extends AnimatedListItem {
|
|
14
14
|
* title: string;
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* @
|
|
3
|
+
* @souscheflabs/reanimated-flashlist
|
|
4
4
|
*
|
|
5
5
|
* A high-performance animated FlashList with drag-to-reorder and entry/exit animations.
|
|
6
6
|
*
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* import {
|
|
10
10
|
* AnimatedFlashList,
|
|
11
11
|
* type AnimatedListItem,
|
|
12
|
-
* } from '@
|
|
12
|
+
* } from '@souscheflabs/reanimated-flashlist';
|
|
13
13
|
*
|
|
14
14
|
* interface MyItem extends AnimatedListItem {
|
|
15
15
|
* title: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@souscheflabs/reanimated-flashlist",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useLayoutEffect, useMemo, useCallback, useRef } from 'react';
|
|
2
2
|
import Animated, { useAnimatedRef } from 'react-native-reanimated';
|
|
3
|
-
import type {
|
|
3
|
+
import type { LayoutChangeEvent } from 'react-native';
|
|
4
4
|
import {
|
|
5
5
|
useDragGesture,
|
|
6
6
|
useDragShift,
|
|
@@ -131,13 +131,6 @@ function AnimatedFlashListItemInner<T extends AnimatedListItem>({
|
|
|
131
131
|
measuredHeightRef.current = event.nativeEvent.layout.height;
|
|
132
132
|
}, []);
|
|
133
133
|
|
|
134
|
-
// Create combined animated style
|
|
135
|
-
const combinedAnimatedStyle = useMemo<ViewStyle>(() => {
|
|
136
|
-
// We can't directly combine animated styles here since they're worklet-based
|
|
137
|
-
// Instead, we'll let the consumer apply them via the render prop
|
|
138
|
-
return {};
|
|
139
|
-
}, []);
|
|
140
|
-
|
|
141
134
|
// Create drag handle props
|
|
142
135
|
const dragHandleProps = useMemo(
|
|
143
136
|
() =>
|
|
@@ -163,12 +156,14 @@ function AnimatedFlashListItemInner<T extends AnimatedListItem>({
|
|
|
163
156
|
);
|
|
164
157
|
|
|
165
158
|
// Create render info
|
|
159
|
+
// Note: animatedStyle is empty because animations are applied to the wrapper View automatically.
|
|
160
|
+
// This field is kept for backward compatibility with the AnimatedRenderItemInfo interface.
|
|
166
161
|
const renderInfo = useMemo<AnimatedRenderItemInfo<T>>(
|
|
167
162
|
() => ({
|
|
168
163
|
item,
|
|
169
164
|
index,
|
|
170
165
|
totalItems,
|
|
171
|
-
animatedStyle:
|
|
166
|
+
animatedStyle: {},
|
|
172
167
|
dragHandleProps,
|
|
173
168
|
isDragging: false, // This is a SharedValue, consumer should use dragHandleProps.isDragging
|
|
174
169
|
isDragEnabled,
|
|
@@ -179,7 +174,6 @@ function AnimatedFlashListItemInner<T extends AnimatedListItem>({
|
|
|
179
174
|
item,
|
|
180
175
|
index,
|
|
181
176
|
totalItems,
|
|
182
|
-
combinedAnimatedStyle,
|
|
183
177
|
dragHandleProps,
|
|
184
178
|
isDragEnabled,
|
|
185
179
|
triggerExitAnimation,
|
|
@@ -309,15 +309,15 @@ describe('ListAnimationContext', () => {
|
|
|
309
309
|
wrapper: createWrapper(),
|
|
310
310
|
});
|
|
311
311
|
|
|
312
|
-
expect(result.current.layoutAnimationDuration).toBe(
|
|
312
|
+
expect(result.current.layoutAnimationDuration).toBe(300);
|
|
313
313
|
});
|
|
314
314
|
|
|
315
315
|
it('uses custom layoutAnimationDuration', () => {
|
|
316
316
|
const { result } = renderHook(() => useListAnimation(), {
|
|
317
|
-
wrapper: createWrapper({ layoutAnimationDuration:
|
|
317
|
+
wrapper: createWrapper({ layoutAnimationDuration: 400 }),
|
|
318
318
|
});
|
|
319
319
|
|
|
320
|
-
expect(result.current.layoutAnimationDuration).toBe(
|
|
320
|
+
expect(result.current.layoutAnimationDuration).toBe(400);
|
|
321
321
|
});
|
|
322
322
|
});
|
|
323
323
|
});
|
|
@@ -32,7 +32,7 @@ export const DEFAULT_EXIT_ANIMATION: ExitAnimationConfig = {
|
|
|
32
32
|
},
|
|
33
33
|
removalDelay: 300,
|
|
34
34
|
layoutAnimation: {
|
|
35
|
-
duration:
|
|
35
|
+
duration: 300,
|
|
36
36
|
},
|
|
37
37
|
};
|
|
38
38
|
|
|
@@ -60,7 +60,9 @@ export const FAST_EXIT_ANIMATION: ExitAnimationConfig = {
|
|
|
60
60
|
},
|
|
61
61
|
removalDelay: 200,
|
|
62
62
|
layoutAnimation: {
|
|
63
|
-
|
|
63
|
+
// Slightly longer than the exit animation for a smoother overall effect
|
|
64
|
+
// Items below start moving up as the exiting item is sliding out
|
|
65
|
+
duration: 250,
|
|
64
66
|
},
|
|
65
67
|
};
|
|
66
68
|
|
package/src/constants/drag.ts
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
import type { DragConfig } from '../types';
|
|
2
2
|
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Animation Timing & Easing Constants
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Unified animation timing configuration for consistency across all drag hooks.
|
|
9
|
+
* Using consistent timing prevents visual "mismatches" during complex interactions.
|
|
10
|
+
*/
|
|
11
|
+
export const ANIMATION_TIMING = {
|
|
12
|
+
/** Duration for item shift animations (when items move to make room) */
|
|
13
|
+
SHIFT_DURATION: 100,
|
|
14
|
+
/** Duration for drop settle animation (when dragged item animates to final position) */
|
|
15
|
+
DROP_SETTLE_DURATION: 150,
|
|
16
|
+
/** Duration for shadow opacity fade transition */
|
|
17
|
+
SHADOW_FADE_DURATION: 100,
|
|
18
|
+
/** Duration for non-dragged item compensation animations */
|
|
19
|
+
COMPENSATION_DURATION: 100,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Threshold Constants
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configurable thresholds for drag calculations.
|
|
28
|
+
* These values control sensitivity and visual behavior.
|
|
29
|
+
*/
|
|
30
|
+
export const DRAG_THRESHOLDS = {
|
|
31
|
+
/**
|
|
32
|
+
* Offset factor for hover position calculation.
|
|
33
|
+
* This adds a bias when determining which index is being hovered over,
|
|
34
|
+
* making it slightly easier to "cross" into the next/prev position.
|
|
35
|
+
* Range: 0-0.5, where 0 = exact center, 0.5 = edge of item
|
|
36
|
+
*/
|
|
37
|
+
HOVER_OFFSET_FACTOR: 0.2,
|
|
38
|
+
/**
|
|
39
|
+
* Z-index for elevated (dragged) items.
|
|
40
|
+
* Should be high enough to appear above other UI but not conflict
|
|
41
|
+
* with modals or overlays (which typically use 1000+).
|
|
42
|
+
*/
|
|
43
|
+
ELEVATED_Z_INDEX: 999,
|
|
44
|
+
/**
|
|
45
|
+
* Minimum translateY/shiftY value to consider "significant".
|
|
46
|
+
* Values below this are treated as zero for optimization.
|
|
47
|
+
*/
|
|
48
|
+
SHIFT_SIGNIFICANCE_THRESHOLD: 1,
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Default Drag Configuration
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
3
55
|
/**
|
|
4
56
|
* Default drag configuration
|
|
5
57
|
* All values can be overridden via AnimatedFlashList config prop
|
|
@@ -88,15 +88,55 @@ interface DragStateProviderProps {
|
|
|
88
88
|
*
|
|
89
89
|
* Using SharedValues ensures animations run on the UI thread at 60fps.
|
|
90
90
|
*/
|
|
91
|
+
/**
|
|
92
|
+
* Validates drag configuration values to prevent runtime errors.
|
|
93
|
+
* Logs warnings in development for invalid configurations.
|
|
94
|
+
*/
|
|
95
|
+
function validateConfig(config: DragConfig): void {
|
|
96
|
+
if (__DEV__) {
|
|
97
|
+
if (config.itemHeight <= 0) {
|
|
98
|
+
console.warn(
|
|
99
|
+
'[AnimatedFlashList] Invalid itemHeight: must be positive. Got:',
|
|
100
|
+
config.itemHeight,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (config.longPressDuration <= 0) {
|
|
104
|
+
console.warn(
|
|
105
|
+
'[AnimatedFlashList] Invalid longPressDuration: must be positive. Got:',
|
|
106
|
+
config.longPressDuration,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (config.dragScale <= 0) {
|
|
110
|
+
console.warn(
|
|
111
|
+
'[AnimatedFlashList] Invalid dragScale: must be positive. Got:',
|
|
112
|
+
config.dragScale,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (config.edgeThreshold < 0) {
|
|
116
|
+
console.warn(
|
|
117
|
+
'[AnimatedFlashList] Invalid edgeThreshold: must be non-negative. Got:',
|
|
118
|
+
config.edgeThreshold,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (config.maxScrollSpeed <= 0) {
|
|
122
|
+
console.warn(
|
|
123
|
+
'[AnimatedFlashList] Invalid maxScrollSpeed: must be positive. Got:',
|
|
124
|
+
config.maxScrollSpeed,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
91
130
|
export const DragStateProvider: React.FC<DragStateProviderProps> = ({
|
|
92
131
|
children,
|
|
93
132
|
config: configOverrides,
|
|
94
133
|
}) => {
|
|
95
|
-
// Merge config with defaults
|
|
96
|
-
const config = useMemo<DragConfig>(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
134
|
+
// Merge config with defaults and validate
|
|
135
|
+
const config = useMemo<DragConfig>(() => {
|
|
136
|
+
const mergedConfig = { ...DEFAULT_DRAG_CONFIG, ...configOverrides };
|
|
137
|
+
validateConfig(mergedConfig);
|
|
138
|
+
return mergedConfig;
|
|
139
|
+
}, [configOverrides]);
|
|
100
140
|
|
|
101
141
|
// Shared values are created once and persist for the lifetime of the provider
|
|
102
142
|
const isDragging = useSharedValue(false);
|
|
@@ -120,8 +120,11 @@ interface ListAnimationProviderProps {
|
|
|
120
120
|
*/
|
|
121
121
|
entryAnimationTimeout?: number;
|
|
122
122
|
/**
|
|
123
|
-
* Duration for layout animations when items are removed (ms)
|
|
124
|
-
*
|
|
123
|
+
* Duration for layout animations when items are removed (ms).
|
|
124
|
+
* This controls how fast remaining items animate into their new positions
|
|
125
|
+
* after an item is removed from the list.
|
|
126
|
+
* Lower values = faster/snappier, higher values = smoother/more gradual
|
|
127
|
+
* @default 300
|
|
125
128
|
*/
|
|
126
129
|
layoutAnimationDuration?: number;
|
|
127
130
|
/**
|
|
@@ -143,7 +146,7 @@ interface ListAnimationProviderProps {
|
|
|
143
146
|
export const ListAnimationProvider: React.FC<ListAnimationProviderProps> = ({
|
|
144
147
|
children,
|
|
145
148
|
entryAnimationTimeout = 5000,
|
|
146
|
-
layoutAnimationDuration =
|
|
149
|
+
layoutAnimationDuration = 300,
|
|
147
150
|
enableLayoutAnimation = true,
|
|
148
151
|
}) => {
|
|
149
152
|
// Map of itemId -> exit animation trigger function
|
|
@@ -234,14 +237,18 @@ export const ListAnimationProvider: React.FC<ListAnimationProviderProps> = ({
|
|
|
234
237
|
|
|
235
238
|
// Configure native LayoutAnimation for remaining items
|
|
236
239
|
if (enableLayoutAnimation) {
|
|
237
|
-
// Use
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
240
|
+
// Use spring animation for smoother, more natural item transitions
|
|
241
|
+
// Spring animations better handle position changes than timed animations
|
|
242
|
+
// and provide a more "organic" feel to remaining items moving up
|
|
243
|
+
LayoutAnimation.configureNext(
|
|
244
|
+
LayoutAnimation.create(
|
|
245
|
+
layoutAnimationDuration,
|
|
246
|
+
LayoutAnimation.Types.easeOut,
|
|
247
|
+
// Use opacity as the trigger property - this works better for
|
|
248
|
+
// position-based animations than scaleY which can cause visual artifacts
|
|
249
|
+
LayoutAnimation.Properties.opacity,
|
|
250
|
+
),
|
|
251
|
+
);
|
|
245
252
|
}
|
|
246
253
|
},
|
|
247
254
|
[enableLayoutAnimation, layoutAnimationDuration, exitingItemsVersion],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
1
|
+
import { useEffect, useRef, useMemo } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
useAnimatedStyle,
|
|
4
4
|
useSharedValue,
|
|
@@ -42,11 +42,14 @@ export const useListEntryAnimation = (
|
|
|
42
42
|
) => {
|
|
43
43
|
const animationContext = useListAnimationOptional();
|
|
44
44
|
|
|
45
|
-
//
|
|
46
|
-
const config =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
// Memoize merged config to prevent unnecessary re-renders and effect re-runs
|
|
46
|
+
const config = useMemo(
|
|
47
|
+
() => ({
|
|
48
|
+
fade: { ...DEFAULT_ENTRY_ANIMATION.fade, ...configOverrides?.fade },
|
|
49
|
+
slide: { ...DEFAULT_ENTRY_ANIMATION.slide, ...configOverrides?.slide },
|
|
50
|
+
}),
|
|
51
|
+
[configOverrides],
|
|
52
|
+
);
|
|
50
53
|
|
|
51
54
|
// Shared values for entry animation - start at final position (no animation by default)
|
|
52
55
|
const translateX = useSharedValue(0);
|
|
@@ -158,7 +158,12 @@ export const useListExitAnimation = (
|
|
|
158
158
|
|
|
159
159
|
// Register exiting item for layout compensation (if configured)
|
|
160
160
|
if (config?.onExitStart && config.index !== undefined) {
|
|
161
|
-
|
|
161
|
+
// Use measured height if available and valid (> 0), otherwise use animation distance
|
|
162
|
+
// This handles the case where layout hasn't been measured yet (height = 0)
|
|
163
|
+
const height =
|
|
164
|
+
config.measuredHeight && config.measuredHeight > 0
|
|
165
|
+
? config.measuredHeight
|
|
166
|
+
: animConfig.slide.distance;
|
|
162
167
|
config.onExitStart(config.index, height);
|
|
163
168
|
}
|
|
164
169
|
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useAnimatedStyle,
|
|
3
|
+
interpolate,
|
|
4
|
+
useSharedValue,
|
|
5
|
+
useAnimatedReaction,
|
|
6
|
+
withTiming,
|
|
7
|
+
} from 'react-native-reanimated';
|
|
2
8
|
import type { SharedValue } from 'react-native-reanimated';
|
|
3
9
|
import type { ViewStyle } from 'react-native';
|
|
4
10
|
import { useDragState } from '../../contexts/DragStateContext';
|
|
11
|
+
import { ANIMATION_TIMING, DRAG_THRESHOLDS } from '../../constants';
|
|
5
12
|
import type { UseDragAnimatedStyleResult } from '../../types';
|
|
6
13
|
|
|
7
14
|
/**
|
|
@@ -43,19 +50,46 @@ export function useDragAnimatedStyle(
|
|
|
43
50
|
const { draggedItemId, draggedScale, config, scrollOffset, dragStartScrollOffset } =
|
|
44
51
|
useDragState();
|
|
45
52
|
|
|
53
|
+
// Issue 2 Fix: Track shadow opacity with smooth transitions to prevent "flash"
|
|
54
|
+
// When drag ends, shadow opacity should fade smoothly instead of snapping
|
|
55
|
+
const animatedShadowOpacity = useSharedValue(0.1);
|
|
56
|
+
|
|
57
|
+
// React to drag state changes and animate shadow opacity
|
|
58
|
+
useAnimatedReaction(
|
|
59
|
+
() => ({
|
|
60
|
+
isThisItemDragged: draggedItemId.value === itemId,
|
|
61
|
+
scale: draggedScale.value,
|
|
62
|
+
}),
|
|
63
|
+
(current, previous) => {
|
|
64
|
+
'worklet';
|
|
65
|
+
if (current.isThisItemDragged) {
|
|
66
|
+
// When dragged, interpolate shadow opacity based on scale
|
|
67
|
+
const targetOpacity = interpolate(
|
|
68
|
+
current.scale,
|
|
69
|
+
[1, config.dragScale],
|
|
70
|
+
[0.1, config.dragShadowOpacity],
|
|
71
|
+
);
|
|
72
|
+
animatedShadowOpacity.value = targetOpacity;
|
|
73
|
+
} else if (previous?.isThisItemDragged && !current.isThisItemDragged) {
|
|
74
|
+
// Transition from dragged to not-dragged: fade shadow smoothly
|
|
75
|
+
animatedShadowOpacity.value = withTiming(0.1, {
|
|
76
|
+
duration: ANIMATION_TIMING.SHADOW_FADE_DURATION,
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
// Non-dragged item: keep at base opacity
|
|
80
|
+
animatedShadowOpacity.value = 0.1;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
46
85
|
// Animated style for drag offset with scale and shadow
|
|
47
86
|
const dragAnimatedStyle = useAnimatedStyle(() => {
|
|
48
87
|
const isThisItemDragged = draggedItemId.value === itemId;
|
|
49
88
|
|
|
50
89
|
// Keep elevated if: actively dragging OR has offset (animating back)
|
|
51
90
|
const shouldBeElevated =
|
|
52
|
-
isDragging.value ||
|
|
53
|
-
|
|
54
|
-
const shadowOpacity = interpolate(
|
|
55
|
-
draggedScale.value,
|
|
56
|
-
[1, config.dragScale],
|
|
57
|
-
[0.1, config.dragShadowOpacity],
|
|
58
|
-
);
|
|
91
|
+
isDragging.value ||
|
|
92
|
+
Math.abs(translateY.value) > DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
|
|
59
93
|
|
|
60
94
|
// Calculate scroll delta for position compensation during autoscroll
|
|
61
95
|
const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
|
|
@@ -70,8 +104,9 @@ export function useDragAnimatedStyle(
|
|
|
70
104
|
{ translateY: yOffset },
|
|
71
105
|
{ scale: isThisItemDragged ? draggedScale.value : 1 },
|
|
72
106
|
],
|
|
73
|
-
zIndex: shouldBeElevated ?
|
|
74
|
-
|
|
107
|
+
zIndex: shouldBeElevated ? DRAG_THRESHOLDS.ELEVATED_Z_INDEX : 0,
|
|
108
|
+
// Use animated shadow opacity for smooth transitions
|
|
109
|
+
shadowOpacity: animatedShadowOpacity.value,
|
|
75
110
|
elevation: isDragging.value ? 12 : 4,
|
|
76
111
|
} as ViewStyle;
|
|
77
112
|
});
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Easing,
|
|
7
7
|
} from 'react-native-reanimated';
|
|
8
8
|
import { useDragState } from '../../contexts/DragStateContext';
|
|
9
|
+
import { ANIMATION_TIMING, DRAG_THRESHOLDS } from '../../constants';
|
|
9
10
|
import type { UseDragShiftConfig, UseDragShiftResult } from '../../types';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -49,7 +50,21 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
|
|
|
49
50
|
// Calculate target shift using useDerivedValue
|
|
50
51
|
const targetShiftY = useDerivedValue(() => {
|
|
51
52
|
'worklet';
|
|
52
|
-
|
|
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
|
+
*/
|
|
53
68
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
54
69
|
dragUpdateTrigger.value;
|
|
55
70
|
|
|
@@ -80,8 +95,14 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
|
|
|
80
95
|
const effectiveTranslateY = translateYNow + scrollDelta;
|
|
81
96
|
|
|
82
97
|
// Calculate which index the dragged item is hovering over
|
|
98
|
+
// The HOVER_OFFSET_FACTOR adds a small bias to make crossing into
|
|
99
|
+
// the next position slightly easier (prevents "sticky" hover positions)
|
|
83
100
|
const offset =
|
|
84
|
-
effectiveTranslateY > 0
|
|
101
|
+
effectiveTranslateY > 0
|
|
102
|
+
? DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR
|
|
103
|
+
: effectiveTranslateY < 0
|
|
104
|
+
? -DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR
|
|
105
|
+
: 0;
|
|
85
106
|
const hoveredIndex =
|
|
86
107
|
currentDraggedIndex + Math.round(effectiveTranslateY / itemHeight + offset);
|
|
87
108
|
|
|
@@ -108,7 +129,7 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
|
|
|
108
129
|
'worklet';
|
|
109
130
|
if (target !== prev) {
|
|
110
131
|
shiftY.value = withTiming(target, {
|
|
111
|
-
duration:
|
|
132
|
+
duration: ANIMATION_TIMING.SHIFT_DURATION,
|
|
112
133
|
easing: Easing.out(Easing.ease),
|
|
113
134
|
});
|
|
114
135
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from 'react-native-reanimated';
|
|
8
8
|
import { useRecyclingState } from '@shopify/flash-list';
|
|
9
9
|
import { useDragState } from '../../contexts/DragStateContext';
|
|
10
|
+
import { ANIMATION_TIMING, DRAG_THRESHOLDS } from '../../constants';
|
|
10
11
|
import type { UseDropCompensationConfig } from '../../types';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -56,9 +57,23 @@ export function useDropCompensation(config: UseDropCompensationConfig): void {
|
|
|
56
57
|
// Handle index changes after data updates (drop compensation)
|
|
57
58
|
useLayoutEffect(() => {
|
|
58
59
|
if (index !== prevIndex) {
|
|
60
|
+
// Guard: Skip compensation during active drop transition to prevent double-compensation
|
|
61
|
+
// The isDropping flag is set when drop begins and cleared after settle animation
|
|
62
|
+
if (isDropping.value && draggedItemId.value !== itemId) {
|
|
63
|
+
// Non-dragged items should wait for drop to complete
|
|
64
|
+
setPrevIndex(index);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Issue 1 Fix: Use dynamically measured height when available, matching useDragShift behavior
|
|
69
|
+
const itemHeight =
|
|
70
|
+
measuredItemHeight.value > 0
|
|
71
|
+
? measuredItemHeight.value + dragConfig.itemVerticalMargin
|
|
72
|
+
: dragConfig.itemHeight;
|
|
73
|
+
|
|
59
74
|
// Compensate for index change by adjusting translateY
|
|
60
75
|
const indexDelta = index - prevIndex;
|
|
61
|
-
const heightDelta = indexDelta *
|
|
76
|
+
const heightDelta = indexDelta * itemHeight;
|
|
62
77
|
|
|
63
78
|
// Check if this is the dragged item completing its drop
|
|
64
79
|
const isTheDraggedItem = draggedItemId.value === itemId;
|
|
@@ -71,7 +86,10 @@ export function useDropCompensation(config: UseDropCompensationConfig): void {
|
|
|
71
86
|
translateY.value = compensatedY;
|
|
72
87
|
translateY.value = withTiming(
|
|
73
88
|
0,
|
|
74
|
-
{
|
|
89
|
+
{
|
|
90
|
+
duration: ANIMATION_TIMING.DROP_SETTLE_DURATION,
|
|
91
|
+
easing: Easing.out(Easing.ease),
|
|
92
|
+
},
|
|
75
93
|
finished => {
|
|
76
94
|
'worklet';
|
|
77
95
|
if (finished) {
|
|
@@ -81,15 +99,19 @@ export function useDropCompensation(config: UseDropCompensationConfig): void {
|
|
|
81
99
|
draggedIndex.value = -1;
|
|
82
100
|
draggedItemId.value = '';
|
|
83
101
|
measuredItemHeight.value = 0;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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;
|
|
87
105
|
}
|
|
88
106
|
},
|
|
89
107
|
);
|
|
90
|
-
} else if (Math.abs(translateY.value) >
|
|
91
|
-
// Non-dragged
|
|
92
|
-
|
|
108
|
+
} else if (Math.abs(translateY.value) > DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD) {
|
|
109
|
+
// Issue 3 Fix: Non-dragged items now animate their compensation for visual consistency
|
|
110
|
+
// This prevents the jarring "jump" when only the dragged item animates smoothly
|
|
111
|
+
translateY.value = withTiming(compensatedY, {
|
|
112
|
+
duration: ANIMATION_TIMING.COMPENSATION_DURATION,
|
|
113
|
+
easing: Easing.out(Easing.ease),
|
|
114
|
+
});
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
// Update tracked index
|
|
@@ -111,7 +133,7 @@ export function useDropCompensation(config: UseDropCompensationConfig): void {
|
|
|
111
133
|
if (
|
|
112
134
|
prevIdx !== null &&
|
|
113
135
|
currentIndex !== prevIdx &&
|
|
114
|
-
Math.abs(shiftY.value) >
|
|
136
|
+
Math.abs(shiftY.value) > DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD
|
|
115
137
|
) {
|
|
116
138
|
shiftY.value = 0;
|
|
117
139
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
2
|
+
* @souscheflabs/reanimated-flashlist
|
|
3
3
|
*
|
|
4
4
|
* A high-performance animated FlashList with drag-to-reorder and entry/exit animations.
|
|
5
5
|
*
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* import {
|
|
9
9
|
* AnimatedFlashList,
|
|
10
10
|
* type AnimatedListItem,
|
|
11
|
-
* } from '@
|
|
11
|
+
* } from '@souscheflabs/reanimated-flashlist';
|
|
12
12
|
*
|
|
13
13
|
* interface MyItem extends AnimatedListItem {
|
|
14
14
|
* title: string;
|