react-native-gesture-image-viewer 2.1.4 → 2.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.
Files changed (56) hide show
  1. package/README.md +6 -1
  2. package/lib/module/GestureTrigger.js +22 -5
  3. package/lib/module/GestureTrigger.js.map +1 -1
  4. package/lib/module/GestureViewer.js +2 -2
  5. package/lib/module/GestureViewer.js.map +1 -1
  6. package/lib/module/GestureViewerManager.js +3 -0
  7. package/lib/module/GestureViewerManager.js.map +1 -1
  8. package/lib/module/GestureViewerRegistry.js +52 -8
  9. package/lib/module/GestureViewerRegistry.js.map +1 -1
  10. package/lib/module/index.js.map +1 -1
  11. package/lib/module/useGestureViewer.js +141 -43
  12. package/lib/module/useGestureViewer.js.map +1 -1
  13. package/lib/module/useGestureViewerEvent.js +15 -1
  14. package/lib/module/useGestureViewerEvent.js.map +1 -1
  15. package/lib/module/useGestureViewerPaging.js +16 -2
  16. package/lib/module/useGestureViewerPaging.js.map +1 -1
  17. package/lib/module/useGestureViewerPaging.web.js +62 -28
  18. package/lib/module/useGestureViewerPaging.web.js.map +1 -1
  19. package/lib/module/utils/dismiss.js +25 -0
  20. package/lib/module/utils/dismiss.js.map +1 -0
  21. package/lib/typescript/src/GestureTrigger.d.ts +9 -3
  22. package/lib/typescript/src/GestureTrigger.d.ts.map +1 -1
  23. package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
  24. package/lib/typescript/src/GestureViewerManager.d.ts +2 -1
  25. package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
  26. package/lib/typescript/src/GestureViewerRegistry.d.ts +11 -4
  27. package/lib/typescript/src/GestureViewerRegistry.d.ts.map +1 -1
  28. package/lib/typescript/src/index.d.ts +1 -1
  29. package/lib/typescript/src/index.d.ts.map +1 -1
  30. package/lib/typescript/src/types.d.ts +33 -4
  31. package/lib/typescript/src/types.d.ts.map +1 -1
  32. package/lib/typescript/src/useGestureViewer.d.ts +2 -2
  33. package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
  34. package/lib/typescript/src/useGestureViewerEvent.d.ts +15 -1
  35. package/lib/typescript/src/useGestureViewerEvent.d.ts.map +1 -1
  36. package/lib/typescript/src/useGestureViewerPaging.d.ts +1 -1
  37. package/lib/typescript/src/useGestureViewerPaging.d.ts.map +1 -1
  38. package/lib/typescript/src/useGestureViewerPaging.types.d.ts +10 -1
  39. package/lib/typescript/src/useGestureViewerPaging.types.d.ts.map +1 -1
  40. package/lib/typescript/src/useGestureViewerPaging.web.d.ts +1 -1
  41. package/lib/typescript/src/useGestureViewerPaging.web.d.ts.map +1 -1
  42. package/lib/typescript/src/utils/dismiss.d.ts +4 -0
  43. package/lib/typescript/src/utils/dismiss.d.ts.map +1 -0
  44. package/package.json +1 -1
  45. package/src/GestureTrigger.tsx +31 -6
  46. package/src/GestureViewer.tsx +4 -2
  47. package/src/GestureViewerManager.ts +4 -0
  48. package/src/GestureViewerRegistry.ts +67 -8
  49. package/src/index.tsx +1 -0
  50. package/src/types.ts +31 -4
  51. package/src/useGestureViewer.ts +219 -81
  52. package/src/useGestureViewerEvent.ts +15 -1
  53. package/src/useGestureViewerPaging.ts +18 -2
  54. package/src/useGestureViewerPaging.types.ts +8 -1
  55. package/src/useGestureViewerPaging.web.ts +77 -33
  56. package/src/utils/dismiss.ts +36 -0
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { InteractionManager, Platform, useWindowDimensions } from 'react-native';
2
+ import { InteractionManager, Platform, type View, useWindowDimensions } from 'react-native';
3
3
  import { Gesture, type GestureType } from 'react-native-gesture-handler';
