react-native-gesture-image-viewer 2.2.0 → 2.3.1

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 (47) hide show
  1. package/README.md +6 -1
  2. package/lib/module/GestureViewer.js +2 -2
  3. package/lib/module/GestureViewer.js.map +1 -1
  4. package/lib/module/GestureViewerManager.js +27 -23
  5. package/lib/module/GestureViewerManager.js.map +1 -1
  6. package/lib/module/index.js.map +1 -1
  7. package/lib/module/useGestureViewer.js +156 -106
  8. package/lib/module/useGestureViewer.js.map +1 -1
  9. package/lib/module/useGestureViewerEvent.js +15 -1
  10. package/lib/module/useGestureViewerEvent.js.map +1 -1
  11. package/lib/module/useGestureViewerPaging.js +1 -1
  12. package/lib/module/useGestureViewerPaging.js.map +1 -1
  13. package/lib/module/useGestureViewerPaging.web.js +47 -26
  14. package/lib/module/useGestureViewerPaging.web.js.map +1 -1
  15. package/lib/module/utils/dismiss.js +25 -0
  16. package/lib/module/utils/dismiss.js.map +1 -0
  17. package/lib/module/utils/tapZoom.js +6 -6
  18. package/lib/module/utils/tapZoom.js.map +1 -1
  19. package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
  20. package/lib/typescript/src/GestureViewerManager.d.ts +2 -1
  21. package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
  22. package/lib/typescript/src/index.d.ts +1 -1
  23. package/lib/typescript/src/index.d.ts.map +1 -1
  24. package/lib/typescript/src/types.d.ts +33 -4
  25. package/lib/typescript/src/types.d.ts.map +1 -1
  26. package/lib/typescript/src/useGestureViewer.d.ts +2 -2
  27. package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
  28. package/lib/typescript/src/useGestureViewerEvent.d.ts +15 -1
  29. package/lib/typescript/src/useGestureViewerEvent.d.ts.map +1 -1
  30. package/lib/typescript/src/useGestureViewerPaging.types.d.ts +9 -1
  31. package/lib/typescript/src/useGestureViewerPaging.types.d.ts.map +1 -1
  32. package/lib/typescript/src/useGestureViewerPaging.web.d.ts +1 -1
  33. package/lib/typescript/src/useGestureViewerPaging.web.d.ts.map +1 -1
  34. package/lib/typescript/src/utils/dismiss.d.ts +4 -0
  35. package/lib/typescript/src/utils/dismiss.d.ts.map +1 -0
  36. package/package.json +1 -1
  37. package/src/GestureViewer.tsx +4 -2
  38. package/src/GestureViewerManager.ts +31 -25
  39. package/src/index.tsx +1 -0
  40. package/src/types.ts +31 -4
  41. package/src/useGestureViewer.ts +239 -157
  42. package/src/useGestureViewerEvent.ts +15 -1
  43. package/src/useGestureViewerPaging.ts +1 -1
  44. package/src/useGestureViewerPaging.types.ts +7 -1
  45. package/src/useGestureViewerPaging.web.ts +56 -32
  46. package/src/utils/dismiss.ts +36 -0
  47. package/src/utils/tapZoom.ts +6 -6
