react-native-gesture-image-viewer 2.3.0 → 2.3.2

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.
@@ -19,6 +19,7 @@ import { useGestureViewerPaging } from './useGestureViewerPaging';
19
19
  import { createBoundsConstraint, createScrollAction } from './utils';
20
20
  import { getDismissDistance, shouldDismissByDirection } from './utils/dismiss';
21
21
  import { applyTapZoomAtPoint } from './utils/tapZoom';
22
+ import { calculateFocalPointTranslation, shouldAcceptFocalPoint } from './utils/zoom';
22
23
 
23
24
  type UseGestureViewerProps<ItemT, LC> = Omit<
24
25
  GestureViewerProps<ItemT, LC>,
@@ -95,6 +96,9 @@ export const useGestureViewer = <ItemT, LC>({
95
96
 
96
97
  const lastFocalX = useSharedValue(0);
97
98
  const lastFocalY = useSharedValue(0);
99
+ const startFocalX = useSharedValue(0);
100
+ const startFocalY = useSharedValue(0);
101
+ const hasActiveFocal = useSharedValue(false);
98
102
 
99
103
  const dataLength = data?.length || 0;
100
104
 
@@ -147,13 +151,13 @@ export const useGestureViewer = <ItemT, LC>({
147
151
  );
148
152
 
149
153
  const resetTransformState = useCallback(() => {
150
- translateX.value = withTiming(0);
151
- translateY.value = withTiming(0);
152
- initialTranslateX.value = withTiming(0);
153
- initialTranslateY.value = withTiming(0);
154
- startScale.value = withTiming(1);
155
- scale.value = withTiming(1);
156
- rotation.value = 0;
154
+ translateX.set(withTiming(0));
155
+ translateY.set(withTiming(0));
156
+ initialTranslateX.set(withTiming(0));
157
+ initialTranslateY.set(withTiming(0));
158
+ startScale.set(withTiming(1));
159
+ scale.set(withTiming(1));
160
+ rotation.set(0);
157
161
  }, [initialTranslateX, initialTranslateY, rotation, scale, startScale, translateX, translateY]);
158
162
 
159
163
  const syncPendingIndex = useCallback(
@@ -201,7 +205,7 @@ export const useGestureViewer = <ItemT, LC>({
201
205
  }, []);
202
206
 
203
207
  useAnimatedReaction(
204
- () => scale.value,
208
+ () => scale.get(),
205
209
  (currentScale, previousScale) => {
206
210
  if (currentScale !== previousScale) {
207
211
  scheduleOnRN(emitZoomChange, currentScale, previousScale);
@@ -212,7 +216,7 @@ export const useGestureViewer = <ItemT, LC>({
212
216
  );
213
217
 
214
218
  useAnimatedReaction(
215
- () => rotation.value,
219
+ () => rotation.get(),
216
220
  (currentRotation, previousRotation) => {
217
221
  if (currentRotation !== previousRotation) {
218
222
  scheduleOnRN(emitRotationChange, currentRotation, previousRotation);
@@ -286,12 +290,12 @@ export const useGestureViewer = <ItemT, LC>({
286
290
  }, [manager]);
287
291
 
288
292
  useEffect(() => {
289
- translateY.value = 0;
290
- translateX.value = 0;
291
- scale.value = 1;
292
- backdropOpacity.value = 1;
293
- startScale.value = 1;
294
- rotation.value = 0;
293
+ translateY.set(0);
294
+ translateX.set(0);
295
+ scale.set(1);
296
+ backdropOpacity.set(1);
297
+ startScale.set(1);
298
+ rotation.set(0);
295
299
 
296
300
  if (adjustedInitialIndex <= 0 || !listRef.current) {
297
301
  return;
@@ -340,23 +344,27 @@ export const useGestureViewer = <ItemT, LC>({
340
344
  triggerRectRef.current.height / height,
341
345
  );
342
346
 
343
- triggerScale.value = initialScaleFromTrigger;
344
- triggerTranslateX.value = startX;
345
- triggerTranslateY.value = startY;
346
- triggerOpacity.value = 0;
347
+ triggerScale.set(initialScaleFromTrigger);
348
+ triggerTranslateX.set(startX);
349
+ triggerTranslateY.set(startY);
350
+ triggerOpacity.set(0);
347
351
 
348
- triggerScale.value = withTiming(1, animationConfig, (finished) => {
349
- if (finished) {
350
- scheduleOnRN(onAnimationComplete);
351
- }
352
- });
353
- triggerTranslateX.value = withTiming(0, animationConfig);
354
- triggerTranslateY.value = withTiming(0, animationConfig);
355
- triggerOpacity.value = withTiming(1, {
356
- duration: animationConfig.duration / 2,
357
- easing: animationConfig.easing,
358
- reduceMotion: animationConfig.reduceMotion,
359
- });
352
+ triggerScale.set(
353
+ withTiming(1, animationConfig, (finished) => {
354
+ if (finished) {
355
+ scheduleOnRN(onAnimationComplete);
356
+ }
357
+ }),
358
+ );
359
+ triggerTranslateX.set(withTiming(0, animationConfig));
360
+ triggerTranslateY.set(withTiming(0, animationConfig));
361
+ triggerOpacity.set(
362
+ withTiming(1, {
363
+ duration: animationConfig.duration / 2,
364
+ easing: animationConfig.easing,
365
+ reduceMotion: animationConfig.reduceMotion,
366
+ }),
367
+ );
360
368
 
361
369
  setShouldStartTriggerAnimation(false);
362
370
  }
@@ -391,7 +399,7 @@ export const useGestureViewer = <ItemT, LC>({
391
399
  }
392
400
 
393
401
  triggerRectRef.current = nextTriggerRect;
394
- triggerOpacity.value = 0;
402
+ triggerOpacity.set(0);
395
403
  setShouldStartTriggerAnimation(true);
396
404
  registry.clearActiveTriggerNode(id);
397
405
  });
@@ -409,14 +417,16 @@ export const useGestureViewer = <ItemT, LC>({
409
417
  const endY = rect.y + rect.height / 2 - height / 2;
410
418
  const endScale = Math.min(rect.width / width, rect.height / height);
411
419
 
412
- triggerScale.value = withTiming(endScale, animationConfig);
413
- triggerTranslateX.value = withTiming(endX, animationConfig);
414
- triggerTranslateY.value = withTiming(endY, animationConfig);
415
- triggerOpacity.value = withTiming(0, animationConfig, (finished) => {
416
- if (finished && onDismiss) {
417
- scheduleOnRN(onDismiss);
418
- }
419
- });
420
+ triggerScale.set(withTiming(endScale, animationConfig));
421
+ triggerTranslateX.set(withTiming(endX, animationConfig));
422
+ triggerTranslateY.set(withTiming(endY, animationConfig));
423
+ triggerOpacity.set(
424
+ withTiming(0, animationConfig, (finished) => {
425
+ if (finished && onDismiss) {
426
+ scheduleOnRN(onDismiss);
427
+ }
428
+ }),
429
+ );
420
430
  },
421
431
  [
422
432
  animationConfig,
@@ -486,7 +496,7 @@ export const useGestureViewer = <ItemT, LC>({
486
496
  .withRef(dismissGestureRef)
487
497
  .enabled(canDismiss)
488
498
  .onUpdate((event) => {
489
- translateY.value = event.translationY / dismissOptions.resistance;
499
+ translateY.set(event.translationY / dismissOptions.resistance);
490
500
  })
491
501
  .onEnd((event) => {
492
502
  if (
@@ -501,13 +511,15 @@ export const useGestureViewer = <ItemT, LC>({
501
511
  return;
502
512
  }
503
513
 
504
- translateY.value = withSpring(0, {
505
- damping: 50,
506
- energyThreshold: 6e-9,
507
- mass: 4,
508
- overshootClamping: false,
509
- stiffness: 600,
510
- });
514
+ translateY.set(
515
+ withSpring(0, {
516
+ damping: 50,
517
+ energyThreshold: 6e-9,
518
+ mass: 4,
519
+ overshootClamping: false,
520
+ stiffness: 600,
521
+ }),
522
+ );
511
523
  });
512
524
  }, [translateY, dismissOptions, handleDismiss, isZoomed]);
513
525
 
@@ -520,40 +532,66 @@ export const useGestureViewer = <ItemT, LC>({
520
532
  scheduleOnRN(setIsPinching, true);
521
533
  }
522
534
  })
523
- .onBegin((event) => {
524
- startScale.value = scale.value;
525
- initialTranslateX.value = translateX.value;
526
- initialTranslateY.value = translateY.value;
527
- lastFocalX.value = event.focalX;
528
- lastFocalY.value = event.focalY;
535
+ .onStart((event) => {
536
+ startScale.set(scale.get());
537
+ initialTranslateX.set(translateX.get());
538
+ initialTranslateY.set(translateY.get());
539
+ startFocalX.set(event.focalX);
540
+ startFocalY.set(event.focalY);
541
+ lastFocalX.set(event.focalX);
542
+ lastFocalY.set(event.focalY);
543
+ hasActiveFocal.set(false);
529
544
  })
530
545
  .onUpdate((event) => {
531
- const newScale = startScale.value * event.scale;
546
+ const initialScale = startScale.get();
547
+ const newScale = initialScale * event.scale;
532
548
 
533
- scale.value = newScale;
549
+ scale.set(newScale);
534
550
 
535
551
  if (newScale <= 1) {
536
- translateX.value = withTiming(0);
537
- translateY.value = withTiming(0);
552
+ translateX.set(withTiming(0));
553
+ translateY.set(withTiming(0));
538
554
  return;
539
555
  }
540
556
 
541
- const focalDeltaX = Math.abs(event.focalX - lastFocalX.value);
542
- const focalDeltaY = Math.abs(event.focalY - lastFocalY.value);
543
557
  const threshold = 50;
544
558
 
545
- if (focalDeltaX < threshold && focalDeltaY < threshold) {
546
- lastFocalX.value = event.focalX;
547
- lastFocalY.value = event.focalY;
559
+ if (
560
+ shouldAcceptFocalPoint({
561
+ focalX: event.focalX,
562
+ focalY: event.focalY,
563
+ hasActiveFocal: hasActiveFocal.get(),
564
+ lastFocalX: lastFocalX.get(),
565
+ lastFocalY: lastFocalY.get(),
566
+ threshold,
567
+ })
568
+ ) {
569
+ if (!hasActiveFocal.get()) {
570
+ startFocalX.set(event.focalX);
571
+ startFocalY.set(event.focalY);
572
+ }
573
+
574
+ lastFocalX.set(event.focalX);
575
+ lastFocalY.set(event.focalY);
576
+ hasActiveFocal.set(true);
548
577
  }
549
578
 
550
- const deltaScale = newScale - startScale.value;
551
- const centerX = lastFocalX.value - width / 2;
552
- const centerY = lastFocalY.value - height / 2;
553
-
554
- // NOTE 새로운 이동값 = 기존 이동값 - (중심점 거리 × 스케일 변화량) / 원래 스케일 (중심점이 화면 중심에서 멀수록, 확대 배율이 클수록 더 많이 이동)
555
- const newTranslateX = initialTranslateX.value - (centerX * deltaScale) / startScale.value;
556
- const newTranslateY = initialTranslateY.value - (centerY * deltaScale) / startScale.value;
579
+ const nextLastFocalX = lastFocalX.get();
580
+ const nextLastFocalY = lastFocalY.get();
581
+
582
+ const { translateX: newTranslateX, translateY: newTranslateY } =
583
+ calculateFocalPointTranslation({
584
+ currentFocalX: nextLastFocalX,
585
+ currentFocalY: nextLastFocalY,
586
+ height,
587
+ initialScale,
588
+ initialTranslateX: initialTranslateX.get(),
589
+ initialTranslateY: initialTranslateY.get(),
590
+ nextScale: newScale,
591
+ startFocalX: startFocalX.get(),
592
+ startFocalY: startFocalY.get(),
593
+ width,
594
+ });
557
595
 
558
596
  const { translateX: constrainedTranslateX, translateY: constrainedTranslateY } =
559
597
  constrainTranslation({
@@ -562,55 +600,63 @@ export const useGestureViewer = <ItemT, LC>({
562
600
  translateY: newTranslateY,
563
601
  });
564
602
 
565
- translateX.value = constrainedTranslateX;
566
- translateY.value = constrainedTranslateY;
603
+ translateX.set(constrainedTranslateX);
604
+ translateY.set(constrainedTranslateY);
567
605
  })
568
606
  .onEnd(() => {
569
- if (scale.value > maxZoomScale) {
570
- scale.value = withTiming(maxZoomScale, {
571
- duration: 300,
572
- easing: Easing.bezier(0.25, 0.1, 0.25, 1),
573
- });
607
+ const currentScale = scale.get();
608
+
609
+ if (currentScale > maxZoomScale) {
610
+ scale.set(
611
+ withTiming(maxZoomScale, {
612
+ duration: 300,
613
+ easing: Easing.bezier(0.25, 0.1, 0.25, 1),
614
+ }),
615
+ );
574
616
 
575
617
  const { translateX: constrainedTranslateX, translateY: constrainedTranslateY } =
576
618
  constrainTranslation({
577
619
  scale: maxZoomScale,
578
- translateX: translateX.value,
579
- translateY: translateY.value,
620
+ translateX: translateX.get(),
621
+ translateY: translateY.get(),
580
622
  });
581
623
 
582
- translateX.value = withTiming(constrainedTranslateX);
583
- translateY.value = withTiming(constrainedTranslateY);
624
+ translateX.set(withTiming(constrainedTranslateX));
625
+ translateY.set(withTiming(constrainedTranslateY));
584
626
 
585
627
  return;
586
628
  }
587
629
 
588
- if (scale.value < 1) {
589
- scale.value = withTiming(1, {
590
- duration: 300,
591
- easing: Easing.bezier(0.25, 0.1, 0.25, 1),
592
- });
593
- translateX.value = withTiming(0);
594
- translateY.value = withTiming(0);
595
- initialTranslateX.value = withTiming(0);
596
- initialTranslateY.value = withTiming(0);
630
+ if (currentScale < 1) {
631
+ scale.set(
632
+ withTiming(1, {
633
+ duration: 300,
634
+ easing: Easing.bezier(0.25, 0.1, 0.25, 1),
635
+ }),
636
+ );
637
+ translateX.set(withTiming(0));
638
+ translateY.set(withTiming(0));
639
+ initialTranslateX.set(withTiming(0));
640
+ initialTranslateY.set(withTiming(0));
641
+ hasActiveFocal.set(false);
597
642
  return;
598
643
  }
599
644
 
600
645
  const { translateX: constrainedTranslateX, translateY: constrainedTranslateY } =
601
646
  constrainTranslation({
602
- scale: scale.value,
603
- translateX: translateX.value,
604
- translateY: translateY.value,
647
+ scale: currentScale,
648
+ translateX: translateX.get(),
649
+ translateY: translateY.get(),
605
650
  });
606
651
 
607
- translateX.value = withTiming(constrainedTranslateX);
608
- translateY.value = withTiming(constrainedTranslateY);
652
+ translateX.set(withTiming(constrainedTranslateX));
653
+ translateY.set(withTiming(constrainedTranslateY));
609
654
  })
610
655
  .onTouchesUp(() => {
611
656
  scheduleOnRN(setIsPinching, false);
612
657
  })
613
658
  .onFinalize(() => {
659
+ hasActiveFocal.set(false);
614
660
  scheduleOnRN(setIsPinching, false);
615
661
  }),
616
662
  [
@@ -627,6 +673,9 @@ export const useGestureViewer = <ItemT, LC>({
627
673
  constrainTranslation,
628
674
  lastFocalX,
629
675
  lastFocalY,
676
+ startFocalX,
677
+ startFocalY,
678
+ hasActiveFocal,
630
679
  ],
631
680
  );
632
681
 
@@ -637,23 +686,25 @@ export const useGestureViewer = <ItemT, LC>({
637
686
  .activeCursor('grabbing')
638
687
  .averageTouches(true)
639
688
  .onBegin(() => {
640
- initialTranslateX.value = translateX.value;
641
- initialTranslateY.value = translateY.value;
689
+ initialTranslateX.set(translateX.get());
690
+ initialTranslateY.set(translateY.get());
642
691
  })
643
692
  .onUpdate((event) => {
644
- if (scale.value > 1) {
645
- const newTranslateX = initialTranslateX.value + event.translationX;
646
- const newTranslateY = initialTranslateY.value + event.translationY;
693
+ const currentScale = scale.get();
694
+
695
+ if (currentScale > 1) {
696
+ const newTranslateX = initialTranslateX.get() + event.translationX;
697
+ const newTranslateY = initialTranslateY.get() + event.translationY;
647
698
 
648
699
  const { translateX: constrainedTranslateX, translateY: constrainedTranslateY } =
649
700
  constrainTranslation({
650
- scale: scale.value,
701
+ scale: currentScale,
651
702
  translateX: newTranslateX,
652
703
  translateY: newTranslateY,
653
704
  });
654
705
 
655
- translateX.value = constrainedTranslateX;
656
- translateY.value = constrainedTranslateY;
706
+ translateX.set(constrainedTranslateX);
707
+ translateY.set(constrainedTranslateY);
657
708
  }
658
709
  }),
659
710
  [
@@ -733,27 +784,27 @@ export const useGestureViewer = <ItemT, LC>({
733
784
  );
734
785
 
735
786
  const animatedStyle = useAnimatedStyle(() => ({
736
- opacity: triggerOpacity.value,
787
+ opacity: triggerOpacity.get(),
737
788
  transform: [
738
- { translateX: triggerTranslateX.value },
739
- { translateY: triggerTranslateY.value },
740
- { scale: triggerScale.value },
741
-
742
- { translateY: translateY.value },
743
- { translateX: translateX.value },
744
- { scale: scale.value },
745
- { rotate: `${rotation.value}deg` },
789
+ { translateX: triggerTranslateX.get() },
790
+ { translateY: triggerTranslateY.get() },
791
+ { scale: triggerScale.get() },
792
+
793
+ { translateY: translateY.get() },
794
+ { translateX: translateX.get() },
795
+ { scale: scale.get() },
796
+ { rotate: `${rotation.get()}deg` },
746
797
  ],
747
798
  }));
748
799
 
749
800
  const backdropStyle = useAnimatedStyle(() => {
750
- const baseOpacity = triggerOpacity.value;
801
+ const baseOpacity = triggerOpacity.get();
751
802
 
752
- if (!dismissOptions.fadeBackdrop || scale.value !== 1) {
803
+ if (!dismissOptions.fadeBackdrop || scale.get() !== 1) {
753
804
  return { opacity: baseOpacity };
754
805
  }
755
806
 
756
- const dismissDistance = getDismissDistance(translateY.value, dismissOptions.direction);
807
+ const dismissDistance = getDismissDistance(translateY.get(), dismissOptions.direction);
757
808
  const dismissOpacity = interpolate(dismissDistance, [0, 200], [1, 0], 'clamp');
758
809
 
759
810
  return { opacity: baseOpacity * dismissOpacity };
@@ -22,7 +22,7 @@ export const applyTapZoomAtPoint = ({
22
22
  }) => {
23
23
  'worklet';
24
24
 
25
- const nextScale = scale.value > 1 ? 1 : maxZoomScale;
25
+ const nextScale = scale.get() > 1 ? 1 : maxZoomScale;
26
26
  const timingConfig = {
27
27
  duration: 300,
28
28
  easing: Easing.bezier(0.25, 0.1, 0.25, 1),
@@ -33,12 +33,12 @@ export const applyTapZoomAtPoint = ({
33
33
  const centerY = y - height / 2;
34
34
 
35
35
  // NOTE 확대로 밀려난 거리만큼 반대로 이동해서 탭 지점을 제자리에 유지
36
- translateX.value = withTiming(-centerX * (nextScale - 1), timingConfig);
37
- translateY.value = withTiming(-centerY * (nextScale - 1), timingConfig);
36
+ translateX.set(withTiming(-centerX * (nextScale - 1), timingConfig));
37
+ translateY.set(withTiming(-centerY * (nextScale - 1), timingConfig));
38
38
  } else {
39
- translateX.value = withTiming(0, timingConfig);
40
- translateY.value = withTiming(0, timingConfig);
39
+ translateX.set(withTiming(0, timingConfig));
40
+ translateY.set(withTiming(0, timingConfig));
41
41
  }
42
42
 
43
- scale.value = withTiming(nextScale, timingConfig);
43
+ scale.set(withTiming(nextScale, timingConfig));
44
44
  };
@@ -0,0 +1,60 @@
1
+ export const calculateFocalPointTranslation = ({
2
+ currentFocalX,
3
+ currentFocalY,
4
+ height,
5
+ initialScale,
6
+ initialTranslateX,
7
+ initialTranslateY,
8
+ nextScale,
9
+ startFocalX,
10
+ startFocalY,
11
+ width,
12
+ }: {
13
+ currentFocalX: number;
14
+ currentFocalY: number;
15
+ height: number;
16
+ initialScale: number;
17
+ initialTranslateX: number;
18
+ initialTranslateY: number;
19
+ nextScale: number;
20
+ startFocalX: number;
21
+ startFocalY: number;
22
+ width: number;
23
+ }) => {
24
+ 'worklet';
25
+
26
+ const scaleRatio = nextScale / initialScale;
27
+ const currentCenterX = currentFocalX - width / 2;
28
+ const currentCenterY = currentFocalY - height / 2;
29
+ const startCenterX = startFocalX - width / 2;
30
+ const startCenterY = startFocalY - height / 2;
31
+
32
+ return {
33
+ translateX: initialTranslateX * scaleRatio + currentCenterX - startCenterX * scaleRatio,
34
+ translateY: initialTranslateY * scaleRatio + currentCenterY - startCenterY * scaleRatio,
35
+ };
36
+ };
37
+
38
+ export const shouldAcceptFocalPoint = ({
39
+ focalX,
40
+ focalY,
41
+ hasActiveFocal,
42
+ lastFocalX,
43
+ lastFocalY,
44
+ threshold,
45
+ }: {
46
+ focalX: number;
47
+ focalY: number;
48
+ hasActiveFocal: boolean;
49
+ lastFocalX: number;
50
+ lastFocalY: number;
51
+ threshold: number;
52
+ }) => {
53
+ 'worklet';
54
+
55
+ if (!hasActiveFocal) {
56
+ return true;
57
+ }
58
+
59
+ return Math.abs(focalX - lastFocalX) < threshold && Math.abs(focalY - lastFocalY) < threshold;
60
+ };