4
4
  import {
5
5
  Easing,
@@ -17,6 +17,7 @@ import { registry } from './GestureViewerRegistry';
17
17
  import type { GestureViewerProps, TriggerRect } from './types';
18
18
  import { useGestureViewerPaging } from './useGestureViewerPaging';
19
19
  import { createBoundsConstraint, createScrollAction } from './utils';
20
+ import { getDismissDistance, shouldDismissByDirection } from './utils/dismiss';
20
21
  import { applyTapZoomAtPoint } from './utils/tapZoom';
21
22
 
22
23
  type UseGestureViewerProps<ItemT, LC> = Omit<
@@ -34,6 +35,7 @@ export const useGestureViewer = <ItemT, LC>({
34
35
  data,
35
36
  initialIndex = 0,
36
37
  onDismiss,
38
+ onSingleTap,
37
39
  width: customWidth,
38
40
  dismiss,
39
41
  enableDoubleTapZoom = true,
@@ -62,10 +64,19 @@ export const useGestureViewer = <ItemT, LC>({
62
64
  const [shouldStartTriggerAnimation, setShouldStartTriggerAnimation] = useState(false);
63
65
  const [manager, setManager] = useState<GestureViewerManager | null>(null);
64
66
  const [currentIndex, setCurrentIndex] = useState(initialIndex);
67
+ const [activeTriggerNode, setActiveTriggerNode] = useState<View | null>(null);
65
68
 
66
69
  const listRef = useRef<any>(null);
67
70
  const triggerRectRef = useRef<TriggerRect | null>(null);
71
+ const pendingIndexRef = useRef(initialIndex);
68
72
  const onAnimationCompleteRef = useRef(triggerAnimation?.onAnimationComplete);
73
+ const onSingleTapRef = useRef(onSingleTap);
74
+ const dataRef = useRef(data);
75
+ const managerRef = useRef(manager);
76
+
77
+ const isValidTriggerRect = useCallback((rect: TriggerRect | null): rect is TriggerRect => {
78
+ return !!rect && rect.width > 0 && rect.height > 0;
79
+ }, []);
69
80
 
70
81
  const initialTranslateY = useSharedValue(0);
71
82
  const initialTranslateX = useSharedValue(0);
@@ -98,12 +109,19 @@ export const useGestureViewer = <ItemT, LC>({
98
109
 
99
110
  const dismissOptions = useMemo(
100
111
  () => ({
112
+ direction: dismiss?.direction ?? 'down',
101
113
  enabled: dismiss?.enabled ?? true,
102
114
  fadeBackdrop: dismiss?.fadeBackdrop ?? true,
103
115
  resistance: dismiss?.resistance ?? 2,
104
116
  threshold: dismiss?.threshold ?? 80,
105
117
  }),
106
- [dismiss?.enabled, dismiss?.threshold, dismiss?.resistance, dismiss?.fadeBackdrop],
118
+ [
119
+ dismiss?.direction,
120
+ dismiss?.enabled,
121
+ dismiss?.threshold,
122
+ dismiss?.resistance,
123
+ dismiss?.fadeBackdrop,
124
+ ],
107
125
  );
108
126
 
109
127
  const adjustedInitialIndex = useMemo(() => {
@@ -138,12 +156,25 @@ export const useGestureViewer = <ItemT, LC>({
138
156
  rotation.value = 0;
139
157
  }, [initialTranslateX, initialTranslateY, rotation, scale, startScale, translateX, translateY]);
140
158
 
159
+ const syncPendingIndex = useCallback(
160
+ (nextIndex: number) => {
161
+ if (nextIndex < 0 || nextIndex >= dataLength) {
162
+ return;
163
+ }
164
+
165
+ pendingIndexRef.current = nextIndex;
166
+ },
167
+ [dataLength],
168
+ );
169
+
141
170
  const syncCurrentIndex = useCallback(
142
171
  (nextIndex: number) => {
143
172
  if (!manager || nextIndex < 0 || nextIndex >= dataLength) {
144
173
  return;
145
174
  }
146
175
 
176
+ pendingIndexRef.current = nextIndex;
177
+
147
178
  const managerCurrentIndex = manager.getState().currentIndex;
148
179
 
149
180
  if (nextIndex === managerCurrentIndex) {
@@ -157,23 +188,13 @@ export const useGestureViewer = <ItemT, LC>({
157
188
  [dataLength, manager, resetTransformState],
158
189
  );
159
190
 
160
- const emitZoomChange = useCallback(
161
- (currentScale: number, prevScale: number | null) => {
162
- if (manager) {
163
- manager.emitZoomChange(currentScale, prevScale);
164
- }
165
- },
166
- [manager],
167
- );
191
+ const emitZoomChange = useCallback((currentScale: number, prevScale: number | null) => {
192
+ managerRef.current?.emitZoomChange(currentScale, prevScale);
193
+ }, []);
168
194
 
169
- const emitRotationChange = useCallback(
170
- (currentRotation: number, prevRotation: number | null) => {
171
- if (manager) {
172
- manager.emitRotationChange(currentRotation, prevRotation);
173
- }
174
- },
175
- [manager],
176
- );
195
+ const emitRotationChange = useCallback((currentRotation: number, prevRotation: number | null) => {
196
+ managerRef.current?.emitRotationChange(currentRotation, prevRotation);
197
+ }, []);
177
198
 
178
199
  const onAnimationComplete = useCallback(() => {
179
200
  onAnimationCompleteRef.current?.();
@@ -206,7 +227,10 @@ export const useGestureViewer = <ItemT, LC>({
206
227
 
207
228
  const unsubscribeFromRegistry = registry.subscribeToManager(id, (managerInstance) => {
208
229
  setManager(managerInstance);
209
- unsubscribe = managerInstance?.subscribe((state) => setCurrentIndex(state.currentIndex));
230
+ unsubscribe = managerInstance?.subscribe((state) => {
231
+ pendingIndexRef.current = state.currentIndex;
232
+ setCurrentIndex(state.currentIndex);
233
+ });
210
234
  });
211
235
 
212
236
  return () => {
@@ -216,6 +240,12 @@ export const useGestureViewer = <ItemT, LC>({
216
240
  }, [id]);
217
241
 
218
242
  useEffect(() => {
243
+ return registry.subscribeToActiveTrigger(id, setActiveTriggerNode);
244
+ }, [id]);
245
+
246
+ useEffect(() => {
247
+ pendingIndexRef.current = initialIndex;
248
+
219
249
  if (!manager) {
220
250
  return;
221
251
  }
@@ -289,6 +319,18 @@ export const useGestureViewer = <ItemT, LC>({
289
319
  onAnimationCompleteRef.current = triggerAnimation?.onAnimationComplete;
290
320
  }, [triggerAnimation?.onAnimationComplete]);
291
321
 
322
+ useEffect(() => {
323
+ dataRef.current = data;
324
+ }, [data]);
325
+
326
+ useEffect(() => {
327
+ managerRef.current = manager;
328
+ }, [manager]);
329
+
330
+ useEffect(() => {
331
+ onSingleTapRef.current = onSingleTap;
332
+ }, [onSingleTap]);
333
+
292
334
  useEffect(() => {
293
335
  if (shouldStartTriggerAnimation && triggerRectRef.current) {
294
336
  const startX = triggerRectRef.current.x + triggerRectRef.current.width / 2 - width / 2;
@@ -331,32 +373,41 @@ export const useGestureViewer = <ItemT, LC>({
331
373
  ]);
332
374
 
333
375
  useEffect(() => {
334
- const node = registry.getTriggerNode(id);
335
-
336
- if (node && typeof node.measure === 'function') {
337
- node.measure((_x, _y, width, height, pageX, pageY) => {
338
- triggerRectRef.current = { height, width, x: pageX, y: pageY };
339
- triggerOpacity.value = 0;
340
- setShouldStartTriggerAnimation(true);
341
- registry.clearTriggerNode(id);
342
- });
376
+ if (!activeTriggerNode || typeof activeTriggerNode.measure !== 'function') {
377
+ return;
343
378
  }
344
379
 
380
+ activeTriggerNode.measure((_x, _y, measuredWidth, measuredHeight, pageX, pageY) => {
381
+ const nextTriggerRect = {
382
+ height: measuredHeight,
383
+ width: measuredWidth,
384
+ x: pageX,
385
+ y: pageY,
386
+ } satisfies TriggerRect;
387
+
388
+ if (!isValidTriggerRect(nextTriggerRect)) {
389
+ registry.clearActiveTriggerNode(id);
390
+ return;
391
+ }
392
+
393
+ triggerRectRef.current = nextTriggerRect;
394
+ triggerOpacity.value = 0;
395
+ setShouldStartTriggerAnimation(true);
396
+ registry.clearActiveTriggerNode(id);
397
+ });
398
+ }, [activeTriggerNode, id, isValidTriggerRect, triggerOpacity]);
399
+
400
+ useEffect(() => {
345
401
  return () => {
346
402
  triggerRectRef.current = null;
347
403
  };
348
- }, [id, triggerOpacity]);
349
-
350
- const handleDismiss = useCallback(() => {
351
- onDismissStart?.();
404
+ }, []);
352
405
 
353
- if (triggerRectRef.current) {
354
- const endX = triggerRectRef.current.x + triggerRectRef.current.width / 2 - width / 2;
355
- const endY = triggerRectRef.current.y + triggerRectRef.current.height / 2 - height / 2;
356
- const endScale = Math.min(
357
- triggerRectRef.current.width / width,
358
- triggerRectRef.current.height / height,
359
- );
406
+ const animateDismissToRect = useCallback(
407
+ (rect: TriggerRect) => {
408
+ const endX = rect.x + rect.width / 2 - width / 2;
409
+ const endY = rect.y + rect.height / 2 - height / 2;
410
+ const endScale = Math.min(rect.width / width, rect.height / height);
360
411
 
361
412
  triggerScale.value = withTiming(endScale, animationConfig);
362
413
  triggerTranslateX.value = withTiming(endX, animationConfig);
@@ -366,23 +417,62 @@ export const useGestureViewer = <ItemT, LC>({
366
417
  scheduleOnRN(onDismiss);
367
418
  }
368
419
  });
369
- return;
370
- }
420
+ },
421
+ [
422
+ animationConfig,
423
+ height,
424
+ onDismiss,
425
+ triggerOpacity,
426
+ triggerScale,
427
+ triggerTranslateX,
428
+ triggerTranslateY,
429
+ width,
430
+ ],
431
+ );
371
432
 
433
+ const dismissWithoutTrigger = useCallback(() => {
372
434
  if (onDismiss) {
373
435
  scheduleOnRN(onDismiss);
374
436
  }
375
- }, [
376
- animationConfig,
377
- onDismiss,
378
- onDismissStart,
379
- width,
380
- height,
381
- triggerTranslateX,
382
- triggerScale,
383
- triggerTranslateY,
384
- triggerOpacity,
385
- ]);
437
+ }, [onDismiss]);
438
+
439
+ const handleDismiss = useCallback(() => {
440
+ onDismissStart?.();
441
+
442
+ const dismissTargetIndex = pendingIndexRef.current;
443
+ const indexedTriggerNode = registry.getIndexedTriggerNode(id, dismissTargetIndex);
444
+
445
+ if (indexedTriggerNode && typeof indexedTriggerNode.measure === 'function') {
446
+ indexedTriggerNode.measure((_x, _y, measuredWidth, measuredHeight, pageX, pageY) => {
447
+ const currentTriggerRect = {
448
+ height: measuredHeight,
449
+ width: measuredWidth,
450
+ x: pageX,
451
+ y: pageY,
452
+ } satisfies TriggerRect;
453
+
454
+ if (isValidTriggerRect(currentTriggerRect)) {
455
+ animateDismissToRect(currentTriggerRect);
456
+ return;
457
+ }
458
+
459
+ if (isValidTriggerRect(triggerRectRef.current)) {
460
+ animateDismissToRect(triggerRectRef.current);
461
+ return;
462
+ }
463
+
464
+ dismissWithoutTrigger();
465
+ });
466
+ return;
467
+ }
468
+
469
+ if (isValidTriggerRect(triggerRectRef.current)) {
470
+ animateDismissToRect(triggerRectRef.current);
471
+ return;
472
+ }
473
+
474
+ dismissWithoutTrigger();
475
+ }, [animateDismissToRect, dismissWithoutTrigger, id, isValidTriggerRect, onDismissStart]);
386
476
 
387
477
  const dismissGesture = useMemo(() => {
388
478
  const canDismiss = !isZoomed && dismissOptions.enabled;
@@ -399,7 +489,14 @@ export const useGestureViewer = <ItemT, LC>({
399
489
  translateY.value = event.translationY / dismissOptions.resistance;
400
490
  })
401
491
  .onEnd((event) => {
402
- if (canDismiss && event.translationY > dismissOptions.threshold) {
492
+ if (
493
+ canDismiss &&
494
+ shouldDismissByDirection(
495
+ event.translationY,
496
+ dismissOptions.threshold,
497
+ dismissOptions.direction,
498
+ )
499
+ ) {
403
500
  scheduleOnRN(handleDismiss);
404
501
  return;
405
502
  }
@@ -571,6 +668,40 @@ export const useGestureViewer = <ItemT, LC>({
571
668
  ],
572
669
  );
573
670
 
671
+ const emitSingleTap = useCallback((x: number, y: number) => {
672
+ const index = pendingIndexRef.current;
673
+ const currentData = dataRef.current;
674
+
675
+ if (index < 0 || index >= currentData.length) {
676
+ return;
677
+ }
678
+
679
+ managerRef.current?.emitTap({ kind: 'single', x, y, index });
680
+
681
+ const item = currentData[index];
682
+
683
+ if (item === undefined) {
684
+ return;
685
+ }
686
+
687
+ onSingleTapRef.current?.({ x, y, index, item });
688
+ }, []);
689
+
690
+ const singleTapGesture = useMemo(
691
+ () =>
692
+ Gesture.Tap()
693
+ .enabled(Platform.OS !== 'web')
694
+ .numberOfTaps(1)
695
+ .onEnd((event, success) => {
696
+ if (!success) {
697
+ return;
698
+ }
699
+
700
+ scheduleOnRN(emitSingleTap, event.x, event.y);
701
+ }),
702
+ [emitSingleTap],
703
+ );
704
+
574
705
  const doubleTapGesture = useMemo(
575
706
  () =>
576
707
  Gesture.Tap()
@@ -591,9 +722,14 @@ export const useGestureViewer = <ItemT, LC>({
591
722
  [enableDoubleTapZoom, height, maxZoomScale, scale, translateX, translateY, width],
592
723
  );
593
724
 
725
+ const tapGesture = useMemo(
726
+ () => Gesture.Exclusive(doubleTapGesture, singleTapGesture),
727
+ [doubleTapGesture, singleTapGesture],
728
+ );
729
+
594
730
  const zoomGesture = useMemo(
595
- () => Gesture.Race(zoomPinchGesture, Gesture.Exclusive(zoomPanGesture, doubleTapGesture)),
596
- [zoomPinchGesture, zoomPanGesture, doubleTapGesture],
731
+ () => Gesture.Race(zoomPinchGesture, Gesture.Exclusive(zoomPanGesture, tapGesture)),
732
+ [zoomPinchGesture, zoomPanGesture, tapGesture],
597
733
  );
598
734
 
599
735
  const animatedStyle = useAnimatedStyle(() => ({
@@ -617,38 +753,40 @@ export const useGestureViewer = <ItemT, LC>({
617
753
  return { opacity: baseOpacity };
618
754
  }
619
755
 
620
- const dismissOpacity = interpolate(translateY.value, [0, 200], [1, 0], 'clamp');
756
+ const dismissDistance = getDismissDistance(translateY.value, dismissOptions.direction);
757
+ const dismissOpacity = interpolate(dismissDistance, [0, 200], [1, 0], 'clamp');
621
758
 
622
759
  return { opacity: baseOpacity * dismissOpacity };
623
- }, [dismissOptions.fadeBackdrop]);
760
+ }, [dismissOptions.direction, dismissOptions.fadeBackdrop]);
624
761
 
625
762
  const nativeScrollGesture = useMemo(() => {
626
763
  return Gesture.Native().requireExternalGestureToFail(dismissGestureRef);
627
764
  }, []);
628
765
 
629
- const { onMomentumScrollEnd, onScroll, onScrollBeginDrag, onWebDoubleClick } =
630
- useGestureViewerPaging({
631
- adjustedInitialIndex,
632
- autoPlay,
633
- autoPlayInterval,
634
- currentIndex,
635
- dataLength,
636
- enableDoubleTapZoom,
637
- enableHorizontalSwipe,
638
- enableLoop,
639
- height,
640
- isRotated,
641
- isZoomed,
642
- itemSpacing,
643
- manager,
644
- maxZoomScale,
645
- scale,
646
- scrollTo,
647
- syncCurrentIndex,
648
- translateX,
649
- translateY,
650
- width,
651
- });
766
+ const { onMomentumScrollEnd, onScroll, onScrollBeginDrag, onWebClick } = useGestureViewerPaging({
767
+ adjustedInitialIndex,
768
+ autoPlay,
769
+ autoPlayInterval,
770
+ currentIndex,
771
+ dataLength,
772
+ enableDoubleTapZoom,
773
+ enableHorizontalSwipe,
774
+ enableLoop,
775
+ height,
776
+ isRotated,
777
+ isZoomed,
778
+ itemSpacing,
779
+ manager,
780
+ maxZoomScale,
781
+ onSingleTap: emitSingleTap,
782
+ scale,
783
+ scrollTo,
784
+ syncCurrentIndex,
785
+ syncPendingIndex,
786
+ translateX,
787
+ translateY,
788
+ width,
789
+ });
652
790
 
653
791
  return {
654
792
  animatedStyle,
@@ -662,7 +800,7 @@ export const useGestureViewer = <ItemT, LC>({
662
800
  isZoomed,
663
801
  listRef,
664
802
  nativeScrollGesture,
665
- onWebDoubleClick,
803
+ onWebClick,
666
804
  onMomentumScrollEnd,
667
805
  onScroll,
668
806
 
@@ -8,7 +8,7 @@ import type { GestureViewerEventCallback, GestureViewerEventType } from './types
8
8
  * Hook for subscribing to GestureViewer events on the default instance.
9
9
  *
10
10
  * This hook allows you to listen to specific events from the default GestureViewer instance
11
- * (with ID 'default'), such as zoom changes or rotation changes. Events are automatically
11
+ * (with ID 'default'), such as zoom changes, rotation changes, or confirmed tap events. Events are automatically
12
12
  * throttled to prevent excessive callback invocations during gestures.
13
13
  *
14
14
  * @param eventType - The type of event to listen for
@@ -25,6 +25,13 @@ import type { GestureViewerEventCallback, GestureViewerEventType } from './types
25
25
  * useGestureViewerEvent('rotationChange', (data) => {
26
26
  * console.log(`Rotation changed from ${data.previousRotation}° to ${data.rotation}°`);
27
27
  * });
28
+ *
29
+ * // Listen to confirmed single taps on the default instance (ID: 'default')
30
+ * useGestureViewerEvent('tap', (data) => {
31
+ * if (data.kind === 'single') {
32
+ * console.log(`Tapped item ${data.index} at (${data.x}, ${data.y})`);
33
+ * }
34
+ * });
28
35
  * ```
29
36
  */
30
37
  export function useGestureViewerEvent<T extends GestureViewerEventType>(
@@ -53,6 +60,13 @@ export function useGestureViewerEvent<T extends GestureViewerEventType>(
53
60
  * useGestureViewerEvent('modal-viewer', 'rotationChange', (data) => {
54
61
  * updateRotationIndicator(data.rotation);
55
62
  * });
63
+ *
64
+ * // Listen to confirmed single taps on a specific instance
65
+ * useGestureViewerEvent('gallery', 'tap', (data) => {
66
+ * if (data.kind === 'single') {
67
+ * console.log(`Gallery tap on item ${data.index}`);
68
+ * }
69
+ * });
56
70
  * ```
57
71
  */
58
72
  export function useGestureViewerEvent<T extends GestureViewerEventType>(
@@ -20,6 +20,7 @@ export function useGestureViewerPaging({
20
20
  manager,
21
21
  scrollTo,
22
22
  syncCurrentIndex,
23
+ syncPendingIndex,
23
24
  width,
24
25
  }: UseGestureViewerPagingArgs): UseGestureViewerPagingResult {
25
26
  useEffect(() => {
@@ -99,14 +100,29 @@ export function useGestureViewerPaging({
99
100
  ],
100
101
  );
101
102
 
103
+ const onScroll = useCallback(
104
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
105
+ if (!enableHorizontalSwipe) {
106
+ return;
107
+ }
108
+
109
+ const { contentOffset } = event.nativeEvent;
110
+ const scrollIndex = Math.round(contentOffset.x / (width + itemSpacing));
111
+ const { realIndex } = getLoopAdjustedIndex(scrollIndex, dataLength, enableLoop);
112
+
113
+ syncPendingIndex(realIndex);
114
+ },
115
+ [dataLength, enableHorizontalSwipe, enableLoop, itemSpacing, syncPendingIndex, width],
116
+ );
117
+
102
118
  const onScrollBeginDrag = useCallback(() => {
103
119
  manager?.handleScrollBeginDrag();
104
120
  }, [manager]);
105
121
 
106
122
  return {
107
123
  onMomentumScrollEnd,
108
- onScroll: undefined,
124
+ onScroll,
109
125
  onScrollBeginDrag,
110
- onWebDoubleClick: undefined,
126
+ onWebClick: undefined,
111
127
  };
112
128
  }
@@ -1,3 +1,4 @@
1
+ import type { MouseEvent } from 'react';
1
2
  import type { ScrollViewProps } from 'react-native';
2
3
  import type { SharedValue } from 'react-native-reanimated';
3
4
 
@@ -18,17 +19,23 @@ export type UseGestureViewerPagingArgs = {
18
19
  itemSpacing: number;
19
20
  manager: GestureViewerManager | null;
20
21
  maxZoomScale: number;
22
+ onSingleTap?: (x: number, y: number) => void;
21
23
  scale: SharedValue<number>;
22
24
  scrollTo: (index: number, animated: boolean) => void;
23
25
  syncCurrentIndex: (nextIndex: number) => void;
26
+ syncPendingIndex: (nextIndex: number) => void;
24
27
  translateX: SharedValue<number>;
25
28
  translateY: SharedValue<number>;
26
29
  width: number;
27
30
  };
28
31
 
32
+ export type WebClickTarget = {
33
+ getBoundingClientRect: () => { left: number; top: number };
34
+ };
35
+
29
36
  export type UseGestureViewerPagingResult = {
30
37
  onMomentumScrollEnd?: ScrollViewProps['onMomentumScrollEnd'];
31
38
  onScroll?: ScrollViewProps['onScroll'];
32
39
  onScrollBeginDrag?: ScrollViewProps['onScrollBeginDrag'];
33
- onWebDoubleClick?: (event: any) => void;
40
+ onWebClick?: (event: MouseEvent<WebClickTarget>) => void;
34
41
  };