@@ -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,
@@ -68,6 +70,9 @@ export const useGestureViewer = <ItemT, LC>({
68
70
  const triggerRectRef = useRef<TriggerRect | null>(null);
69
71
  const pendingIndexRef = useRef(initialIndex);
70
72
  const onAnimationCompleteRef = useRef(triggerAnimation?.onAnimationComplete);
73
+ const onSingleTapRef = useRef(onSingleTap);
74
+ const dataRef = useRef(data);
75
+ const managerRef = useRef(manager);
71
76
 
72
77
  const isValidTriggerRect = useCallback((rect: TriggerRect | null): rect is TriggerRect => {
73
78
  return !!rect && rect.width > 0 && rect.height > 0;
@@ -104,12 +109,19 @@ export const useGestureViewer = <ItemT, LC>({
104
109
 
105
110
  const dismissOptions = useMemo(
106
111
  () => ({
112
+ direction: dismiss?.direction ?? 'down',
107
113
  enabled: dismiss?.enabled ?? true,
108
114
  fadeBackdrop: dismiss?.fadeBackdrop ?? true,
109
115
  resistance: dismiss?.resistance ?? 2,
110
116
  threshold: dismiss?.threshold ?? 80,
111
117
  }),
112
- [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
+ ],
113
125
  );
114
126
 
115
127
  const adjustedInitialIndex = useMemo(() => {
@@ -135,13 +147,13 @@ export const useGestureViewer = <ItemT, LC>({
135
147
  );
136
148
 
137
149
  const resetTransformState = useCallback(() => {
138
- translateX.value = withTiming(0);
139
- translateY.value = withTiming(0);
140
- initialTranslateX.value = withTiming(0);
141
- initialTranslateY.value = withTiming(0);
142
- startScale.value = withTiming(1);
143
- scale.value = withTiming(1);
144
- rotation.value = 0;
150
+ translateX.set(withTiming(0));
151
+ translateY.set(withTiming(0));
152
+ initialTranslateX.set(withTiming(0));
153
+ initialTranslateY.set(withTiming(0));
154
+ startScale.set(withTiming(1));
155
+ scale.set(withTiming(1));
156
+ rotation.set(0);
145
157
  }, [initialTranslateX, initialTranslateY, rotation, scale, startScale, translateX, translateY]);
146
158
 
147
159
  const syncPendingIndex = useCallback(
@@ -176,30 +188,20 @@ export const useGestureViewer = <ItemT, LC>({
176
188
  [dataLength, manager, resetTransformState],
177
189
  );
178
190
 
179
- const emitZoomChange = useCallback(
180
- (currentScale: number, prevScale: number | null) => {
181
- if (manager) {
182
- manager.emitZoomChange(currentScale, prevScale);
183
- }
184
- },
185
- [manager],
186
- );
191
+ const emitZoomChange = useCallback((currentScale: number, prevScale: number | null) => {
192
+ managerRef.current?.emitZoomChange(currentScale, prevScale);
193
+ }, []);
187
194
 
188
- const emitRotationChange = useCallback(
189
- (currentRotation: number, prevRotation: number | null) => {
190
- if (manager) {
191
- manager.emitRotationChange(currentRotation, prevRotation);
192
- }
193
- },
194
- [manager],
195
- );
195
+ const emitRotationChange = useCallback((currentRotation: number, prevRotation: number | null) => {
196
+ managerRef.current?.emitRotationChange(currentRotation, prevRotation);
197
+ }, []);
196
198
 
197
199
  const onAnimationComplete = useCallback(() => {
198
200
  onAnimationCompleteRef.current?.();
199
201
  }, []);
200
202
 
201
203
  useAnimatedReaction(
202
- () => scale.value,
204
+ () => scale.get(),
203
205
  (currentScale, previousScale) => {
204
206
  if (currentScale !== previousScale) {
205
207
  scheduleOnRN(emitZoomChange, currentScale, previousScale);
@@ -210,7 +212,7 @@ export const useGestureViewer = <ItemT, LC>({
210
212
  );
211
213
 
212
214
  useAnimatedReaction(
213
- () => rotation.value,
215
+ () => rotation.get(),
214
216
  (currentRotation, previousRotation) => {
215
217
  if (currentRotation !== previousRotation) {
216
218
  scheduleOnRN(emitRotationChange, currentRotation, previousRotation);
@@ -284,12 +286,12 @@ export const useGestureViewer = <ItemT, LC>({
284
286
  }, [manager]);
285
287
 
286
288
  useEffect(() => {
287
- translateY.value = 0;
288
- translateX.value = 0;
289
- scale.value = 1;
290
- backdropOpacity.value = 1;
291
- startScale.value = 1;
292
- rotation.value = 0;
289
+ translateY.set(0);
290
+ translateX.set(0);
291
+ scale.set(1);
292
+ backdropOpacity.set(1);
293
+ startScale.set(1);
294
+ rotation.set(0);
293
295
 
294
296
  if (adjustedInitialIndex <= 0 || !listRef.current) {
295
297
  return;
@@ -317,6 +319,18 @@ export const useGestureViewer = <ItemT, LC>({
317
319
  onAnimationCompleteRef.current = triggerAnimation?.onAnimationComplete;
318
320
  }, [triggerAnimation?.onAnimationComplete]);
319
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
+
320
334
  useEffect(() => {
321
335
  if (shouldStartTriggerAnimation && triggerRectRef.current) {
322
336
  const startX = triggerRectRef.current.x + triggerRectRef.current.width / 2 - width / 2;
@@ -326,23 +340,27 @@ export const useGestureViewer = <ItemT, LC>({
326
340
  triggerRectRef.current.height / height,
327
341
  );
328
342
 
329
- triggerScale.value = initialScaleFromTrigger;
330
- triggerTranslateX.value = startX;
331
- triggerTranslateY.value = startY;
332
- triggerOpacity.value = 0;
343
+ triggerScale.set(initialScaleFromTrigger);
344
+ triggerTranslateX.set(startX);
345
+ triggerTranslateY.set(startY);
346
+ triggerOpacity.set(0);
333
347
 
334
- triggerScale.value = withTiming(1, animationConfig, (finished) => {
335
- if (finished) {
336
- scheduleOnRN(onAnimationComplete);
337
- }
338
- });
339
- triggerTranslateX.value = withTiming(0, animationConfig);
340
- triggerTranslateY.value = withTiming(0, animationConfig);
341
- triggerOpacity.value = withTiming(1, {
342
- duration: animationConfig.duration / 2,
343
- easing: animationConfig.easing,
344
- reduceMotion: animationConfig.reduceMotion,
345
- });
348
+ triggerScale.set(
349
+ withTiming(1, animationConfig, (finished) => {
350
+ if (finished) {
351
+ scheduleOnRN(onAnimationComplete);
352
+ }
353
+ }),
354
+ );
355
+ triggerTranslateX.set(withTiming(0, animationConfig));
356
+ triggerTranslateY.set(withTiming(0, animationConfig));
357
+ triggerOpacity.set(
358
+ withTiming(1, {
359
+ duration: animationConfig.duration / 2,
360
+ easing: animationConfig.easing,
361
+ reduceMotion: animationConfig.reduceMotion,
362
+ }),
363
+ );
346
364
 
347
365
  setShouldStartTriggerAnimation(false);
348
366
  }
@@ -377,7 +395,7 @@ export const useGestureViewer = <ItemT, LC>({
377
395
  }
378
396
 
379
397
  triggerRectRef.current = nextTriggerRect;
380
- triggerOpacity.value = 0;
398
+ triggerOpacity.set(0);
381
399
  setShouldStartTriggerAnimation(true);
382
400
  registry.clearActiveTriggerNode(id);
383
401
  });
@@ -395,14 +413,16 @@ export const useGestureViewer = <ItemT, LC>({
395
413
  const endY = rect.y + rect.height / 2 - height / 2;
396
414
  const endScale = Math.min(rect.width / width, rect.height / height);
397
415
 
398
- triggerScale.value = withTiming(endScale, animationConfig);
399
- triggerTranslateX.value = withTiming(endX, animationConfig);
400
- triggerTranslateY.value = withTiming(endY, animationConfig);
401
- triggerOpacity.value = withTiming(0, animationConfig, (finished) => {
402
- if (finished && onDismiss) {
403
- scheduleOnRN(onDismiss);
404
- }
405
- });
416
+ triggerScale.set(withTiming(endScale, animationConfig));
417
+ triggerTranslateX.set(withTiming(endX, animationConfig));
418
+ triggerTranslateY.set(withTiming(endY, animationConfig));
419
+ triggerOpacity.set(
420
+ withTiming(0, animationConfig, (finished) => {
421
+ if (finished && onDismiss) {
422
+ scheduleOnRN(onDismiss);
423
+ }
424
+ }),
425
+ );
406
426
  },
407
427
  [
408
428
  animationConfig,
@@ -472,21 +492,30 @@ export const useGestureViewer = <ItemT, LC>({
472
492
  .withRef(dismissGestureRef)
473
493
  .enabled(canDismiss)
474
494
  .onUpdate((event) => {
475
- translateY.value = event.translationY / dismissOptions.resistance;
495
+ translateY.set(event.translationY / dismissOptions.resistance);
476
496
  })
477
497
  .onEnd((event) => {
478
- if (canDismiss && event.translationY > dismissOptions.threshold) {
498
+ if (
499
+ canDismiss &&
500
+ shouldDismissByDirection(
501
+ event.translationY,
502
+ dismissOptions.threshold,
503
+ dismissOptions.direction,
504
+ )
505
+ ) {
479
506
  scheduleOnRN(handleDismiss);
480
507
  return;
481
508
  }
482
509
 
483
- translateY.value = withSpring(0, {
484
- damping: 50,
485
- energyThreshold: 6e-9,
486
- mass: 4,
487
- overshootClamping: false,
488
- stiffness: 600,
489
- });
510
+ translateY.set(
511
+ withSpring(0, {
512
+ damping: 50,
513
+ energyThreshold: 6e-9,
514
+ mass: 4,
515
+ overshootClamping: false,
516
+ stiffness: 600,
517
+ }),
518
+ );
490
519
  });
491
520
  }, [translateY, dismissOptions, handleDismiss, isZoomed]);
492
521
 
@@ -500,39 +529,44 @@ export const useGestureViewer = <ItemT, LC>({
500
529
  }
501
530
  })
502
531
  .onBegin((event) => {
503
- startScale.value = scale.value;
504
- initialTranslateX.value = translateX.value;
505
- initialTranslateY.value = translateY.value;
506
- lastFocalX.value = event.focalX;
507
- lastFocalY.value = event.focalY;
532
+ startScale.set(scale.get());
533
+ initialTranslateX.set(translateX.get());
534
+ initialTranslateY.set(translateY.get());
535
+ lastFocalX.set(event.focalX);
536
+ lastFocalY.set(event.focalY);
508
537
  })
509
538
  .onUpdate((event) => {
510
- const newScale = startScale.value * event.scale;
539
+ const initialScale = startScale.get();
540
+ const newScale = initialScale * event.scale;
511
541
 
512
- scale.value = newScale;
542
+ scale.set(newScale);
513
543
 
514
544
  if (newScale <= 1) {
515
- translateX.value = withTiming(0);
516
- translateY.value = withTiming(0);
545
+ translateX.set(withTiming(0));
546
+ translateY.set(withTiming(0));
517
547
  return;
518
548
  }
519
549
 
520
- const focalDeltaX = Math.abs(event.focalX - lastFocalX.value);
521
- const focalDeltaY = Math.abs(event.focalY - lastFocalY.value);
550
+ const currentLastFocalX = lastFocalX.get();
551
+ const currentLastFocalY = lastFocalY.get();
552
+ const focalDeltaX = Math.abs(event.focalX - currentLastFocalX);
553
+ const focalDeltaY = Math.abs(event.focalY - currentLastFocalY);
522
554
  const threshold = 50;
523
555
 
524
556
  if (focalDeltaX < threshold && focalDeltaY < threshold) {
525
- lastFocalX.value = event.focalX;
526
- lastFocalY.value = event.focalY;
557
+ lastFocalX.set(event.focalX);
558
+ lastFocalY.set(event.focalY);
527
559
  }
528
560
 
529
- const deltaScale = newScale - startScale.value;
530
- const centerX = lastFocalX.value - width / 2;
531
- const centerY = lastFocalY.value - height / 2;
561
+ const nextLastFocalX = lastFocalX.get();
562
+ const nextLastFocalY = lastFocalY.get();
563
+ const deltaScale = newScale - initialScale;
564
+ const centerX = nextLastFocalX - width / 2;
565
+ const centerY = nextLastFocalY - height / 2;
532
566
 
533
567
  // NOTE 새로운 이동값 = 기존 이동값 - (중심점 거리 × 스케일 변화량) / 원래 스케일 (중심점이 화면 중심에서 멀수록, 확대 배율이 클수록 더 많이 이동)
534
- const newTranslateX = initialTranslateX.value - (centerX * deltaScale) / startScale.value;
535
- const newTranslateY = initialTranslateY.value - (centerY * deltaScale) / startScale.value;
568
+ const newTranslateX = initialTranslateX.get() - (centerX * deltaScale) / initialScale;
569
+ const newTranslateY = initialTranslateY.get() - (centerY * deltaScale) / initialScale;
536
570
 
537
571
  const { translateX: constrainedTranslateX, translateY: constrainedTranslateY } =
538
572
  constrainTranslation({
@@ -541,50 +575,56 @@ export const useGestureViewer = <ItemT, LC>({
541
575
  translateY: newTranslateY,
542
576
  });
543
577
 
544
- translateX.value = constrainedTranslateX;
545
- translateY.value = constrainedTranslateY;
578
+ translateX.set(constrainedTranslateX);
579
+ translateY.set(constrainedTranslateY);
546
580
  })
547
581
  .onEnd(() => {
548
- if (scale.value > maxZoomScale) {
549
- scale.value = withTiming(maxZoomScale, {
550
- duration: 300,
551
- easing: Easing.bezier(0.25, 0.1, 0.25, 1),
552
- });
582
+ const currentScale = scale.get();
583
+
584
+ if (currentScale > maxZoomScale) {
585
+ scale.set(
586
+ withTiming(maxZoomScale, {
587
+ duration: 300,
588
+ easing: Easing.bezier(0.25, 0.1, 0.25, 1),
589
+ }),
590
+ );
553
591
 
554
592
  const { translateX: constrainedTranslateX, translateY: constrainedTranslateY } =
555
593
  constrainTranslation({
556
594
  scale: maxZoomScale,
557
- translateX: translateX.value,
558
- translateY: translateY.value,
595
+ translateX: translateX.get(),
596
+ translateY: translateY.get(),
559
597
  });
560
598
 
561
- translateX.value = withTiming(constrainedTranslateX);
562
- translateY.value = withTiming(constrainedTranslateY);
599
+ translateX.set(withTiming(constrainedTranslateX));
600
+ translateY.set(withTiming(constrainedTranslateY));
563
601
 
564
602
  return;
565
603
  }
566
604
 
567
- if (scale.value < 1) {
568
- scale.value = withTiming(1, {
569
- duration: 300,
570
- easing: Easing.bezier(0.25, 0.1, 0.25, 1),
571
- });
572
- translateX.value = withTiming(0);
573
- translateY.value = withTiming(0);
574
- initialTranslateX.value = withTiming(0);
575
- initialTranslateY.value = withTiming(0);
605
+ if (currentScale < 1) {
606
+ scale.set(
607
+ withTiming(1, {
608
+ duration: 300,
609
+ easing: Easing.bezier(0.25, 0.1, 0.25, 1),
610
+ }),
611
+ );
612
+ translateX.set(withTiming(0));
613
+ translateY.set(withTiming(0));
614
+ initialTranslateX.set(withTiming(0));
615
+ initialTranslateY.set(withTiming(0));
576
616
  return;
577
617
  }
578
618
 
579
619
  const { translateX: constrainedTranslateX, translateY: constrainedTranslateY } =
580
620
  constrainTranslation({
581
- scale: scale.value,
582
- translateX: translateX.value,
583
- translateY: translateY.value,
621
+ scale: currentScale,
622
+ translateX: translateX.get(),
623
+ translateY: translateY.get(),
584
624
  });
585
625
 
586
- translateX.value = withTiming(constrainedTranslateX);
587
- translateY.value = withTiming(constrainedTranslateY);
626
+ translateX.set(withTiming(constrainedTranslateX));
627
+ translateY.set(withTiming(constrainedTranslateY));
588
628
  })
589
629
  .onTouchesUp(() => {
590
630
  scheduleOnRN(setIsPinching, false);
@@ -616,23 +656,25 @@ export const useGestureViewer = <ItemT, LC>({
616
656
  .activeCursor('grabbing')
617
657
  .averageTouches(true)
618
658
  .onBegin(() => {
619
- initialTranslateX.value = translateX.value;
620
- initialTranslateY.value = translateY.value;
659
+ initialTranslateX.set(translateX.get());
660
+ initialTranslateY.set(translateY.get());
621
661
  })
622
662
  .onUpdate((event) => {
623
- if (scale.value > 1) {
624
- const newTranslateX = initialTranslateX.value + event.translationX;
625
- const newTranslateY = initialTranslateY.value + event.translationY;
663
+ const currentScale = scale.get();
664
+
665
+ if (currentScale > 1) {
666
+ const newTranslateX = initialTranslateX.get() + event.translationX;
667
+ const newTranslateY = initialTranslateY.get() + event.translationY;
626
668
 
627
669
  const { translateX: constrainedTranslateX, translateY: constrainedTranslateY } =
628
670
  constrainTranslation({
629
- scale: scale.value,
671
+ scale: currentScale,
630
672
  translateX: newTranslateX,
631
673
  translateY: newTranslateY,
632
674
  });
633
675
 
634
- translateX.value = constrainedTranslateX;
635
- translateY.value = constrainedTranslateY;
676
+ translateX.set(constrainedTranslateX);
677
+ translateY.set(constrainedTranslateY);
636
678
  }
637
679
  }),
638
680
  [
@@ -647,6 +689,40 @@ export const useGestureViewer = <ItemT, LC>({
647
689
  ],
648
690
  );
649
691
 
692
+ const emitSingleTap = useCallback((x: number, y: number) => {
693
+ const index = pendingIndexRef.current;
694
+ const currentData = dataRef.current;
695
+
696
+ if (index < 0 || index >= currentData.length) {
697
+ return;
698
+ }
699
+
700
+ managerRef.current?.emitTap({ kind: 'single', x, y, index });
701
+
702
+ const item = currentData[index];
703
+
704
+ if (item === undefined) {
705
+ return;
706
+ }
707
+
708
+ onSingleTapRef.current?.({ x, y, index, item });
709
+ }, []);
710
+
711
+ const singleTapGesture = useMemo(
712
+ () =>
713
+ Gesture.Tap()
714
+ .enabled(Platform.OS !== 'web')
715
+ .numberOfTaps(1)
716
+ .onEnd((event, success) => {
717
+ if (!success) {
718
+ return;
719
+ }
720
+
721
+ scheduleOnRN(emitSingleTap, event.x, event.y);
722
+ }),
723
+ [emitSingleTap],
724
+ );
725
+
650
726
  const doubleTapGesture = useMemo(
651
727
  () =>
652
728
  Gesture.Tap()
@@ -667,65 +743,71 @@ export const useGestureViewer = <ItemT, LC>({
667
743
  [enableDoubleTapZoom, height, maxZoomScale, scale, translateX, translateY, width],
668
744
  );
669
745
 
746
+ const tapGesture = useMemo(
747
+ () => Gesture.Exclusive(doubleTapGesture, singleTapGesture),
748
+ [doubleTapGesture, singleTapGesture],
749
+ );
750
+
670
751
  const zoomGesture = useMemo(
671
- () => Gesture.Race(zoomPinchGesture, Gesture.Exclusive(zoomPanGesture, doubleTapGesture)),
672
- [zoomPinchGesture, zoomPanGesture, doubleTapGesture],
752
+ () => Gesture.Race(zoomPinchGesture, Gesture.Exclusive(zoomPanGesture, tapGesture)),
753
+ [zoomPinchGesture, zoomPanGesture, tapGesture],
673
754
  );
674
755
 
675
756
  const animatedStyle = useAnimatedStyle(() => ({
676
- opacity: triggerOpacity.value,
757
+ opacity: triggerOpacity.get(),
677
758
  transform: [
678
- { translateX: triggerTranslateX.value },
679
- { translateY: triggerTranslateY.value },
680
- { scale: triggerScale.value },
681
-
682
- { translateY: translateY.value },
683
- { translateX: translateX.value },
684
- { scale: scale.value },
685
- { rotate: `${rotation.value}deg` },
759
+ { translateX: triggerTranslateX.get() },
760
+ { translateY: triggerTranslateY.get() },
761
+ { scale: triggerScale.get() },
762
+
763
+ { translateY: translateY.get() },
764
+ { translateX: translateX.get() },
765
+ { scale: scale.get() },
766
+ { rotate: `${rotation.get()}deg` },
686
767
  ],
687
768
  }));
688
769
 
689
770
  const backdropStyle = useAnimatedStyle(() => {
690
- const baseOpacity = triggerOpacity.value;
771
+ const baseOpacity = triggerOpacity.get();
691
772
 
692
- if (!dismissOptions.fadeBackdrop || scale.value !== 1) {
773
+ if (!dismissOptions.fadeBackdrop || scale.get() !== 1) {
693
774
  return { opacity: baseOpacity };
694
775
  }
695
776
 
696
- const dismissOpacity = interpolate(translateY.value, [0, 200], [1, 0], 'clamp');
777
+ const dismissDistance = getDismissDistance(translateY.get(), dismissOptions.direction);
778
+ const dismissOpacity = interpolate(dismissDistance, [0, 200], [1, 0], 'clamp');
697
779
 
698
780
  return { opacity: baseOpacity * dismissOpacity };
699
- }, [dismissOptions.fadeBackdrop]);
781
+ }, [dismissOptions.direction, dismissOptions.fadeBackdrop]);
700
782
 
701
783
  const nativeScrollGesture = useMemo(() => {
702
784
  return Gesture.Native().requireExternalGestureToFail(dismissGestureRef);
703
785
  }, []);
704
786
 
705
- const { onMomentumScrollEnd, onScroll, onScrollBeginDrag, onWebDoubleClick } =
706
- useGestureViewerPaging({
707
- adjustedInitialIndex,
708
- autoPlay,
709
- autoPlayInterval,
710
- currentIndex,
711
- dataLength,
712
- enableDoubleTapZoom,
713
- enableHorizontalSwipe,
714
- enableLoop,
715
- height,
716
- isRotated,
717
- isZoomed,
718
- itemSpacing,
719
- manager,
720
- maxZoomScale,
721
- scale,
722
- scrollTo,
723
- syncCurrentIndex,
724
- syncPendingIndex,
725
- translateX,
726
- translateY,
727
- width,
728
- });
787
+ const { onMomentumScrollEnd, onScroll, onScrollBeginDrag, onWebClick } = useGestureViewerPaging({
788
+ adjustedInitialIndex,
789
+ autoPlay,
790
+ autoPlayInterval,
791
+ currentIndex,
792
+ dataLength,
793
+ enableDoubleTapZoom,
794
+ enableHorizontalSwipe,
795
+ enableLoop,
796
+ height,
797
+ isRotated,
798
+ isZoomed,
799
+ itemSpacing,
800
+ manager,
801
+ maxZoomScale,
802
+ onSingleTap: emitSingleTap,
803
+ scale,
804
+ scrollTo,
805
+ syncCurrentIndex,
806
+ syncPendingIndex,
807
+ translateX,
808
+ translateY,
809
+ width,
810
+ });
729
811
 
730
812
  return {
731
813
  animatedStyle,
@@ -739,7 +821,7 @@ export const useGestureViewer = <ItemT, LC>({
739
821
  isZoomed,
740
822
  listRef,
741
823
  nativeScrollGesture,
742
- onWebDoubleClick,
824
+ onWebClick,
743
825
  onMomentumScrollEnd,
744
826
  onScroll,
745
827
 
@@ -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>(
@@ -123,6 +123,6 @@ export function useGestureViewerPaging({
123
123
  onMomentumScrollEnd,
124
124
  onScroll,
125
125
  onScrollBeginDrag,
126
- onWebDoubleClick: undefined,
126
+ onWebClick: undefined,
127
127
  };
128
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,6 +19,7 @@ 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;
@@ -27,9 +29,13 @@ export type UseGestureViewerPagingArgs = {
27
29
  width: number;
28
30
  };
29
31
 
32
+ export type WebClickTarget = {
33
+ getBoundingClientRect: () => { left: number; top: number };
34
+ };
35
+
30
36
  export type UseGestureViewerPagingResult = {
31
37
  onMomentumScrollEnd?: ScrollViewProps['onMomentumScrollEnd'];
32
38
  onScroll?: ScrollViewProps['onScroll'];
33
39
  onScrollBeginDrag?: ScrollViewProps['onScrollBeginDrag'];
34
- onWebDoubleClick?: (event: any) => void;
40
+ onWebClick?: (event: MouseEvent<WebClickTarget>) => void;
35
41
  };