react-native-gesture-image-viewer 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/module/GestureImageViewer.js +25 -10
- package/lib/module/GestureImageViewer.js.map +1 -1
- package/lib/module/ImageViewerManager.js +78 -0
- package/lib/module/ImageViewerManager.js.map +1 -0
- package/lib/module/ImageViewerRegistry.js +26 -0
- package/lib/module/ImageViewerRegistry.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/useGestureImageViewer.js +138 -59
- package/lib/module/useGestureImageViewer.js.map +1 -1
- package/lib/module/useImageViewerController.js +28 -0
- package/lib/module/useImageViewerController.js.map +1 -0
- package/lib/typescript/src/GestureImageViewer.d.ts +1 -1
- package/lib/typescript/src/GestureImageViewer.d.ts.map +1 -1
- package/lib/typescript/src/ImageViewerManager.d.ts +28 -0
- package/lib/typescript/src/ImageViewerManager.d.ts.map +1 -0
- package/lib/typescript/src/ImageViewerRegistry.d.ts +10 -0
- package/lib/typescript/src/ImageViewerRegistry.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +5 -0
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useGestureImageViewer.d.ts +16 -7
- package/lib/typescript/src/useGestureImageViewer.d.ts.map +1 -1
- package/lib/typescript/src/useImageViewerController.d.ts +8 -0
- package/lib/typescript/src/useImageViewerController.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/GestureImageViewer.tsx +42 -14
- package/src/ImageViewerManager.ts +92 -0
- package/src/ImageViewerRegistry.ts +30 -0
- package/src/index.tsx +1 -0
- package/src/types.ts +5 -0
- package/src/useGestureImageViewer.ts +210 -62
- package/src/useImageViewerController.ts +34 -0
|
@@ -6,7 +6,18 @@ import {
|
|
|
6
6
|
useWindowDimensions,
|
|
7
7
|
} from 'react-native';
|
|
8
8
|
import { Gesture } from 'react-native-gesture-handler';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
Easing,
|
|
11
|
+
interpolate,
|
|
12
|
+
runOnJS,
|
|
13
|
+
useAnimatedReaction,
|
|
14
|
+
useAnimatedStyle,
|
|
15
|
+
useSharedValue,
|
|
16
|
+
withSpring,
|
|
17
|
+
withTiming,
|
|
18
|
+
} from 'react-native-reanimated';
|
|
19
|
+
import type { ImageViewerManagerState } from './ImageViewerManager';
|
|
20
|
+
import { registry } from './ImageViewerRegistry';
|
|
10
21
|
import type { GestureImageViewerProps } from './types';
|
|
11
22
|
|
|
12
23
|
type UseGestureImageViewerProps<T = any> = Omit<
|
|
@@ -27,30 +38,92 @@ export const useGestureImageViewer = <T = any>({
|
|
|
27
38
|
animateBackdrop = true,
|
|
28
39
|
enableDismissGesture = true,
|
|
29
40
|
enableSwipeGesture = true,
|
|
41
|
+
enableZoomGesture = true,
|
|
42
|
+
enableDoubleTapGesture = true,
|
|
43
|
+
enableZoomPanGesture = true,
|
|
44
|
+
maxZoomScale = 2,
|
|
45
|
+
id = 'default',
|
|
30
46
|
}: UseGestureImageViewerProps<T>) => {
|
|
31
|
-
const { width: screenWidth } = useWindowDimensions();
|
|
47
|
+
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
32
48
|
const width = customWidth || screenWidth;
|
|
33
49
|
|
|
34
|
-
const [
|
|
50
|
+
const [isZoomed, setIsZoomed] = useState(false);
|
|
51
|
+
|
|
52
|
+
const [managerState, setManagerState] = useState<ImageViewerManagerState>({
|
|
53
|
+
currentIndex: initialIndex,
|
|
54
|
+
dataLength: data?.length || 0,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const initialTranslateY = useSharedValue(0);
|
|
58
|
+
const initialTranslateX = useSharedValue(0);
|
|
59
|
+
const startScale = useSharedValue(1);
|
|
35
60
|
|
|
36
61
|
const translateY = useSharedValue(0);
|
|
62
|
+
const translateX = useSharedValue(0);
|
|
63
|
+
const scale = useSharedValue(1);
|
|
37
64
|
const backdropOpacity = useSharedValue(1);
|
|
38
65
|
|
|
39
66
|
const flatListRef = useRef<any>(null);
|
|
40
67
|
|
|
41
68
|
const dataLength = data?.length || 0;
|
|
42
69
|
|
|
43
|
-
|
|
70
|
+
useAnimatedReaction(
|
|
71
|
+
() => scale.value,
|
|
72
|
+
(currentScale) => {
|
|
73
|
+
runOnJS(setIsZoomed)(currentScale > 1);
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const manager = registry.getManager(id);
|
|
79
|
+
|
|
80
|
+
if (!manager) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setManagerState(manager.getState());
|
|
85
|
+
|
|
86
|
+
return manager.subscribe(setManagerState);
|
|
87
|
+
}, [id]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const manager = registry.getManager(id);
|
|
91
|
+
|
|
92
|
+
if (!manager) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
manager.setDataLength(dataLength);
|
|
97
|
+
manager.setEnableSwipeGesture(enableSwipeGesture);
|
|
98
|
+
manager.setCurrentIndex(initialIndex);
|
|
99
|
+
manager.notifyStateChange();
|
|
100
|
+
}, [dataLength, enableSwipeGesture, initialIndex, id]);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const manager = registry.getManager(id);
|
|
104
|
+
|
|
105
|
+
if (!manager || !flatListRef.current) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
manager.setFlatListRef(flatListRef.current);
|
|
110
|
+
}, [id]);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
onIndexChange?.(managerState.currentIndex);
|
|
114
|
+
}, [managerState.currentIndex, onIndexChange]);
|
|
115
|
+
|
|
44
116
|
useEffect(() => {
|
|
45
|
-
setCurrentIndex(initialIndex);
|
|
46
117
|
translateY.value = 0;
|
|
118
|
+
translateX.value = 0;
|
|
119
|
+
scale.value = 1;
|
|
47
120
|
backdropOpacity.value = 1;
|
|
121
|
+
startScale.value = 1;
|
|
48
122
|
|
|
49
123
|
if (initialIndex <= 0 || !flatListRef.current) {
|
|
50
124
|
return;
|
|
51
125
|
}
|
|
52
126
|
|
|
53
|
-
// FlatList 초기 위치 설정
|
|
54
127
|
const runAfterInteractions = InteractionManager.runAfterInteractions(() => {
|
|
55
128
|
flatListRef.current?.scrollToIndex({
|
|
56
129
|
index: initialIndex,
|
|
@@ -61,14 +134,8 @@ export const useGestureImageViewer = <T = any>({
|
|
|
61
134
|
return () => {
|
|
62
135
|
runAfterInteractions?.cancel();
|
|
63
136
|
};
|
|
64
|
-
}, [initialIndex, translateY, backdropOpacity]);
|
|
65
|
-
|
|
66
|
-
// 인덱스 변경 알림
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
onIndexChange?.(currentIndex);
|
|
69
|
-
}, [currentIndex, onIndexChange]);
|
|
137
|
+
}, [initialIndex, translateY, backdropOpacity, translateX, scale, startScale]);
|
|
70
138
|
|
|
71
|
-
// FlatList 스크롤 핸들러
|
|
72
139
|
const onMomentumScrollEnd = useCallback(
|
|
73
140
|
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
74
141
|
if (!enableSwipeGesture) {
|
|
@@ -78,52 +145,49 @@ export const useGestureImageViewer = <T = any>({
|
|
|
78
145
|
const contentOffset = event.nativeEvent.contentOffset;
|
|
79
146
|
const newIndex = Math.round(contentOffset.x / width);
|
|
80
147
|
|
|
81
|
-
if (newIndex !== currentIndex && newIndex >= 0 && newIndex < dataLength) {
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
[currentIndex, dataLength, width, enableSwipeGesture],
|
|
86
|
-
);
|
|
148
|
+
if (newIndex !== managerState.currentIndex && newIndex >= 0 && newIndex < dataLength) {
|
|
149
|
+
const manager = registry.getManager(id);
|
|
87
150
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
151
|
+
if (manager) {
|
|
152
|
+
manager.setCurrentIndex(newIndex);
|
|
153
|
+
}
|
|
93
154
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
155
|
+
translateX.value = withTiming(0);
|
|
156
|
+
translateY.value = withTiming(0);
|
|
157
|
+
initialTranslateX.value = withTiming(0);
|
|
158
|
+
initialTranslateY.value = withTiming(0);
|
|
159
|
+
startScale.value = withTiming(1);
|
|
160
|
+
scale.value = withTiming(1);
|
|
161
|
+
}
|
|
99
162
|
},
|
|
100
|
-
[
|
|
163
|
+
[
|
|
164
|
+
id,
|
|
165
|
+
managerState.currentIndex,
|
|
166
|
+
dataLength,
|
|
167
|
+
width,
|
|
168
|
+
enableSwipeGesture,
|
|
169
|
+
translateX,
|
|
170
|
+
translateY,
|
|
171
|
+
scale,
|
|
172
|
+
initialTranslateX,
|
|
173
|
+
initialTranslateY,
|
|
174
|
+
startScale,
|
|
175
|
+
],
|
|
101
176
|
);
|
|
102
177
|
|
|
103
|
-
const
|
|
104
|
-
if (currentIndex > 0) {
|
|
105
|
-
goToIndex(currentIndex - 1);
|
|
106
|
-
}
|
|
107
|
-
}, [currentIndex, goToIndex]);
|
|
108
|
-
|
|
109
|
-
const goToNext = useCallback(() => {
|
|
110
|
-
if (currentIndex < dataLength - 1) {
|
|
111
|
-
goToIndex(currentIndex + 1);
|
|
112
|
-
}
|
|
113
|
-
}, [currentIndex, dataLength, goToIndex]);
|
|
114
|
-
|
|
115
|
-
const panGesture = useMemo(() => {
|
|
178
|
+
const dismissGesture = useMemo(() => {
|
|
116
179
|
return Gesture.Pan()
|
|
117
180
|
.minDistance(10)
|
|
181
|
+
.averageTouches(true)
|
|
118
182
|
.activeOffsetY([-10, 10])
|
|
119
183
|
.failOffsetX([-10, 10])
|
|
184
|
+
.enabled(!isZoomed)
|
|
120
185
|
.onUpdate((event) => {
|
|
121
|
-
'worklet';
|
|
122
|
-
|
|
123
186
|
translateY.value = event.translationY / resistance;
|
|
124
187
|
})
|
|
125
188
|
.onEnd((event) => {
|
|
126
189
|
'worklet';
|
|
190
|
+
|
|
127
191
|
if (event.translationY > dismissThreshold && enableDismissGesture && onDismiss) {
|
|
128
192
|
runOnJS(onDismiss)();
|
|
129
193
|
return;
|
|
@@ -134,44 +198,128 @@ export const useGestureImageViewer = <T = any>({
|
|
|
134
198
|
stiffness: 150,
|
|
135
199
|
});
|
|
136
200
|
});
|
|
137
|
-
}, [translateY, dismissThreshold, enableDismissGesture, onDismiss, resistance]);
|
|
201
|
+
}, [translateY, dismissThreshold, enableDismissGesture, onDismiss, resistance, isZoomed]);
|
|
202
|
+
|
|
203
|
+
const zoomPinchGesture = useMemo(() => {
|
|
204
|
+
return Gesture.Pinch()
|
|
205
|
+
.enabled(enableZoomGesture)
|
|
206
|
+
.onBegin(() => {
|
|
207
|
+
startScale.value = scale.value;
|
|
208
|
+
})
|
|
209
|
+
.onUpdate((event) => {
|
|
210
|
+
scale.value = startScale.value * event.scale;
|
|
211
|
+
})
|
|
212
|
+
.onEnd(() => {
|
|
213
|
+
if (scale.value > maxZoomScale) {
|
|
214
|
+
scale.value = withTiming(maxZoomScale, {
|
|
215
|
+
duration: 300,
|
|
216
|
+
easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (scale.value < 1) {
|
|
222
|
+
scale.value = withTiming(1, {
|
|
223
|
+
duration: 300,
|
|
224
|
+
easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
|
|
225
|
+
});
|
|
226
|
+
translateX.value = withTiming(0);
|
|
227
|
+
translateY.value = withTiming(0);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}, [scale, enableZoomGesture, maxZoomScale, translateX, translateY, startScale]);
|
|
231
|
+
|
|
232
|
+
const zoomPanGesture = useMemo(() => {
|
|
233
|
+
return Gesture.Pan()
|
|
234
|
+
.enabled(enableZoomPanGesture && isZoomed)
|
|
235
|
+
.onBegin(() => {
|
|
236
|
+
initialTranslateX.value = translateX.value;
|
|
237
|
+
initialTranslateY.value = translateY.value;
|
|
238
|
+
})
|
|
239
|
+
.onUpdate((event) => {
|
|
240
|
+
'worklet';
|
|
241
|
+
|
|
242
|
+
if (scale.value > 1) {
|
|
243
|
+
const maxTranslateX = (width * scale.value - width) / 2;
|
|
244
|
+
const maxTranslateY = (screenHeight * scale.value - screenHeight) / 2;
|
|
245
|
+
|
|
246
|
+
translateX.value = Math.max(
|
|
247
|
+
-maxTranslateX,
|
|
248
|
+
Math.min(maxTranslateX, initialTranslateX.value + event.translationX),
|
|
249
|
+
);
|
|
250
|
+
translateY.value = Math.max(
|
|
251
|
+
-maxTranslateY,
|
|
252
|
+
Math.min(maxTranslateY, initialTranslateY.value + event.translationY),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}, [
|
|
257
|
+
translateX,
|
|
258
|
+
translateY,
|
|
259
|
+
enableZoomPanGesture,
|
|
260
|
+
isZoomed,
|
|
261
|
+
scale,
|
|
262
|
+
initialTranslateX,
|
|
263
|
+
initialTranslateY,
|
|
264
|
+
width,
|
|
265
|
+
screenHeight,
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
const doubleTapGesture = useMemo(() => {
|
|
269
|
+
return Gesture.Tap()
|
|
270
|
+
.enabled(enableDoubleTapGesture)
|
|
271
|
+
.numberOfTaps(2)
|
|
272
|
+
.onEnd(() => {
|
|
273
|
+
const nextScale = scale.value > 1 ? 1 : maxZoomScale;
|
|
274
|
+
|
|
275
|
+
translateX.value = withTiming(0, {
|
|
276
|
+
duration: 300,
|
|
277
|
+
easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
|
|
278
|
+
});
|
|
279
|
+
translateY.value = withTiming(0, {
|
|
280
|
+
duration: 300,
|
|
281
|
+
easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
scale.value = withTiming(nextScale, {
|
|
285
|
+
duration: 300,
|
|
286
|
+
easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}, [scale, enableDoubleTapGesture, maxZoomScale, translateX, translateY]);
|
|
290
|
+
|
|
291
|
+
const zoomGesture = useMemo(() => {
|
|
292
|
+
return Gesture.Simultaneous(zoomPinchGesture, zoomPanGesture, doubleTapGesture);
|
|
293
|
+
}, [zoomPinchGesture, zoomPanGesture, doubleTapGesture]);
|
|
138
294
|
|
|
139
|
-
// 애니메이션 스타일
|
|
140
295
|
const animatedStyle = useAnimatedStyle(() => {
|
|
141
296
|
return {
|
|
142
|
-
transform: [{ translateY: translateY.value }],
|
|
297
|
+
transform: [{ translateY: translateY.value }, { translateX: translateX.value }, { scale: scale.value }],
|
|
143
298
|
};
|
|
144
|
-
}
|
|
299
|
+
});
|
|
145
300
|
|
|
146
301
|
const backdropStyle = useAnimatedStyle(() => {
|
|
147
|
-
if (!animateBackdrop) {
|
|
302
|
+
if (!animateBackdrop || scale.value > 1) {
|
|
148
303
|
return { opacity: 1 };
|
|
149
304
|
}
|
|
150
305
|
|
|
151
306
|
const opacity = interpolate(translateY.value, [0, 200], [1, 0], 'clamp');
|
|
152
307
|
|
|
153
308
|
return { opacity };
|
|
154
|
-
}, [animateBackdrop
|
|
309
|
+
}, [animateBackdrop]);
|
|
155
310
|
|
|
156
311
|
return {
|
|
157
|
-
|
|
158
|
-
currentIndex,
|
|
312
|
+
currentIndex: managerState.currentIndex,
|
|
159
313
|
dataLength,
|
|
160
314
|
translateY,
|
|
161
315
|
flatListRef,
|
|
316
|
+
isZoomed,
|
|
162
317
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
goToNext,
|
|
166
|
-
goToIndex,
|
|
167
|
-
|
|
168
|
-
// Gesture
|
|
169
|
-
panGesture,
|
|
318
|
+
dismissGesture,
|
|
319
|
+
zoomGesture,
|
|
170
320
|
|
|
171
|
-
// FlatList handlers
|
|
172
321
|
onMomentumScrollEnd,
|
|
173
322
|
|
|
174
|
-
// Animated styles
|
|
175
323
|
animatedStyle,
|
|
176
324
|
backdropStyle,
|
|
177
325
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import type { ImageViewerManagerState } from './ImageViewerManager';
|
|
3
|
+
import { registry } from './ImageViewerRegistry';
|
|
4
|
+
|
|
5
|
+
export const useImageViewerController = (id = 'default') => {
|
|
6
|
+
const [state, setState] = useState<ImageViewerManagerState>({
|
|
7
|
+
currentIndex: 0,
|
|
8
|
+
dataLength: 0,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const manager = registry.createManager(id);
|
|
13
|
+
|
|
14
|
+
if (!manager) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setState(manager.getState());
|
|
19
|
+
|
|
20
|
+
return manager.subscribe(setState);
|
|
21
|
+
}, [id]);
|
|
22
|
+
|
|
23
|
+
const manager = registry.getManager(id);
|
|
24
|
+
|
|
25
|
+
const noopFunction = useMemo(() => () => {}, []);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
goToIndex: manager?.goToIndex || noopFunction,
|
|
29
|
+
goToPrevious: manager?.goToPrevious || noopFunction,
|
|
30
|
+
goToNext: manager?.goToNext || noopFunction,
|
|
31
|
+
currentIndex: state.currentIndex,
|
|
32
|
+
totalCount: state.dataLength,
|
|
33
|
+
};
|
|
34
|
+
};
|