react-native-gesture-image-viewer 2.0.0-beta.3 → 2.0.0-beta.5
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 +43 -19
- package/lib/module/GestureTrigger.js +82 -0
- package/lib/module/GestureTrigger.js.map +1 -0
- package/lib/module/GestureViewer.js +14 -8
- package/lib/module/GestureViewer.js.map +1 -1
- package/lib/module/GestureViewerManager.js +5 -5
- package/lib/module/GestureViewerManager.js.map +1 -1
- package/lib/module/GestureViewerRegistry.js +11 -0
- package/lib/module/GestureViewerRegistry.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/useGestureViewer.js +140 -71
- package/lib/module/useGestureViewer.js.map +1 -1
- package/lib/module/useGestureViewerController.js +17 -66
- package/lib/module/useGestureViewerController.js.map +1 -1
- package/lib/module/useGestureViewerState.js +78 -0
- package/lib/module/useGestureViewerState.js.map +1 -0
- package/lib/typescript/src/GestureTrigger.d.ts +55 -0
- package/lib/typescript/src/GestureTrigger.d.ts.map +1 -0
- package/lib/typescript/src/GestureViewer.d.ts +2 -2
- package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
- package/lib/typescript/src/GestureViewerManager.d.ts +5 -8
- package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
- package/lib/typescript/src/GestureViewerRegistry.d.ts +5 -0
- package/lib/typescript/src/GestureViewerRegistry.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +116 -58
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useGestureViewer.d.ts +9 -8
- package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
- package/lib/typescript/src/useGestureViewerController.d.ts +10 -21
- package/lib/typescript/src/useGestureViewerController.d.ts.map +1 -1
- package/lib/typescript/src/useGestureViewerState.d.ts +35 -0
- package/lib/typescript/src/useGestureViewerState.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/GestureTrigger.tsx +86 -0
- package/src/GestureViewer.tsx +15 -9
- package/src/GestureViewerManager.ts +9 -9
- package/src/GestureViewerRegistry.ts +16 -0
- package/src/index.tsx +4 -1
- package/src/types.ts +116 -60
- package/src/useGestureViewer.ts +180 -77
- package/src/useGestureViewerController.ts +19 -76
- package/src/useGestureViewerState.ts +85 -0
package/src/useGestureViewer.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { runOnJS } from 'react-native-worklets';
|
|
19
19
|
import type GestureViewerManager from './GestureViewerManager';
|
|
20
20
|
import { registry } from './GestureViewerRegistry';
|
|
21
|
-
import type { GestureViewerProps } from './types';
|
|
21
|
+
import type { GestureViewerProps, TriggerRect } from './types';
|
|
22
22
|
import { createBoundsConstraint, createScrollAction, getLoopAdjustedIndex } from './utils';
|
|
23
23
|
|
|
24
24
|
type UseGestureViewerProps<T = any> = Omit<
|
|
@@ -29,36 +29,32 @@ type UseGestureViewerProps<T = any> = Omit<
|
|
|
29
29
|
export const useGestureViewer = <T = any>({
|
|
30
30
|
data,
|
|
31
31
|
initialIndex = 0,
|
|
32
|
-
onIndexChange,
|
|
33
32
|
onDismiss,
|
|
34
33
|
width: customWidth,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
enableDismissGesture = true,
|
|
41
|
-
enableSwipeGesture = true,
|
|
42
|
-
enableZoomGesture = true,
|
|
43
|
-
enableDoubleTapGesture = true,
|
|
44
|
-
enableZoomPanGesture = true,
|
|
34
|
+
dismiss,
|
|
35
|
+
enableDoubleTapZoom = true,
|
|
36
|
+
enablePinchZoom = true,
|
|
37
|
+
enableHorizontalSwipe = true,
|
|
38
|
+
enablePanWhenZoomed = true,
|
|
45
39
|
enableLoop = false,
|
|
46
40
|
maxZoomScale = 2,
|
|
47
41
|
itemSpacing = 0,
|
|
48
|
-
|
|
42
|
+
enableSnapMode = false,
|
|
49
43
|
id = 'default',
|
|
44
|
+
onDismissStart,
|
|
45
|
+
triggerAnimation,
|
|
50
46
|
}: UseGestureViewerProps<T>) => {
|
|
51
47
|
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
52
|
-
const width =
|
|
48
|
+
const width = enableSnapMode ? customWidth || screenWidth : screenWidth;
|
|
53
49
|
|
|
54
50
|
const [isZoomed, setIsZoomed] = useState(false);
|
|
55
51
|
const [isRotated, setIsRotated] = useState(false);
|
|
52
|
+
const [shouldStartTriggerAnimation, setShouldStartTriggerAnimation] = useState(false);
|
|
56
53
|
const [manager, setManager] = useState<GestureViewerManager | null>(null);
|
|
57
54
|
|
|
58
55
|
const listRef = useRef<any>(null);
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const lastEmittedIndexRef = useRef<number>(null);
|
|
56
|
+
const triggerRectRef = useRef<TriggerRect | null>(null);
|
|
57
|
+
const onAnimationCompleteRef = useRef(triggerAnimation?.onAnimationComplete);
|
|
62
58
|
|
|
63
59
|
const initialTranslateY = useSharedValue(0);
|
|
64
60
|
const initialTranslateX = useSharedValue(0);
|
|
@@ -70,8 +66,31 @@ export const useGestureViewer = <T = any>({
|
|
|
70
66
|
const backdropOpacity = useSharedValue(1);
|
|
71
67
|
const rotation = useSharedValue(0);
|
|
72
68
|
|
|
69
|
+
const triggerScale = useSharedValue(1);
|
|
70
|
+
const triggerTranslateX = useSharedValue(0);
|
|
71
|
+
const triggerTranslateY = useSharedValue(0);
|
|
72
|
+
const triggerOpacity = useSharedValue(1);
|
|
73
|
+
|
|
73
74
|
const dataLength = data?.length || 0;
|
|
74
75
|
|
|
76
|
+
const animationConfig = useMemo(
|
|
77
|
+
() => ({
|
|
78
|
+
duration: triggerAnimation?.duration ?? 300,
|
|
79
|
+
easing: triggerAnimation?.easing ?? Easing.bezier(0.25, 0.1, 0.25, 1.0),
|
|
80
|
+
reduceMotion: triggerAnimation?.reduceMotion,
|
|
81
|
+
}),
|
|
82
|
+
[triggerAnimation?.duration, triggerAnimation?.easing, triggerAnimation?.reduceMotion],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const dismissOptions = useMemo(() => {
|
|
86
|
+
return {
|
|
87
|
+
enabled: dismiss?.enabled ?? true,
|
|
88
|
+
threshold: dismiss?.threshold ?? 80,
|
|
89
|
+
resistance: dismiss?.resistance ?? 2,
|
|
90
|
+
fadeBackdrop: dismiss?.fadeBackdrop ?? true,
|
|
91
|
+
};
|
|
92
|
+
}, [dismiss?.enabled, dismiss?.threshold, dismiss?.resistance, dismiss?.fadeBackdrop]);
|
|
93
|
+
|
|
75
94
|
const adjustedInitialIndex = useMemo(() => {
|
|
76
95
|
if (enableLoop && data.length > 1) {
|
|
77
96
|
return initialIndex + 1;
|
|
@@ -96,22 +115,30 @@ export const useGestureViewer = <T = any>({
|
|
|
96
115
|
|
|
97
116
|
const emitZoomChange = useCallback(
|
|
98
117
|
(currentScale: number, prevScale: number | null) => {
|
|
99
|
-
manager
|
|
118
|
+
if (manager) {
|
|
119
|
+
manager.emitZoomChange(currentScale, prevScale);
|
|
120
|
+
}
|
|
100
121
|
},
|
|
101
122
|
[manager],
|
|
102
123
|
);
|
|
103
124
|
|
|
104
125
|
const emitRotationChange = useCallback(
|
|
105
126
|
(currentRotation: number, prevRotation: number | null) => {
|
|
106
|
-
manager
|
|
127
|
+
if (manager) {
|
|
128
|
+
manager.emitRotationChange(currentRotation, prevRotation);
|
|
129
|
+
}
|
|
107
130
|
},
|
|
108
131
|
[manager],
|
|
109
132
|
);
|
|
110
133
|
|
|
134
|
+
const onAnimationComplete = useCallback(() => {
|
|
135
|
+
onAnimationCompleteRef.current?.();
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
111
138
|
useAnimatedReaction(
|
|
112
139
|
() => scale.value,
|
|
113
140
|
(currentScale, previousScale) => {
|
|
114
|
-
if (
|
|
141
|
+
if (currentScale !== previousScale) {
|
|
115
142
|
runOnJS(emitZoomChange)(currentScale, previousScale);
|
|
116
143
|
}
|
|
117
144
|
|
|
@@ -122,7 +149,7 @@ export const useGestureViewer = <T = any>({
|
|
|
122
149
|
useAnimatedReaction(
|
|
123
150
|
() => rotation.value,
|
|
124
151
|
(currentRotation, previousRotation) => {
|
|
125
|
-
if (
|
|
152
|
+
if (currentRotation !== previousRotation) {
|
|
126
153
|
runOnJS(emitRotationChange)(currentRotation, previousRotation);
|
|
127
154
|
}
|
|
128
155
|
|
|
@@ -131,41 +158,9 @@ export const useGestureViewer = <T = any>({
|
|
|
131
158
|
);
|
|
132
159
|
|
|
133
160
|
useEffect(() => {
|
|
134
|
-
|
|
135
|
-
});
|
|
161
|
+
const unsubscribe = registry.subscribeToManager(id, setManager);
|
|
136
162
|
|
|
137
|
-
|
|
138
|
-
return () => {
|
|
139
|
-
onIndexChangeRef.current = null;
|
|
140
|
-
};
|
|
141
|
-
}, []);
|
|
142
|
-
|
|
143
|
-
useEffect(() => {
|
|
144
|
-
const handleManagerChange = (manager: GestureViewerManager | null) => {
|
|
145
|
-
unsubscribeRef.current?.();
|
|
146
|
-
unsubscribeRef.current = null;
|
|
147
|
-
|
|
148
|
-
setManager(manager);
|
|
149
|
-
|
|
150
|
-
lastEmittedIndexRef.current = null;
|
|
151
|
-
|
|
152
|
-
if (manager) {
|
|
153
|
-
unsubscribeRef.current = manager.subscribe((state) => {
|
|
154
|
-
if (lastEmittedIndexRef.current !== state.currentIndex) {
|
|
155
|
-
lastEmittedIndexRef.current = state.currentIndex;
|
|
156
|
-
onIndexChangeRef.current?.(state.currentIndex);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const unsubscribeFromRegistry = registry.subscribeToManager(id, handleManagerChange);
|
|
164
|
-
|
|
165
|
-
return () => {
|
|
166
|
-
unsubscribeFromRegistry();
|
|
167
|
-
unsubscribeRef.current?.();
|
|
168
|
-
};
|
|
163
|
+
return unsubscribe;
|
|
169
164
|
}, [id]);
|
|
170
165
|
|
|
171
166
|
useEffect(() => {
|
|
@@ -174,7 +169,7 @@ export const useGestureViewer = <T = any>({
|
|
|
174
169
|
}
|
|
175
170
|
|
|
176
171
|
manager.setDataLength(dataLength);
|
|
177
|
-
manager.
|
|
172
|
+
manager.setEnableHorizontalSwipe(enableHorizontalSwipe);
|
|
178
173
|
manager.setCurrentIndex(initialIndex);
|
|
179
174
|
manager.setWidth(width + itemSpacing);
|
|
180
175
|
manager.setHeight(screenHeight);
|
|
@@ -184,7 +179,7 @@ export const useGestureViewer = <T = any>({
|
|
|
184
179
|
manager.notifyStateChange();
|
|
185
180
|
}, [
|
|
186
181
|
dataLength,
|
|
187
|
-
|
|
182
|
+
enableHorizontalSwipe,
|
|
188
183
|
initialIndex,
|
|
189
184
|
manager,
|
|
190
185
|
width,
|
|
@@ -227,9 +222,108 @@ export const useGestureViewer = <T = any>({
|
|
|
227
222
|
};
|
|
228
223
|
}, [adjustedInitialIndex, translateY, backdropOpacity, translateX, scale, startScale, rotation, scrollTo]);
|
|
229
224
|
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
onAnimationCompleteRef.current = triggerAnimation?.onAnimationComplete;
|
|
227
|
+
}, [triggerAnimation?.onAnimationComplete]);
|
|
228
|
+
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (shouldStartTriggerAnimation && triggerRectRef.current) {
|
|
231
|
+
const startX = triggerRectRef.current.x + triggerRectRef.current.width / 2 - screenWidth / 2;
|
|
232
|
+
const startY = triggerRectRef.current.y + triggerRectRef.current.height / 2 - screenHeight / 2;
|
|
233
|
+
const initialScaleFromTrigger = Math.min(
|
|
234
|
+
triggerRectRef.current.width / screenWidth,
|
|
235
|
+
triggerRectRef.current.height / screenHeight,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
triggerScale.value = initialScaleFromTrigger;
|
|
239
|
+
triggerTranslateX.value = startX;
|
|
240
|
+
triggerTranslateY.value = startY;
|
|
241
|
+
triggerOpacity.value = 0;
|
|
242
|
+
|
|
243
|
+
triggerScale.value = withTiming(1, animationConfig, (finished) => {
|
|
244
|
+
if (finished) {
|
|
245
|
+
runOnJS(onAnimationComplete)();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
triggerTranslateX.value = withTiming(0, animationConfig);
|
|
249
|
+
triggerTranslateY.value = withTiming(0, animationConfig);
|
|
250
|
+
triggerOpacity.value = withTiming(1, {
|
|
251
|
+
duration: animationConfig.duration / 2,
|
|
252
|
+
easing: animationConfig.easing,
|
|
253
|
+
reduceMotion: animationConfig.reduceMotion,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
setShouldStartTriggerAnimation(false);
|
|
257
|
+
}
|
|
258
|
+
}, [
|
|
259
|
+
shouldStartTriggerAnimation,
|
|
260
|
+
animationConfig,
|
|
261
|
+
screenWidth,
|
|
262
|
+
screenHeight,
|
|
263
|
+
triggerOpacity,
|
|
264
|
+
triggerScale,
|
|
265
|
+
triggerTranslateX,
|
|
266
|
+
triggerTranslateY,
|
|
267
|
+
onAnimationComplete,
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
const node = registry.getTriggerNode(id);
|
|
272
|
+
|
|
273
|
+
if (node && typeof node.measure === 'function') {
|
|
274
|
+
node.measure((_x, _y, width, height, pageX, pageY) => {
|
|
275
|
+
triggerRectRef.current = { x: pageX, y: pageY, width, height };
|
|
276
|
+
triggerOpacity.value = 0;
|
|
277
|
+
setShouldStartTriggerAnimation(true);
|
|
278
|
+
registry.clearTriggerNode(id);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return () => {
|
|
283
|
+
triggerRectRef.current = null;
|
|
284
|
+
};
|
|
285
|
+
}, [id, triggerOpacity]);
|
|
286
|
+
|
|
287
|
+
const handleDismiss = useCallback(() => {
|
|
288
|
+
onDismissStart?.();
|
|
289
|
+
|
|
290
|
+
if (triggerRectRef.current) {
|
|
291
|
+
const endX = triggerRectRef.current.x + triggerRectRef.current.width / 2 - screenWidth / 2;
|
|
292
|
+
const endY = triggerRectRef.current.y + triggerRectRef.current.height / 2 - screenHeight / 2;
|
|
293
|
+
const endScale = Math.min(
|
|
294
|
+
triggerRectRef.current.width / screenWidth,
|
|
295
|
+
triggerRectRef.current.height / screenHeight,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
triggerScale.value = withTiming(endScale, animationConfig);
|
|
299
|
+
triggerTranslateX.value = withTiming(endX, animationConfig);
|
|
300
|
+
triggerTranslateY.value = withTiming(endY, animationConfig);
|
|
301
|
+
triggerOpacity.value = withTiming(0, animationConfig, (finished) => {
|
|
302
|
+
if (finished && onDismiss) {
|
|
303
|
+
runOnJS(onDismiss)();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (onDismiss) {
|
|
310
|
+
runOnJS(onDismiss)();
|
|
311
|
+
}
|
|
312
|
+
}, [
|
|
313
|
+
animationConfig,
|
|
314
|
+
onDismiss,
|
|
315
|
+
onDismissStart,
|
|
316
|
+
screenWidth,
|
|
317
|
+
screenHeight,
|
|
318
|
+
triggerTranslateX,
|
|
319
|
+
triggerScale,
|
|
320
|
+
triggerTranslateY,
|
|
321
|
+
triggerOpacity,
|
|
322
|
+
]);
|
|
323
|
+
|
|
230
324
|
const onMomentumScrollEnd = useCallback(
|
|
231
325
|
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
232
|
-
if (!
|
|
326
|
+
if (!enableHorizontalSwipe) {
|
|
233
327
|
return;
|
|
234
328
|
}
|
|
235
329
|
|
|
@@ -248,7 +342,7 @@ export const useGestureViewer = <T = any>({
|
|
|
248
342
|
scrollTo(jumpToIndex, false);
|
|
249
343
|
}
|
|
250
344
|
|
|
251
|
-
const currentIndex = manager?.getState()
|
|
345
|
+
const currentIndex = manager?.getState()?.currentIndex;
|
|
252
346
|
|
|
253
347
|
if (realIndex !== currentIndex && realIndex >= 0 && realIndex < dataLength) {
|
|
254
348
|
if (manager) {
|
|
@@ -271,7 +365,7 @@ export const useGestureViewer = <T = any>({
|
|
|
271
365
|
dataLength,
|
|
272
366
|
width,
|
|
273
367
|
itemSpacing,
|
|
274
|
-
|
|
368
|
+
enableHorizontalSwipe,
|
|
275
369
|
enableLoop,
|
|
276
370
|
translateX,
|
|
277
371
|
translateY,
|
|
@@ -284,19 +378,21 @@ export const useGestureViewer = <T = any>({
|
|
|
284
378
|
);
|
|
285
379
|
|
|
286
380
|
const dismissGesture = useMemo(() => {
|
|
381
|
+
const canDismiss = !isZoomed && dismissOptions.enabled;
|
|
382
|
+
|
|
287
383
|
return Gesture.Pan()
|
|
288
384
|
.minDistance(10)
|
|
289
385
|
.averageTouches(true)
|
|
290
386
|
.activeCursor('grabbing')
|
|
291
387
|
.activeOffsetY([-10, 10])
|
|
292
388
|
.failOffsetX([-10, 10])
|
|
293
|
-
.enabled(
|
|
389
|
+
.enabled(canDismiss)
|
|
294
390
|
.onUpdate((event) => {
|
|
295
|
-
translateY.value = event.translationY / resistance;
|
|
391
|
+
translateY.value = event.translationY / dismissOptions.resistance;
|
|
296
392
|
})
|
|
297
393
|
.onEnd((event) => {
|
|
298
|
-
if (event.translationY >
|
|
299
|
-
runOnJS(
|
|
394
|
+
if (canDismiss && event.translationY > dismissOptions.threshold) {
|
|
395
|
+
runOnJS(handleDismiss)();
|
|
300
396
|
return;
|
|
301
397
|
}
|
|
302
398
|
|
|
@@ -308,11 +404,11 @@ export const useGestureViewer = <T = any>({
|
|
|
308
404
|
energyThreshold: 6e-9,
|
|
309
405
|
});
|
|
310
406
|
});
|
|
311
|
-
}, [translateY,
|
|
407
|
+
}, [translateY, dismissOptions, handleDismiss, isZoomed]);
|
|
312
408
|
|
|
313
409
|
const zoomPinchGesture = useMemo(() => {
|
|
314
410
|
return Gesture.Pinch()
|
|
315
|
-
.enabled(
|
|
411
|
+
.enabled(enablePinchZoom)
|
|
316
412
|
.onBegin(() => {
|
|
317
413
|
startScale.value = scale.value;
|
|
318
414
|
initialTranslateX.value = translateX.value;
|
|
@@ -388,7 +484,7 @@ export const useGestureViewer = <T = any>({
|
|
|
388
484
|
});
|
|
389
485
|
}, [
|
|
390
486
|
scale,
|
|
391
|
-
|
|
487
|
+
enablePinchZoom,
|
|
392
488
|
maxZoomScale,
|
|
393
489
|
translateX,
|
|
394
490
|
translateY,
|
|
@@ -402,7 +498,7 @@ export const useGestureViewer = <T = any>({
|
|
|
402
498
|
|
|
403
499
|
const zoomPanGesture = useMemo(() => {
|
|
404
500
|
return Gesture.Pan()
|
|
405
|
-
.enabled(
|
|
501
|
+
.enabled(enablePanWhenZoomed && isZoomed)
|
|
406
502
|
.activeCursor('grabbing')
|
|
407
503
|
.averageTouches(true)
|
|
408
504
|
.onBegin(() => {
|
|
@@ -427,7 +523,7 @@ export const useGestureViewer = <T = any>({
|
|
|
427
523
|
}, [
|
|
428
524
|
translateX,
|
|
429
525
|
translateY,
|
|
430
|
-
|
|
526
|
+
enablePanWhenZoomed,
|
|
431
527
|
isZoomed,
|
|
432
528
|
scale,
|
|
433
529
|
initialTranslateX,
|
|
@@ -437,7 +533,7 @@ export const useGestureViewer = <T = any>({
|
|
|
437
533
|
|
|
438
534
|
const doubleTapGesture = useMemo(() => {
|
|
439
535
|
return Gesture.Tap()
|
|
440
|
-
.enabled(
|
|
536
|
+
.enabled(enableDoubleTapZoom)
|
|
441
537
|
.numberOfTaps(2)
|
|
442
538
|
.onEnd((event) => {
|
|
443
539
|
const nextScale = scale.value > 1 ? 1 : maxZoomScale;
|
|
@@ -471,7 +567,7 @@ export const useGestureViewer = <T = any>({
|
|
|
471
567
|
easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
|
|
472
568
|
});
|
|
473
569
|
});
|
|
474
|
-
}, [scale,
|
|
570
|
+
}, [scale, enableDoubleTapZoom, maxZoomScale, translateX, translateY, width, screenHeight]);
|
|
475
571
|
|
|
476
572
|
const zoomGesture = useMemo(() => {
|
|
477
573
|
return Gesture.Race(zoomPinchGesture, Gesture.Exclusive(zoomPanGesture, doubleTapGesture));
|
|
@@ -479,7 +575,12 @@ export const useGestureViewer = <T = any>({
|
|
|
479
575
|
|
|
480
576
|
const animatedStyle = useAnimatedStyle(() => {
|
|
481
577
|
return {
|
|
578
|
+
opacity: triggerOpacity.value,
|
|
482
579
|
transform: [
|
|
580
|
+
{ translateX: triggerTranslateX.value },
|
|
581
|
+
{ translateY: triggerTranslateY.value },
|
|
582
|
+
{ scale: triggerScale.value },
|
|
583
|
+
|
|
483
584
|
{ translateY: translateY.value },
|
|
484
585
|
{ translateX: translateX.value },
|
|
485
586
|
{ scale: scale.value },
|
|
@@ -489,14 +590,16 @@ export const useGestureViewer = <T = any>({
|
|
|
489
590
|
});
|
|
490
591
|
|
|
491
592
|
const backdropStyle = useAnimatedStyle(() => {
|
|
492
|
-
|
|
493
|
-
|
|
593
|
+
const baseOpacity = triggerOpacity.value;
|
|
594
|
+
|
|
595
|
+
if (!dismissOptions.fadeBackdrop || scale.value !== 1) {
|
|
596
|
+
return { opacity: baseOpacity };
|
|
494
597
|
}
|
|
495
598
|
|
|
496
|
-
const
|
|
599
|
+
const dismissOpacity = interpolate(translateY.value, [0, 200], [1, 0], 'clamp');
|
|
497
600
|
|
|
498
|
-
return { opacity };
|
|
499
|
-
}, [
|
|
601
|
+
return { opacity: baseOpacity * dismissOpacity };
|
|
602
|
+
}, [dismissOptions.fadeBackdrop]);
|
|
500
603
|
|
|
501
604
|
const onScrollBeginDrag = useCallback(() => {
|
|
502
605
|
manager?.handleScrollBeginDrag();
|
|
@@ -504,7 +607,6 @@ export const useGestureViewer = <T = any>({
|
|
|
504
607
|
|
|
505
608
|
return {
|
|
506
609
|
dataLength,
|
|
507
|
-
translateY,
|
|
508
610
|
listRef,
|
|
509
611
|
isZoomed,
|
|
510
612
|
isRotated,
|
|
@@ -513,6 +615,7 @@ export const useGestureViewer = <T = any>({
|
|
|
513
615
|
|
|
514
616
|
onMomentumScrollEnd,
|
|
515
617
|
onScrollBeginDrag,
|
|
618
|
+
handleDismiss,
|
|
516
619
|
|
|
517
620
|
animatedStyle,
|
|
518
621
|
backdropStyle,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import type GestureViewerManager from './GestureViewerManager';
|
|
3
3
|
import { registry } from './GestureViewerRegistry';
|
|
4
|
-
import type { GestureViewerController
|
|
4
|
+
import type { GestureViewerController } from './types';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Hook to control the gesture viewer programmatically.
|
|
8
8
|
*
|
|
9
9
|
* @param id - Viewer instance identifier (default: 'default')
|
|
10
|
-
* @returns Methods
|
|
10
|
+
* @returns Methods for controlling the viewer
|
|
11
11
|
*
|
|
12
12
|
* **Available methods:**
|
|
13
13
|
* - `goToIndex(index: number)` - Navigate to specific index (0 to totalCount-1)
|
|
@@ -16,98 +16,46 @@ import type { GestureViewerController, GestureViewerControllerState } from './ty
|
|
|
16
16
|
* - `zoomIn(multiplier?: number)` - Zoom in (default: 0.25)
|
|
17
17
|
* - `zoomOut(multiplier?: number)` - Zoom out (default: 0.25)
|
|
18
18
|
* - `resetZoom(scale?: number)` - Reset zoom level (default: 1.0)
|
|
19
|
-
* - `rotate(angle?:
|
|
20
|
-
*
|
|
21
|
-
* **Available state:**
|
|
22
|
-
* - `currentIndex: number` - Current active index (read-only)
|
|
23
|
-
* - `totalCount: number` - Total number of items (read-only)
|
|
19
|
+
* - `rotate(angle?: RotationAngle, clockwise?: boolean)` - Rotate content (default: 90° clockwise)
|
|
24
20
|
*
|
|
25
21
|
* @example
|
|
26
22
|
* ```tsx
|
|
27
|
-
* const
|
|
23
|
+
* const controller = useGestureViewerController();
|
|
28
24
|
*
|
|
29
25
|
* // Navigate to specific index
|
|
30
|
-
* goToIndex(2);
|
|
26
|
+
* controller.goToIndex(2);
|
|
31
27
|
*
|
|
32
28
|
* // Go to next image
|
|
33
|
-
* goToNext();
|
|
29
|
+
* controller.goToNext();
|
|
34
30
|
*
|
|
35
31
|
* // Zoom in by 25%
|
|
36
|
-
* zoomIn();
|
|
32
|
+
* controller.zoomIn();
|
|
37
33
|
*
|
|
38
34
|
* // Zoom out by 50%
|
|
39
|
-
* zoomOut(0.5);
|
|
35
|
+
* controller.zoomOut(0.5);
|
|
40
36
|
*
|
|
41
37
|
* // Reset to original size
|
|
42
|
-
* resetZoom();
|
|
38
|
+
* controller.resetZoom();
|
|
43
39
|
*
|
|
44
40
|
* // Rotate 90 degrees clockwise
|
|
45
|
-
* rotate();
|
|
41
|
+
* controller.rotate();
|
|
46
42
|
*
|
|
47
43
|
* // Rotate 180 degrees
|
|
48
|
-
* rotate(180);
|
|
49
|
-
*
|
|
50
|
-
* // Check current state
|
|
51
|
-
* console.log(`Image ${currentIndex + 1} of ${totalCount}`);
|
|
52
|
-
*
|
|
53
|
-
* // Check navigation availability
|
|
54
|
-
* const canGoNext = currentIndex < totalCount - 1;
|
|
55
|
-
* const canGoPrevious = currentIndex > 0;
|
|
44
|
+
* controller.rotate(180);
|
|
56
45
|
* ```
|
|
57
46
|
*/
|
|
58
47
|
export const useGestureViewerController = (id = 'default'): GestureViewerController => {
|
|
59
48
|
const managerRef = useRef<GestureViewerManager | null>(null);
|
|
60
|
-
const stateRef = useRef<GestureViewerControllerState>({ currentIndex: 0, totalCount: 0 });
|
|
61
|
-
|
|
62
|
-
const updateState = useCallback((newState: GestureViewerControllerState, onStoreChange: () => void) => {
|
|
63
|
-
if (
|
|
64
|
-
stateRef.current.currentIndex !== newState.currentIndex ||
|
|
65
|
-
stateRef.current.totalCount !== newState.totalCount
|
|
66
|
-
) {
|
|
67
|
-
stateRef.current = {
|
|
68
|
-
currentIndex: newState.currentIndex,
|
|
69
|
-
totalCount: newState.totalCount,
|
|
70
|
-
};
|
|
71
|
-
onStoreChange();
|
|
72
|
-
}
|
|
73
|
-
}, []);
|
|
74
|
-
|
|
75
|
-
const subscribe = useCallback(
|
|
76
|
-
(onStoreChange: () => void) => {
|
|
77
|
-
let unsubscribeFromManager: (() => void) | null = null;
|
|
78
|
-
|
|
79
|
-
const unsubscribeFromRegistry = registry.subscribeToManager(id, (newManager) => {
|
|
80
|
-
unsubscribeFromManager?.();
|
|
81
|
-
unsubscribeFromManager = null;
|
|
82
|
-
managerRef.current = newManager;
|
|
83
49
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const unsubscribe = registry.subscribeToManager(id, (manager) => {
|
|
52
|
+
managerRef.current = manager;
|
|
53
|
+
});
|
|
88
54
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
55
|
+
return unsubscribe;
|
|
56
|
+
}, [id]);
|
|
92
57
|
|
|
93
|
-
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
return () => {
|
|
97
|
-
unsubscribeFromRegistry();
|
|
98
|
-
unsubscribeFromManager?.();
|
|
99
|
-
};
|
|
100
|
-
},
|
|
101
|
-
[id, updateState],
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const getSnapshot = useCallback(() => {
|
|
105
|
-
return stateRef.current;
|
|
106
|
-
}, []);
|
|
107
|
-
|
|
108
|
-
const state = useSyncExternalStore(subscribe, getSnapshot);
|
|
109
|
-
|
|
110
|
-
const controller = useMemo<Omit<GestureViewerController, 'currentIndex' | 'totalCount'>>(
|
|
58
|
+
return useMemo<GestureViewerController>(
|
|
111
59
|
() => ({
|
|
112
60
|
goToIndex: (index) => {
|
|
113
61
|
managerRef.current?.goToIndex(index);
|
|
@@ -133,9 +81,4 @@ export const useGestureViewerController = (id = 'default'): GestureViewerControl
|
|
|
133
81
|
}),
|
|
134
82
|
[],
|
|
135
83
|
);
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
...controller,
|
|
139
|
-
...state,
|
|
140
|
-
};
|
|
141
84
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
2
|
+
import { registry } from './GestureViewerRegistry';
|
|
3
|
+
import type { GestureViewerState } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to access the current state of the gesture viewer.
|
|
7
|
+
*
|
|
8
|
+
* @param id - Viewer instance identifier (default: 'default')
|
|
9
|
+
* @returns Current state of the viewer
|
|
10
|
+
*
|
|
11
|
+
* **Available state:**
|
|
12
|
+
* - `currentIndex: number` - Current active index (read-only)
|
|
13
|
+
* - `totalCount: number` - Total number of items (read-only)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const { currentIndex, totalCount } = useGestureViewerState();
|
|
18
|
+
*
|
|
19
|
+
* // Display current position
|
|
20
|
+
* return (
|
|
21
|
+
* <Text>
|
|
22
|
+
* {currentIndex + 1} / {totalCount}
|
|
23
|
+
* </Text>
|
|
24
|
+
* );
|
|
25
|
+
*
|
|
26
|
+
* // React to index changes
|
|
27
|
+
* useEffect(() => {
|
|
28
|
+
* console.log(`Moved to image ${currentIndex + 1}`);
|
|
29
|
+
* trackPageView(currentIndex);
|
|
30
|
+
* }, [currentIndex]);
|
|
31
|
+
*
|
|
32
|
+
* // Check navigation availability
|
|
33
|
+
* const canGoNext = currentIndex < totalCount - 1;
|
|
34
|
+
* const canGoPrevious = currentIndex > 0;
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export const useGestureViewerState = (id = 'default'): GestureViewerState => {
|
|
38
|
+
const stateRef = useRef<GestureViewerState>({ currentIndex: 0, totalCount: 0 });
|
|
39
|
+
|
|
40
|
+
const updateState = useCallback((newState: GestureViewerState, onStoreChange: () => void) => {
|
|
41
|
+
if (
|
|
42
|
+
stateRef.current.currentIndex !== newState.currentIndex ||
|
|
43
|
+
stateRef.current.totalCount !== newState.totalCount
|
|
44
|
+
) {
|
|
45
|
+
stateRef.current = { ...newState };
|
|
46
|
+
onStoreChange();
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const subscribe = useCallback(
|
|
51
|
+
(onStoreChange: () => void) => {
|
|
52
|
+
let unsubscribeFromManager: (() => void) | null = null;
|
|
53
|
+
|
|
54
|
+
const unsubscribeFromRegistry = registry.subscribeToManager(id, (newManager) => {
|
|
55
|
+
unsubscribeFromManager?.();
|
|
56
|
+
unsubscribeFromManager = null;
|
|
57
|
+
|
|
58
|
+
if (newManager) {
|
|
59
|
+
unsubscribeFromManager = newManager.subscribe((newState) => {
|
|
60
|
+
updateState(newState, onStoreChange);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
updateState(newManager.getState(), onStoreChange);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
updateState({ currentIndex: 0, totalCount: 0 }, onStoreChange);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
unsubscribeFromRegistry();
|
|
72
|
+
unsubscribeFromManager?.();
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
[id, updateState],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const getSnapshot = useCallback(() => {
|
|
79
|
+
return stateRef.current;
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const state = useSyncExternalStore(subscribe, getSnapshot);
|
|
83
|
+
|
|
84
|
+
return state;
|
|
85
|
+
};
|