react-native-reorderable-list 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +118 -60
- package/lib/commonjs/components/ReorderableList/ReorderableList.js +99 -0
- package/lib/commonjs/components/ReorderableList/ReorderableList.js.map +1 -0
- package/lib/commonjs/components/ReorderableList/constants.ios.js +10 -0
- package/lib/commonjs/components/ReorderableList/constants.ios.js.map +1 -0
- package/lib/commonjs/components/ReorderableList/constants.js +10 -0
- package/lib/commonjs/components/ReorderableList/constants.js.map +1 -0
- package/lib/commonjs/components/ReorderableList/index.js +17 -0
- package/lib/commonjs/components/ReorderableList/index.js.map +1 -0
- package/lib/commonjs/components/ReorderableList/useReorderableList.js +311 -0
- package/lib/commonjs/components/ReorderableList/useReorderableList.js.map +1 -0
- package/lib/commonjs/components/ReorderableListCell.js +93 -0
- package/lib/commonjs/components/ReorderableListCell.js.map +1 -0
- package/lib/commonjs/components/ReorderableListItem.js +87 -0
- package/lib/commonjs/components/ReorderableListItem.js.map +1 -0
- package/lib/commonjs/components/index.js +39 -0
- package/lib/commonjs/components/index.js.map +1 -0
- package/lib/commonjs/contexts/ReorderableCellContext.js +10 -0
- package/lib/commonjs/contexts/ReorderableCellContext.js.map +1 -0
- package/lib/commonjs/contexts/ReorderableListContext.js +10 -0
- package/lib/commonjs/contexts/ReorderableListContext.js.map +1 -0
- package/lib/commonjs/contexts/index.js +28 -0
- package/lib/commonjs/contexts/index.js.map +1 -0
- package/lib/commonjs/hooks/index.js +50 -0
- package/lib/commonjs/hooks/index.js.map +1 -0
- package/lib/commonjs/hooks/useContext.js +16 -0
- package/lib/commonjs/hooks/useContext.js.map +1 -0
- package/lib/commonjs/hooks/useReorderableDrag.js +16 -0
- package/lib/commonjs/hooks/useReorderableDrag.js.map +1 -0
- package/lib/commonjs/hooks/useReorderableDragEnd.js +28 -0
- package/lib/commonjs/hooks/useReorderableDragEnd.js.map +1 -0
- package/lib/commonjs/hooks/useReorderableDragStart.js +22 -0
- package/lib/commonjs/hooks/useReorderableDragStart.js.map +1 -0
- package/lib/commonjs/index.js +41 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/types/index.js +28 -0
- package/lib/commonjs/types/index.js.map +1 -0
- package/lib/commonjs/types/misc.js +14 -0
- package/lib/commonjs/types/misc.js.map +1 -0
- package/lib/commonjs/types/props.js +6 -0
- package/lib/commonjs/types/props.js.map +1 -0
- package/lib/commonjs/utils.js +23 -0
- package/lib/commonjs/utils.js.map +1 -0
- package/lib/module/components/ReorderableList/ReorderableList.js +91 -0
- package/lib/module/components/ReorderableList/ReorderableList.js.map +1 -0
- package/lib/module/components/ReorderableList/constants.ios.js +4 -0
- package/lib/module/components/ReorderableList/constants.ios.js.map +1 -0
- package/lib/module/components/ReorderableList/constants.js +4 -0
- package/lib/module/components/ReorderableList/constants.js.map +1 -0
- package/lib/module/components/ReorderableList/index.js +2 -0
- package/lib/module/components/ReorderableList/index.js.map +1 -0
- package/lib/module/components/ReorderableList/useReorderableList.js +302 -0
- package/lib/module/components/ReorderableList/useReorderableList.js.map +1 -0
- package/lib/module/components/ReorderableListCell.js +85 -0
- package/lib/module/components/ReorderableListCell.js.map +1 -0
- package/lib/module/components/ReorderableListItem.js +78 -0
- package/lib/module/components/ReorderableListItem.js.map +1 -0
- package/lib/module/components/index.js +4 -0
- package/lib/module/components/index.js.map +1 -0
- package/lib/module/contexts/ReorderableCellContext.js +3 -0
- package/lib/module/contexts/ReorderableCellContext.js.map +1 -0
- package/lib/module/contexts/ReorderableListContext.js +3 -0
- package/lib/module/contexts/ReorderableListContext.js.map +1 -0
- package/lib/module/contexts/index.js +3 -0
- package/lib/module/contexts/index.js.map +1 -0
- package/lib/module/hooks/index.js +5 -0
- package/lib/module/hooks/index.js.map +1 -0
- package/lib/module/hooks/useContext.js +9 -0
- package/lib/module/hooks/useContext.js.map +1 -0
- package/lib/module/hooks/useReorderableDrag.js +9 -0
- package/lib/module/hooks/useReorderableDrag.js.map +1 -0
- package/lib/module/hooks/useReorderableDragEnd.js +21 -0
- package/lib/module/hooks/useReorderableDragEnd.js.map +1 -0
- package/lib/module/hooks/useReorderableDragStart.js +15 -0
- package/lib/module/hooks/useReorderableDragStart.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/types/index.js +3 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/types/misc.js +8 -0
- package/lib/module/types/misc.js.map +1 -0
- package/lib/module/types/props.js +2 -0
- package/lib/module/types/props.js.map +1 -0
- package/lib/module/utils.js +16 -0
- package/lib/module/utils.js.map +1 -0
- package/lib/typescript/components/ReorderableList/ReorderableList.d.ts +8 -0
- package/lib/typescript/components/ReorderableList/ReorderableList.d.ts.map +1 -0
- package/lib/typescript/components/ReorderableList/constants.d.ts +3 -0
- package/lib/typescript/components/ReorderableList/constants.d.ts.map +1 -0
- package/lib/typescript/components/ReorderableList/constants.ios.d.ts +3 -0
- package/lib/typescript/components/ReorderableList/constants.ios.d.ts.map +1 -0
- package/lib/typescript/components/ReorderableList/index.d.ts +2 -0
- package/lib/typescript/components/ReorderableList/index.d.ts.map +1 -0
- package/lib/typescript/components/ReorderableList/useReorderableList.d.ts +34 -0
- package/lib/typescript/components/ReorderableList/useReorderableList.d.ts.map +1 -0
- package/lib/typescript/components/ReorderableListCell.d.ts +20 -0
- package/lib/typescript/components/ReorderableListCell.d.ts.map +1 -0
- package/lib/typescript/components/ReorderableListItem.d.ts +4 -0
- package/lib/typescript/components/ReorderableListItem.d.ts.map +1 -0
- package/lib/typescript/components/index.d.ts +4 -0
- package/lib/typescript/components/index.d.ts.map +1 -0
- package/lib/typescript/contexts/ReorderableCellContext.d.ts +11 -0
- package/lib/typescript/contexts/ReorderableCellContext.d.ts.map +1 -0
- package/lib/typescript/contexts/ReorderableListContext.d.ts +9 -0
- package/lib/typescript/contexts/ReorderableListContext.d.ts.map +1 -0
- package/lib/typescript/contexts/index.d.ts +3 -0
- package/lib/typescript/contexts/index.d.ts.map +1 -0
- package/lib/typescript/hooks/index.d.ts +5 -0
- package/lib/typescript/hooks/index.d.ts.map +1 -0
- package/lib/typescript/hooks/useContext.d.ts +3 -0
- package/lib/typescript/hooks/useContext.d.ts.map +1 -0
- package/lib/typescript/hooks/useReorderableDrag.d.ts +2 -0
- package/lib/typescript/hooks/useReorderableDrag.d.ts.map +1 -0
- package/lib/typescript/hooks/useReorderableDragEnd.d.ts +2 -0
- package/lib/typescript/hooks/useReorderableDragEnd.d.ts.map +1 -0
- package/lib/typescript/hooks/useReorderableDragStart.d.ts +2 -0
- package/lib/typescript/hooks/useReorderableDragStart.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +7 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/types/index.d.ts +3 -0
- package/lib/typescript/types/index.d.ts.map +1 -0
- package/lib/typescript/types/misc.d.ts +7 -0
- package/lib/typescript/types/misc.d.ts.map +1 -0
- package/lib/typescript/types/props.d.ts +90 -0
- package/lib/typescript/types/props.d.ts.map +1 -0
- package/lib/typescript/utils.d.ts +12 -0
- package/lib/typescript/utils.d.ts.map +1 -0
- package/package.json +199 -39
- package/src/components/ReorderableList/ReorderableList.tsx +123 -0
- package/src/components/ReorderableList/constants.ios.ts +3 -0
- package/src/components/ReorderableList/constants.ts +3 -0
- package/src/components/ReorderableList/index.ts +1 -0
- package/src/components/ReorderableList/useReorderableList.ts +487 -0
- package/src/components/ReorderableListCell.tsx +143 -0
- package/src/components/ReorderableListItem.tsx +108 -0
- package/src/components/index.ts +3 -0
- package/src/contexts/ReorderableCellContext.ts +14 -0
- package/src/contexts/ReorderableListContext.ts +12 -0
- package/src/contexts/index.ts +2 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useContext.ts +11 -0
- package/src/hooks/useReorderableDrag.ts +7 -0
- package/src/hooks/useReorderableDragEnd.ts +25 -0
- package/src/hooks/useReorderableDragStart.ts +18 -0
- package/src/index.ts +26 -0
- package/src/types/index.ts +2 -0
- package/src/types/misc.ts +6 -0
- package/src/types/props.ts +101 -0
- package/src/utils.ts +15 -0
- package/dist/components/ReorderableList.d.ts +0 -7
- package/dist/components/ReorderableList.js +0 -314
- package/dist/components/ReorderableListItem.d.ts +0 -14
- package/dist/components/ReorderableListItem.js +0 -57
- package/dist/hooks/useAnimatedSharedValues.d.ts +0 -3
- package/dist/hooks/useAnimatedSharedValues.js +0 -33
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -2
- package/dist/types/misc.d.ts +0 -15
- package/dist/types/misc.js +0 -7
- package/dist/types/props.d.ts +0 -29
- package/dist/types/props.js +0 -1
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import React, {useCallback, useMemo} from 'react';
|
|
2
|
+
import {
|
|
3
|
+
FlatList,
|
|
4
|
+
LayoutChangeEvent,
|
|
5
|
+
NativeScrollEvent,
|
|
6
|
+
unstable_batchedUpdates,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
|
|
9
|
+
import {Gesture, State} from 'react-native-gesture-handler';
|
|
10
|
+
import Animated, {
|
|
11
|
+
AnimatedRef,
|
|
12
|
+
Easing,
|
|
13
|
+
cancelAnimation,
|
|
14
|
+
measure,
|
|
15
|
+
runOnJS,
|
|
16
|
+
runOnUI,
|
|
17
|
+
scrollTo,
|
|
18
|
+
useAnimatedReaction,
|
|
19
|
+
useAnimatedRef,
|
|
20
|
+
useAnimatedScrollHandler,
|
|
21
|
+
useSharedValue,
|
|
22
|
+
withDelay,
|
|
23
|
+
withTiming,
|
|
24
|
+
} from 'react-native-reanimated';
|
|
25
|
+
|
|
26
|
+
import {AUTOSCROLL_INCREMENT} from './constants';
|
|
27
|
+
import {ReorderableListState} from '../../types';
|
|
28
|
+
import type {ReorderableListReorderEvent} from '../../types';
|
|
29
|
+
|
|
30
|
+
const version = React.version.split('.');
|
|
31
|
+
const hasAutomaticBatching = version.length
|
|
32
|
+
? parseInt(version[0], 10) >= 18
|
|
33
|
+
: false;
|
|
34
|
+
|
|
35
|
+
interface UseReorderableListArgs<T> {
|
|
36
|
+
ref: React.ForwardedRef<FlatList<T>>;
|
|
37
|
+
autoscrollThreshold: number;
|
|
38
|
+
autoscrollSpeedScale: number;
|
|
39
|
+
autoscrollDelay: number;
|
|
40
|
+
animationDuration: number;
|
|
41
|
+
dragReorderThreshold: number;
|
|
42
|
+
onReorder: (event: ReorderableListReorderEvent) => void;
|
|
43
|
+
onScroll?: (event: NativeScrollEvent) => void;
|
|
44
|
+
onLayout?: (e: LayoutChangeEvent) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const useReorderableList = <T>({
|
|
48
|
+
ref,
|
|
49
|
+
autoscrollThreshold,
|
|
50
|
+
autoscrollSpeedScale,
|
|
51
|
+
autoscrollDelay,
|
|
52
|
+
animationDuration,
|
|
53
|
+
dragReorderThreshold,
|
|
54
|
+
onLayout,
|
|
55
|
+
onReorder,
|
|
56
|
+
onScroll,
|
|
57
|
+
}: UseReorderableListArgs<T>) => {
|
|
58
|
+
const scrollEnabled = useSharedValue(true);
|
|
59
|
+
const flatList = useAnimatedRef<FlatList>();
|
|
60
|
+
const gestureState = useSharedValue<State>(State.UNDETERMINED);
|
|
61
|
+
const currentY = useSharedValue(0);
|
|
62
|
+
const currentTranslationY = useSharedValue(0);
|
|
63
|
+
const containerPositionY = useSharedValue(0);
|
|
64
|
+
const currentScrollOffsetY = useSharedValue(0);
|
|
65
|
+
const dragScrollTranslationY = useSharedValue(0);
|
|
66
|
+
const dragInitialScrollOffsetY = useSharedValue(0);
|
|
67
|
+
const draggedHeight = useSharedValue(0);
|
|
68
|
+
const itemOffset = useSharedValue<number[]>([]);
|
|
69
|
+
const itemHeight = useSharedValue<number[]>([]);
|
|
70
|
+
const topAutoscrollArea = useSharedValue(0);
|
|
71
|
+
const bottomAutoscrollArea = useSharedValue(0);
|
|
72
|
+
const autoscrollTrigger = useSharedValue(-1);
|
|
73
|
+
const lastAutoscrollTrigger = useSharedValue(-1);
|
|
74
|
+
const previousY = useSharedValue(0);
|
|
75
|
+
const dragY = useSharedValue(0);
|
|
76
|
+
const previousDirection = useSharedValue(0);
|
|
77
|
+
const previousIndex = useSharedValue(-1);
|
|
78
|
+
const currentIndex = useSharedValue(-1);
|
|
79
|
+
const draggedIndex = useSharedValue(-1);
|
|
80
|
+
const releasedIndex = useSharedValue(-1);
|
|
81
|
+
const state = useSharedValue<ReorderableListState>(ReorderableListState.IDLE);
|
|
82
|
+
|
|
83
|
+
// animation duration as a shared value allows to avoid re-rendering of all cells on value change
|
|
84
|
+
const duration = useSharedValue(animationDuration);
|
|
85
|
+
if (duration.value !== animationDuration) {
|
|
86
|
+
duration.value = animationDuration;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const listContextValue = useMemo(
|
|
90
|
+
() => ({
|
|
91
|
+
draggedHeight,
|
|
92
|
+
currentIndex,
|
|
93
|
+
draggedIndex,
|
|
94
|
+
}),
|
|
95
|
+
[draggedHeight, currentIndex, draggedIndex],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const startY = useSharedValue(0);
|
|
99
|
+
|
|
100
|
+
const panGestureHandler = useMemo(
|
|
101
|
+
() =>
|
|
102
|
+
Gesture.Pan()
|
|
103
|
+
.onBegin(e => {
|
|
104
|
+
// prevent new dragging until item is completely released
|
|
105
|
+
if (state.value === ReorderableListState.IDLE) {
|
|
106
|
+
const relativeY = e.absoluteY - containerPositionY.value;
|
|
107
|
+
|
|
108
|
+
startY.value = relativeY;
|
|
109
|
+
currentY.value = relativeY;
|
|
110
|
+
currentTranslationY.value = e.translationY;
|
|
111
|
+
if (draggedIndex.value >= 0) {
|
|
112
|
+
dragY.value = e.translationY;
|
|
113
|
+
}
|
|
114
|
+
gestureState.value = e.state;
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
.onUpdate(e => {
|
|
118
|
+
if (state.value !== ReorderableListState.RELEASING) {
|
|
119
|
+
currentY.value = startY.value + e.translationY;
|
|
120
|
+
currentTranslationY.value = e.translationY;
|
|
121
|
+
if (draggedIndex.value >= 0) {
|
|
122
|
+
dragY.value = e.translationY + dragScrollTranslationY.value;
|
|
123
|
+
}
|
|
124
|
+
gestureState.value = e.state;
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
.onEnd(e => (gestureState.value = e.state))
|
|
128
|
+
.onFinalize(e => (gestureState.value = e.state)),
|
|
129
|
+
[
|
|
130
|
+
containerPositionY,
|
|
131
|
+
currentTranslationY,
|
|
132
|
+
currentY,
|
|
133
|
+
dragScrollTranslationY,
|
|
134
|
+
draggedIndex,
|
|
135
|
+
gestureState,
|
|
136
|
+
dragY,
|
|
137
|
+
startY,
|
|
138
|
+
state,
|
|
139
|
+
],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const gestureHandler = useMemo(
|
|
143
|
+
() => Gesture.Simultaneous(Gesture.Native(), panGestureHandler),
|
|
144
|
+
[panGestureHandler],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const setScrollEnabled = useCallback(
|
|
148
|
+
(enabled: boolean) => {
|
|
149
|
+
scrollEnabled.value = enabled;
|
|
150
|
+
flatList.current?.setNativeProps({scrollEnabled: enabled});
|
|
151
|
+
},
|
|
152
|
+
[flatList, scrollEnabled],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const resetSharedValues = useCallback(() => {
|
|
156
|
+
'worklet';
|
|
157
|
+
|
|
158
|
+
// reset indexes before arrays to avoid triggering animated reactions
|
|
159
|
+
draggedIndex.value = -1;
|
|
160
|
+
// current index is reset on item render for the on end event
|
|
161
|
+
dragY.value = 0;
|
|
162
|
+
// released flag is reset after release is triggered in the item
|
|
163
|
+
state.value = ReorderableListState.IDLE;
|
|
164
|
+
dragScrollTranslationY.value = 0;
|
|
165
|
+
}, [draggedIndex, dragY, state, dragScrollTranslationY]);
|
|
166
|
+
|
|
167
|
+
const reorder = (fromIndex: number, toIndex: number) => {
|
|
168
|
+
if (fromIndex !== toIndex) {
|
|
169
|
+
const updateState = () => {
|
|
170
|
+
onReorder({from: fromIndex, to: toIndex});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (!hasAutomaticBatching) {
|
|
174
|
+
unstable_batchedUpdates(updateState);
|
|
175
|
+
} else {
|
|
176
|
+
updateState();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
runOnUI(resetSharedValues)();
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const getIndexFromY = useCallback(
|
|
184
|
+
(y: number) => {
|
|
185
|
+
'worklet';
|
|
186
|
+
|
|
187
|
+
const relativeY = currentScrollOffsetY.value + y;
|
|
188
|
+
const count = itemOffset.value.length;
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < count; i++) {
|
|
191
|
+
if (currentIndex.value === i) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const direction = i > currentIndex.value ? 1 : -1;
|
|
196
|
+
const threshold = Math.max(0, Math.min(1, dragReorderThreshold));
|
|
197
|
+
const height = itemHeight.value[i];
|
|
198
|
+
const offset = itemOffset.value[i] + height * threshold * direction;
|
|
199
|
+
|
|
200
|
+
if (
|
|
201
|
+
(i === 0 && relativeY <= offset) ||
|
|
202
|
+
(i === count - 1 && relativeY >= offset + height) ||
|
|
203
|
+
(relativeY >= offset && relativeY <= offset + height)
|
|
204
|
+
) {
|
|
205
|
+
return {index: i, direction};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
index: currentIndex.value,
|
|
211
|
+
direction: previousDirection.value,
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
[
|
|
215
|
+
dragReorderThreshold,
|
|
216
|
+
currentIndex,
|
|
217
|
+
currentScrollOffsetY,
|
|
218
|
+
previousDirection,
|
|
219
|
+
itemOffset,
|
|
220
|
+
itemHeight,
|
|
221
|
+
],
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const setCurrentIndex = useCallback(
|
|
225
|
+
(y: number) => {
|
|
226
|
+
'worklet';
|
|
227
|
+
|
|
228
|
+
const {index: newIndex, direction: newDirection} = getIndexFromY(y);
|
|
229
|
+
const delta = Math.abs(previousY.value - y);
|
|
230
|
+
|
|
231
|
+
if (
|
|
232
|
+
currentIndex.value !== newIndex &&
|
|
233
|
+
// if the same two items re-swap index check delta and direction to avoid swap flickering
|
|
234
|
+
(previousIndex.value !== newIndex ||
|
|
235
|
+
(previousDirection.value !== newDirection && delta >= 5))
|
|
236
|
+
) {
|
|
237
|
+
const itemDirection = newIndex > currentIndex.value;
|
|
238
|
+
const index1 = itemDirection ? currentIndex.value : newIndex;
|
|
239
|
+
const index2 = itemDirection ? newIndex : currentIndex.value;
|
|
240
|
+
|
|
241
|
+
const newOffset1 = itemOffset.value[index1];
|
|
242
|
+
const newHeight1 = itemHeight.value[index2];
|
|
243
|
+
const newOffset2 =
|
|
244
|
+
itemOffset.value[index2] +
|
|
245
|
+
(itemHeight.value[index2] - itemHeight.value[index1]);
|
|
246
|
+
const newHeight2 = itemHeight.value[index1];
|
|
247
|
+
|
|
248
|
+
itemOffset.value[index1] = newOffset1;
|
|
249
|
+
itemHeight.value[index1] = newHeight1;
|
|
250
|
+
itemOffset.value[index2] = newOffset2;
|
|
251
|
+
itemHeight.value[index2] = newHeight2;
|
|
252
|
+
|
|
253
|
+
previousY.value = y;
|
|
254
|
+
previousDirection.value = newDirection;
|
|
255
|
+
previousIndex.value = currentIndex.value;
|
|
256
|
+
currentIndex.value = newIndex;
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
[
|
|
260
|
+
currentIndex,
|
|
261
|
+
previousIndex,
|
|
262
|
+
previousDirection,
|
|
263
|
+
previousY,
|
|
264
|
+
itemOffset,
|
|
265
|
+
itemHeight,
|
|
266
|
+
getIndexFromY,
|
|
267
|
+
],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
useAnimatedReaction(
|
|
271
|
+
() => gestureState.value,
|
|
272
|
+
() => {
|
|
273
|
+
if (
|
|
274
|
+
gestureState.value !== State.ACTIVE &&
|
|
275
|
+
gestureState.value !== State.BEGAN &&
|
|
276
|
+
(state.value === ReorderableListState.DRAGGING ||
|
|
277
|
+
state.value === ReorderableListState.AUTO_SCROLL)
|
|
278
|
+
) {
|
|
279
|
+
state.value = ReorderableListState.RELEASING;
|
|
280
|
+
releasedIndex.value = draggedIndex.value;
|
|
281
|
+
|
|
282
|
+
// enable back scroll on releasing
|
|
283
|
+
runOnJS(setScrollEnabled)(true);
|
|
284
|
+
|
|
285
|
+
// they are actually swapped on drag translation
|
|
286
|
+
const currentItemOffset = itemOffset.value[draggedIndex.value];
|
|
287
|
+
const currentItemHeight = itemHeight.value[draggedIndex.value];
|
|
288
|
+
const draggedItemOffset = itemOffset.value[currentIndex.value];
|
|
289
|
+
const draggedItemHeight = itemHeight.value[currentIndex.value];
|
|
290
|
+
|
|
291
|
+
const newTopPosition =
|
|
292
|
+
currentIndex.value > draggedIndex.value
|
|
293
|
+
? draggedItemOffset - currentItemOffset
|
|
294
|
+
: draggedItemOffset -
|
|
295
|
+
currentItemOffset +
|
|
296
|
+
(draggedItemHeight - currentItemHeight);
|
|
297
|
+
|
|
298
|
+
if (dragY.value !== newTopPosition) {
|
|
299
|
+
// animate dragged item to its new position on release
|
|
300
|
+
dragY.value = withTiming(
|
|
301
|
+
newTopPosition,
|
|
302
|
+
{
|
|
303
|
+
duration: duration.value,
|
|
304
|
+
easing: Easing.out(Easing.ease),
|
|
305
|
+
},
|
|
306
|
+
() => {
|
|
307
|
+
runOnJS(reorder)(draggedIndex.value, currentIndex.value);
|
|
308
|
+
},
|
|
309
|
+
);
|
|
310
|
+
} else {
|
|
311
|
+
// user might drag and release the item without moving it so,
|
|
312
|
+
// since the animation end callback is not executed in that case
|
|
313
|
+
// we need to reset values as the reorder function would do
|
|
314
|
+
resetSharedValues();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
useAnimatedReaction(
|
|
321
|
+
() => currentY.value,
|
|
322
|
+
y => {
|
|
323
|
+
if (
|
|
324
|
+
state.value === ReorderableListState.DRAGGING ||
|
|
325
|
+
state.value === ReorderableListState.AUTO_SCROLL
|
|
326
|
+
) {
|
|
327
|
+
setCurrentIndex(y);
|
|
328
|
+
|
|
329
|
+
if (y <= topAutoscrollArea.value || y >= bottomAutoscrollArea.value) {
|
|
330
|
+
if (state.value !== ReorderableListState.AUTO_SCROLL) {
|
|
331
|
+
// trigger autoscroll
|
|
332
|
+
lastAutoscrollTrigger.value = autoscrollTrigger.value;
|
|
333
|
+
autoscrollTrigger.value *= -1;
|
|
334
|
+
}
|
|
335
|
+
state.value = ReorderableListState.AUTO_SCROLL;
|
|
336
|
+
} else {
|
|
337
|
+
state.value = ReorderableListState.DRAGGING;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
useAnimatedReaction(
|
|
344
|
+
() => autoscrollTrigger.value,
|
|
345
|
+
() => {
|
|
346
|
+
if (
|
|
347
|
+
autoscrollTrigger.value !== lastAutoscrollTrigger.value &&
|
|
348
|
+
state.value === ReorderableListState.AUTO_SCROLL
|
|
349
|
+
) {
|
|
350
|
+
const autoscrollIncrement =
|
|
351
|
+
(currentY.value <= topAutoscrollArea.value
|
|
352
|
+
? -AUTOSCROLL_INCREMENT
|
|
353
|
+
: AUTOSCROLL_INCREMENT) * autoscrollSpeedScale;
|
|
354
|
+
|
|
355
|
+
if (autoscrollIncrement !== 0) {
|
|
356
|
+
scrollTo(
|
|
357
|
+
flatList as unknown as AnimatedRef<Animated.ScrollView>,
|
|
358
|
+
0,
|
|
359
|
+
currentScrollOffsetY.value + autoscrollIncrement,
|
|
360
|
+
true,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// when autoscrolling user may not be moving his finger so we need
|
|
365
|
+
// to update the current position of the dragged item here
|
|
366
|
+
setCurrentIndex(currentY.value);
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const handleScroll = useAnimatedScrollHandler(e => {
|
|
372
|
+
currentScrollOffsetY.value = e.contentOffset.y;
|
|
373
|
+
|
|
374
|
+
// checking if the list is not scrollable instead of the scrolling state
|
|
375
|
+
// fixes a bug on iOS where the item is shifted after autoscrolling and then
|
|
376
|
+
// moving await from autoscroll area
|
|
377
|
+
if (!scrollEnabled.value) {
|
|
378
|
+
dragScrollTranslationY.value =
|
|
379
|
+
currentScrollOffsetY.value - dragInitialScrollOffsetY.value;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (state.value === ReorderableListState.AUTO_SCROLL) {
|
|
383
|
+
dragY.value = currentTranslationY.value + dragScrollTranslationY.value;
|
|
384
|
+
|
|
385
|
+
cancelAnimation(autoscrollTrigger);
|
|
386
|
+
|
|
387
|
+
lastAutoscrollTrigger.value = autoscrollTrigger.value;
|
|
388
|
+
autoscrollTrigger.value = withDelay(
|
|
389
|
+
autoscrollDelay,
|
|
390
|
+
withTiming(autoscrollTrigger.value * -1, {duration: 0}),
|
|
391
|
+
);
|
|
392
|
+
} else {
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (onScroll) {
|
|
396
|
+
onScroll(e);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const startDrag = useCallback(
|
|
401
|
+
(index: number) => {
|
|
402
|
+
'worklet';
|
|
403
|
+
|
|
404
|
+
// allow new drag when item is completely released
|
|
405
|
+
if (state.value === ReorderableListState.IDLE) {
|
|
406
|
+
draggedHeight.value = itemHeight.value[index];
|
|
407
|
+
draggedIndex.value = index;
|
|
408
|
+
previousIndex.value = -1;
|
|
409
|
+
currentIndex.value = index;
|
|
410
|
+
state.value = ReorderableListState.DRAGGING;
|
|
411
|
+
dragInitialScrollOffsetY.value = currentScrollOffsetY.value;
|
|
412
|
+
|
|
413
|
+
runOnJS(setScrollEnabled)(false);
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
[
|
|
417
|
+
setScrollEnabled,
|
|
418
|
+
currentIndex,
|
|
419
|
+
previousIndex,
|
|
420
|
+
draggedHeight,
|
|
421
|
+
draggedIndex,
|
|
422
|
+
state,
|
|
423
|
+
currentScrollOffsetY,
|
|
424
|
+
dragInitialScrollOffsetY,
|
|
425
|
+
itemHeight,
|
|
426
|
+
],
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const measureFlatList = useCallback(() => {
|
|
430
|
+
'worklet';
|
|
431
|
+
|
|
432
|
+
const measurement = measure(flatList);
|
|
433
|
+
if (measurement === null) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
containerPositionY.value = measurement.pageY;
|
|
438
|
+
}, [flatList, containerPositionY]);
|
|
439
|
+
|
|
440
|
+
const handleFlatListLayout = useCallback(
|
|
441
|
+
(e: LayoutChangeEvent) => {
|
|
442
|
+
runOnUI(measureFlatList)();
|
|
443
|
+
|
|
444
|
+
const threshold = Math.max(0, Math.min(autoscrollThreshold, 0.4));
|
|
445
|
+
const {height} = e.nativeEvent.layout;
|
|
446
|
+
|
|
447
|
+
topAutoscrollArea.value = height * threshold;
|
|
448
|
+
bottomAutoscrollArea.value = height * (1 - threshold);
|
|
449
|
+
|
|
450
|
+
if (onLayout) {
|
|
451
|
+
onLayout(e);
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
[
|
|
455
|
+
measureFlatList,
|
|
456
|
+
topAutoscrollArea,
|
|
457
|
+
bottomAutoscrollArea,
|
|
458
|
+
autoscrollThreshold,
|
|
459
|
+
onLayout,
|
|
460
|
+
],
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
const handleRef = (value: FlatList<T>) => {
|
|
464
|
+
flatList(value);
|
|
465
|
+
|
|
466
|
+
if (typeof ref === 'function') {
|
|
467
|
+
ref(value);
|
|
468
|
+
} else if (ref) {
|
|
469
|
+
ref.current = value;
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
gestureHandler,
|
|
475
|
+
handleScroll,
|
|
476
|
+
handleFlatListLayout,
|
|
477
|
+
handleRef,
|
|
478
|
+
startDrag,
|
|
479
|
+
listContextValue,
|
|
480
|
+
itemOffset,
|
|
481
|
+
itemHeight,
|
|
482
|
+
draggedIndex,
|
|
483
|
+
releasedIndex,
|
|
484
|
+
dragY,
|
|
485
|
+
duration,
|
|
486
|
+
};
|
|
487
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import React, {memo, useCallback, useMemo} from 'react';
|
|
2
|
+
import type {LayoutChangeEvent} from 'react-native';
|
|
3
|
+
|
|
4
|
+
import Animated, {
|
|
5
|
+
Easing,
|
|
6
|
+
SharedValue,
|
|
7
|
+
runOnUI,
|
|
8
|
+
useAnimatedReaction,
|
|
9
|
+
useAnimatedStyle,
|
|
10
|
+
useSharedValue,
|
|
11
|
+
withTiming,
|
|
12
|
+
} from 'react-native-reanimated';
|
|
13
|
+
|
|
14
|
+
import {ReorderableCellContext, ReorderableListContext} from '../contexts';
|
|
15
|
+
import {useContext} from '../hooks';
|
|
16
|
+
|
|
17
|
+
interface ReorderableListCellProps<T, U> {
|
|
18
|
+
index: number;
|
|
19
|
+
startDrag: (index: number) => void;
|
|
20
|
+
itemOffset: SharedValue<number[]>;
|
|
21
|
+
itemHeight: SharedValue<number[]>;
|
|
22
|
+
dragY: SharedValue<number>;
|
|
23
|
+
draggedIndex: SharedValue<number>;
|
|
24
|
+
releasedIndex: SharedValue<number>;
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
onLayout?: (e: LayoutChangeEvent) => void;
|
|
27
|
+
// animation duration as a shared value allows to avoid re-renders on value change
|
|
28
|
+
animationDuration: SharedValue<number>;
|
|
29
|
+
item: T;
|
|
30
|
+
extraData: U;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const ReorderableListCell = memo(
|
|
34
|
+
<T, U>({
|
|
35
|
+
index,
|
|
36
|
+
startDrag,
|
|
37
|
+
children,
|
|
38
|
+
onLayout,
|
|
39
|
+
itemOffset,
|
|
40
|
+
itemHeight,
|
|
41
|
+
dragY,
|
|
42
|
+
draggedIndex,
|
|
43
|
+
releasedIndex,
|
|
44
|
+
animationDuration,
|
|
45
|
+
}: ReorderableListCellProps<T, U>) => {
|
|
46
|
+
const dragHandler = useCallback(() => {
|
|
47
|
+
'worklet';
|
|
48
|
+
|
|
49
|
+
startDrag(index);
|
|
50
|
+
}, [startDrag, index]);
|
|
51
|
+
|
|
52
|
+
const contextValue = useMemo(
|
|
53
|
+
() => ({
|
|
54
|
+
index,
|
|
55
|
+
dragHandler,
|
|
56
|
+
draggedIndex,
|
|
57
|
+
releasedIndex,
|
|
58
|
+
}),
|
|
59
|
+
[index, dragHandler, draggedIndex, releasedIndex],
|
|
60
|
+
);
|
|
61
|
+
const {currentIndex, draggedHeight} = useContext(ReorderableListContext);
|
|
62
|
+
|
|
63
|
+
const itemZIndex = useSharedValue(0);
|
|
64
|
+
const itemPositionY = useSharedValue(0);
|
|
65
|
+
const itemDragY = useSharedValue(0);
|
|
66
|
+
const itemIndex = useSharedValue(index);
|
|
67
|
+
|
|
68
|
+
useAnimatedReaction(
|
|
69
|
+
() => dragY.value,
|
|
70
|
+
() => {
|
|
71
|
+
if (
|
|
72
|
+
itemIndex.value === draggedIndex.value &&
|
|
73
|
+
currentIndex.value >= 0 &&
|
|
74
|
+
draggedIndex.value >= 0
|
|
75
|
+
) {
|
|
76
|
+
itemDragY.value = dragY.value;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
useAnimatedReaction(
|
|
82
|
+
() => currentIndex.value,
|
|
83
|
+
() => {
|
|
84
|
+
if (
|
|
85
|
+
itemIndex.value !== draggedIndex.value &&
|
|
86
|
+
currentIndex.value >= 0 &&
|
|
87
|
+
draggedIndex.value >= 0
|
|
88
|
+
) {
|
|
89
|
+
const moveDown = currentIndex.value > draggedIndex.value;
|
|
90
|
+
const startMove = Math.min(draggedIndex.value, currentIndex.value);
|
|
91
|
+
const endMove = Math.max(draggedIndex.value, currentIndex.value);
|
|
92
|
+
let newValue = 0;
|
|
93
|
+
|
|
94
|
+
if (itemIndex.value >= startMove && itemIndex.value <= endMove) {
|
|
95
|
+
newValue = moveDown ? -draggedHeight.value : draggedHeight.value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (newValue !== itemPositionY.value) {
|
|
99
|
+
itemPositionY.value = withTiming(newValue, {
|
|
100
|
+
duration: animationDuration.value,
|
|
101
|
+
easing: Easing.out(Easing.ease),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
useAnimatedReaction(
|
|
109
|
+
() => draggedIndex.value === index,
|
|
110
|
+
newValue => {
|
|
111
|
+
itemZIndex.value = newValue ? 999 : 0;
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
116
|
+
zIndex: itemZIndex.value,
|
|
117
|
+
transform: [
|
|
118
|
+
{translateY: itemDragY.value},
|
|
119
|
+
{translateY: itemPositionY.value},
|
|
120
|
+
],
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
const handleLayout = (e: LayoutChangeEvent) => {
|
|
124
|
+
runOnUI((y: number, height: number) => {
|
|
125
|
+
itemOffset.value[index] = y;
|
|
126
|
+
itemHeight.value[index] = height;
|
|
127
|
+
})(e.nativeEvent.layout.y, e.nativeEvent.layout.height);
|
|
128
|
+
|
|
129
|
+
if (onLayout) {
|
|
130
|
+
onLayout(e);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<ReorderableCellContext.Provider value={contextValue}>
|
|
136
|
+
<Animated.View style={animatedStyle} onLayout={handleLayout}>
|
|
137
|
+
{children}
|
|
138
|
+
</Animated.View>
|
|
139
|
+
</ReorderableCellContext.Provider>
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
(prev, next) => prev.item === next.item && prev.extraData === next.extraData,
|
|
143
|
+
);
|