react-native-gesture-image-viewer 1.6.5 → 1.7.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.
@@ -18,7 +18,7 @@ import {
18
18
  } from 'react-native-reanimated';
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<
@@ -34,8 +34,6 @@ export const useGestureViewer = <T = any>({
34
34
  width: customWidth,
35
35
  dismissThreshold = 80,
36
36
  resistance = 2,
37
- // swipeThreshold = 0.5,
38
- // velocityThreshold = 200,
39
37
  animateBackdrop = true,
40
38
  enableDismissGesture = true,
41
39
  enableSwipeGesture = true,
@@ -47,6 +45,8 @@ export const useGestureViewer = <T = any>({
47
45
  itemSpacing = 0,
48
46
  useSnap = false,
49
47
  id = 'default',
48
+ onDismissStart,
49
+ triggerAnimation,
50
50
  }: UseGestureViewerProps<T>) => {
51
51
  const { width: screenWidth, height: screenHeight } = useWindowDimensions();
52
52
  const width = useSnap ? customWidth || screenWidth : screenWidth;
@@ -54,11 +54,14 @@ export const useGestureViewer = <T = any>({
54
54
  const [isZoomed, setIsZoomed] = useState(false);
55
55
  const [isRotated, setIsRotated] = useState(false);
56
56
  const [manager, setManager] = useState<GestureViewerManager | null>(null);
57
+ const [shouldStartTriggerAnimation, setShouldStartTriggerAnimation] = useState(false);
57
58
 
58
59
  const listRef = useRef<any>(null);
59
60
  const unsubscribeRef = useRef<(() => void) | null>(null);
60
61
  const onIndexChangeRef = useRef<((index: number) => void) | null>(null);
61
62
  const lastEmittedIndexRef = useRef<number>(null);
63
+ const triggerRectRef = useRef<TriggerRect | null>(null);
64
+ const onAnimationCompleteRef = useRef(triggerAnimation?.onAnimationComplete);
62
65
 
63
66
  const initialTranslateY = useSharedValue(0);
64
67
  const initialTranslateX = useSharedValue(0);
@@ -70,8 +73,22 @@ export const useGestureViewer = <T = any>({
70
73
  const backdropOpacity = useSharedValue(1);
71
74
  const rotation = useSharedValue(0);
72
75
 
76
+ const triggerScale = useSharedValue(1);
77
+ const triggerTranslateX = useSharedValue(0);
78
+ const triggerTranslateY = useSharedValue(0);
79
+ const triggerOpacity = useSharedValue(1);
80
+
73
81
  const dataLength = data?.length || 0;
74
82
 
83
+ const animationConfig = useMemo(
84
+ () => ({
85
+ duration: triggerAnimation?.duration ?? 300,
86
+ easing: triggerAnimation?.easing ?? Easing.bezier(0.25, 0.1, 0.25, 1.0),
87
+ reduceMotion: triggerAnimation?.reduceMotion,
88
+ }),
89
+ [triggerAnimation?.duration, triggerAnimation?.easing, triggerAnimation?.reduceMotion],
90
+ );
91
+
75
92
  const adjustedInitialIndex = useMemo(() => {
76
93
  if (enableLoop && data.length > 1) {
77
94
  return initialIndex + 1;
@@ -85,6 +102,10 @@ export const useGestureViewer = <T = any>({
85
102
  [width, screenHeight],
86
103
  );
87
104
 
105
+ const onAnimationComplete = useCallback(() => {
106
+ onAnimationCompleteRef.current?.();
107
+ }, []);
108
+
88
109
  const scrollTo = useCallback(
89
110
  (index: number, animated: boolean) => {
90
111
  const scrollAction = createScrollAction(listRef.current, width + itemSpacing);
@@ -140,6 +161,68 @@ export const useGestureViewer = <T = any>({
140
161
  };
141
162
  }, []);
142
163
 
164
+ useEffect(() => {
165
+ onAnimationCompleteRef.current = triggerAnimation?.onAnimationComplete;
166
+ }, [triggerAnimation?.onAnimationComplete]);
167
+
168
+ useEffect(() => {
169
+ if (shouldStartTriggerAnimation && triggerRectRef.current) {
170
+ const startX = triggerRectRef.current.x + triggerRectRef.current.width / 2 - screenWidth / 2;
171
+ const startY = triggerRectRef.current.y + triggerRectRef.current.height / 2 - screenHeight / 2;
172
+ const initialScaleFromTrigger = Math.min(
173
+ triggerRectRef.current.width / screenWidth,
174
+ triggerRectRef.current.height / screenHeight,
175
+ );
176
+
177
+ triggerScale.value = initialScaleFromTrigger;
178
+ triggerTranslateX.value = startX;
179
+ triggerTranslateY.value = startY;
180
+ triggerOpacity.value = 0;
181
+
182
+ triggerScale.value = withTiming(1, animationConfig, (finished) => {
183
+ if (finished) {
184
+ runOnJS(onAnimationComplete)();
185
+ }
186
+ });
187
+ triggerTranslateX.value = withTiming(0, animationConfig);
188
+ triggerTranslateY.value = withTiming(0, animationConfig);
189
+ triggerOpacity.value = withTiming(1, {
190
+ duration: animationConfig.duration / 2,
191
+ easing: animationConfig.easing,
192
+ reduceMotion: animationConfig.reduceMotion,
193
+ });
194
+
195
+ setShouldStartTriggerAnimation(false);
196
+ }
197
+ }, [
198
+ shouldStartTriggerAnimation,
199
+ animationConfig,
200
+ screenWidth,
201
+ screenHeight,
202
+ triggerOpacity,
203
+ triggerScale,
204
+ triggerTranslateX,
205
+ triggerTranslateY,
206
+ onAnimationComplete,
207
+ ]);
208
+
209
+ useEffect(() => {
210
+ const node = registry.getTriggerNode(id);
211
+
212
+ if (node && typeof node.measure === 'function') {
213
+ node.measure((_x, _y, width, height, pageX, pageY) => {
214
+ triggerRectRef.current = { x: pageX, y: pageY, width, height };
215
+ triggerOpacity.value = 0;
216
+ setShouldStartTriggerAnimation(true);
217
+ registry.clearTriggerNode(id);
218
+ });
219
+ }
220
+
221
+ return () => {
222
+ triggerRectRef.current = null;
223
+ };
224
+ }, [id, triggerOpacity]);
225
+
143
226
  useEffect(() => {
144
227
  const handleManagerChange = (manager: GestureViewerManager | null) => {
145
228
  unsubscribeRef.current?.();
@@ -227,6 +310,43 @@ export const useGestureViewer = <T = any>({
227
310
  };
228
311
  }, [adjustedInitialIndex, translateY, backdropOpacity, translateX, scale, startScale, rotation, scrollTo]);
229
312
 
313
+ const handleDismiss = useCallback(() => {
314
+ onDismissStart?.();
315
+
316
+ if (triggerRectRef.current) {
317
+ const endX = triggerRectRef.current.x + triggerRectRef.current.width / 2 - screenWidth / 2;
318
+ const endY = triggerRectRef.current.y + triggerRectRef.current.height / 2 - screenHeight / 2;
319
+ const endScale = Math.min(
320
+ triggerRectRef.current.width / screenWidth,
321
+ triggerRectRef.current.height / screenHeight,
322
+ );
323
+
324
+ triggerScale.value = withTiming(endScale, animationConfig);
325
+ triggerTranslateX.value = withTiming(endX, animationConfig);
326
+ triggerTranslateY.value = withTiming(endY, animationConfig);
327
+ triggerOpacity.value = withTiming(0, animationConfig, (finished) => {
328
+ if (finished && onDismiss) {
329
+ runOnJS(onDismiss)();
330
+ }
331
+ });
332
+ return;
333
+ }
334
+
335
+ if (onDismiss) {
336
+ runOnJS(onDismiss)();
337
+ }
338
+ }, [
339
+ animationConfig,
340
+ onDismiss,
341
+ onDismissStart,
342
+ screenWidth,
343
+ screenHeight,
344
+ triggerTranslateX,
345
+ triggerScale,
346
+ triggerTranslateY,
347
+ triggerOpacity,
348
+ ]);
349
+
230
350
  const onMomentumScrollEnd = useCallback(
231
351
  (event: NativeSyntheticEvent<NativeScrollEvent>) => {
232
352
  if (!enableSwipeGesture) {
@@ -248,7 +368,7 @@ export const useGestureViewer = <T = any>({
248
368
  scrollTo(jumpToIndex, false);
249
369
  }
250
370
 
251
- const currentIndex = manager?.getState() ?? 0;
371
+ const currentIndex = manager?.getState().currentIndex;
252
372
 
253
373
  if (realIndex !== currentIndex && realIndex >= 0 && realIndex < dataLength) {
254
374
  if (manager) {
@@ -284,19 +404,21 @@ export const useGestureViewer = <T = any>({
284
404
  );
285
405
 
286
406
  const dismissGesture = useMemo(() => {
407
+ const canDismiss = !isZoomed && enableDismissGesture;
408
+
287
409
  return Gesture.Pan()
288
410
  .minDistance(10)
289
411
  .averageTouches(true)
290
412
  .activeCursor('grabbing')
291
413
  .activeOffsetY([-10, 10])
292
414
  .failOffsetX([-10, 10])
293
- .enabled(!isZoomed)
415
+ .enabled(canDismiss)
294
416
  .onUpdate((event) => {
295
417
  translateY.value = event.translationY / resistance;
296
418
  })
297
419
  .onEnd((event) => {
298
- if (event.translationY > dismissThreshold && enableDismissGesture && onDismiss) {
299
- runOnJS(onDismiss)();
420
+ if (canDismiss && event.translationY > dismissThreshold && handleDismiss) {
421
+ runOnJS(handleDismiss)();
300
422
  return;
301
423
  }
302
424
 
@@ -305,7 +427,7 @@ export const useGestureViewer = <T = any>({
305
427
  stiffness: 150,
306
428
  });
307
429
  });
308
- }, [translateY, dismissThreshold, enableDismissGesture, onDismiss, resistance, isZoomed]);
430
+ }, [translateY, dismissThreshold, enableDismissGesture, handleDismiss, resistance, isZoomed]);
309
431
 
310
432
  const zoomPinchGesture = useMemo(() => {
311
433
  return Gesture.Pinch()
@@ -476,7 +598,12 @@ export const useGestureViewer = <T = any>({
476
598
 
477
599
  const animatedStyle = useAnimatedStyle(() => {
478
600
  return {
601
+ opacity: triggerOpacity.value,
479
602
  transform: [
603
+ { translateX: triggerTranslateX.value },
604
+ { translateY: triggerTranslateY.value },
605
+ { scale: triggerScale.value },
606
+
480
607
  { translateY: translateY.value },
481
608
  { translateX: translateX.value },
482
609
  { scale: scale.value },
@@ -486,13 +613,15 @@ export const useGestureViewer = <T = any>({
486
613
  });
487
614
 
488
615
  const backdropStyle = useAnimatedStyle(() => {
616
+ const baseOpacity = triggerOpacity.value;
617
+
489
618
  if (!animateBackdrop || scale.value !== 1) {
490
- return { opacity: 1 };
619
+ return { opacity: baseOpacity };
491
620
  }
492
621
 
493
- const opacity = interpolate(translateY.value, [0, 200], [1, 0], 'clamp');
622
+ const dismissOpacity = interpolate(translateY.value, [0, 200], [1, 0], 'clamp');
494
623
 
495
- return { opacity };
624
+ return { opacity: dismissOpacity * baseOpacity };
496
625
  }, [animateBackdrop]);
497
626
 
498
627
  const onScrollBeginDrag = useCallback(() => {
@@ -501,7 +630,6 @@ export const useGestureViewer = <T = any>({
501
630
 
502
631
  return {
503
632
  dataLength,
504
- translateY,
505
633
  listRef,
506
634
  isZoomed,
507
635
  isRotated,
@@ -510,6 +638,7 @@ export const useGestureViewer = <T = any>({
510
638
 
511
639
  onMomentumScrollEnd,
512
640
  onScrollBeginDrag,
641
+ handleDismiss,
513
642
 
514
643
  animatedStyle,
515
644
  backdropStyle,