react-native-gesture-image-viewer 2.0.0-beta.4 → 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.
@@ -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<
@@ -41,15 +41,20 @@ export const useGestureViewer = <T = any>({
41
41
  itemSpacing = 0,
42
42
  enableSnapMode = false,
43
43
  id = 'default',
44
+ onDismissStart,
45
+ triggerAnimation,
44
46
  }: UseGestureViewerProps<T>) => {
45
47
  const { width: screenWidth, height: screenHeight } = useWindowDimensions();
46
48
  const width = enableSnapMode ? customWidth || screenWidth : screenWidth;
47
49
 
48
50
  const [isZoomed, setIsZoomed] = useState(false);
49
51
  const [isRotated, setIsRotated] = useState(false);
52
+ const [shouldStartTriggerAnimation, setShouldStartTriggerAnimation] = useState(false);
50
53
  const [manager, setManager] = useState<GestureViewerManager | null>(null);
51
54
 
52
55
  const listRef = useRef<any>(null);
56
+ const triggerRectRef = useRef<TriggerRect | null>(null);
57
+ const onAnimationCompleteRef = useRef(triggerAnimation?.onAnimationComplete);
53
58
 
54
59
  const initialTranslateY = useSharedValue(0);
55
60
  const initialTranslateX = useSharedValue(0);
@@ -61,8 +66,22 @@ export const useGestureViewer = <T = any>({
61
66
  const backdropOpacity = useSharedValue(1);
62
67
  const rotation = useSharedValue(0);
63
68
 
69
+ const triggerScale = useSharedValue(1);
70
+ const triggerTranslateX = useSharedValue(0);
71
+ const triggerTranslateY = useSharedValue(0);
72
+ const triggerOpacity = useSharedValue(1);
73
+
64
74
  const dataLength = data?.length || 0;
65
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
+
66
85
  const dismissOptions = useMemo(() => {
67
86
  return {
68
87
  enabled: dismiss?.enabled ?? true,
@@ -112,6 +131,10 @@ export const useGestureViewer = <T = any>({
112
131
  [manager],
113
132
  );
114
133
 
134
+ const onAnimationComplete = useCallback(() => {
135
+ onAnimationCompleteRef.current?.();
136
+ }, []);
137
+
115
138
  useAnimatedReaction(
116
139
  () => scale.value,
117
140
  (currentScale, previousScale) => {
@@ -199,6 +222,105 @@ export const useGestureViewer = <T = any>({
199
222
  };
200
223
  }, [adjustedInitialIndex, translateY, backdropOpacity, translateX, scale, startScale, rotation, scrollTo]);
201
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
+
202
324
  const onMomentumScrollEnd = useCallback(
203
325
  (event: NativeSyntheticEvent<NativeScrollEvent>) => {
204
326
  if (!enableHorizontalSwipe) {
@@ -220,7 +342,7 @@ export const useGestureViewer = <T = any>({
220
342
  scrollTo(jumpToIndex, false);
221
343
  }
222
344
 
223
- const currentIndex = manager?.getState() ?? 0;
345
+ const currentIndex = manager?.getState()?.currentIndex;
224
346
 
225
347
  if (realIndex !== currentIndex && realIndex >= 0 && realIndex < dataLength) {
226
348
  if (manager) {
@@ -256,19 +378,21 @@ export const useGestureViewer = <T = any>({
256
378
  );
257
379
 
258
380
  const dismissGesture = useMemo(() => {
381
+ const canDismiss = !isZoomed && dismissOptions.enabled;
382
+
259
383
  return Gesture.Pan()
260
384
  .minDistance(10)
261
385
  .averageTouches(true)
262
386
  .activeCursor('grabbing')
263
387
  .activeOffsetY([-10, 10])
264
388
  .failOffsetX([-10, 10])
265
- .enabled(!isZoomed)
389
+ .enabled(canDismiss)
266
390
  .onUpdate((event) => {
267
391
  translateY.value = event.translationY / dismissOptions.resistance;
268
392
  })
269
393
  .onEnd((event) => {
270
- if (event.translationY > dismissOptions.threshold && dismissOptions.enabled && onDismiss) {
271
- runOnJS(onDismiss)();
394
+ if (canDismiss && event.translationY > dismissOptions.threshold) {
395
+ runOnJS(handleDismiss)();
272
396
  return;
273
397
  }
274
398
 
@@ -280,7 +404,7 @@ export const useGestureViewer = <T = any>({
280
404
  energyThreshold: 6e-9,
281
405
  });
282
406
  });
283
- }, [translateY, dismissOptions.threshold, dismissOptions.enabled, onDismiss, dismissOptions.resistance, isZoomed]);
407
+ }, [translateY, dismissOptions, handleDismiss, isZoomed]);
284
408
 
285
409
  const zoomPinchGesture = useMemo(() => {
286
410
  return Gesture.Pinch()
@@ -451,7 +575,12 @@ export const useGestureViewer = <T = any>({
451
575
 
452
576
  const animatedStyle = useAnimatedStyle(() => {
453
577
  return {
578
+ opacity: triggerOpacity.value,
454
579
  transform: [
580
+ { translateX: triggerTranslateX.value },
581
+ { translateY: triggerTranslateY.value },
582
+ { scale: triggerScale.value },
583
+
455
584
  { translateY: translateY.value },
456
585
  { translateX: translateX.value },
457
586
  { scale: scale.value },
@@ -461,13 +590,15 @@ export const useGestureViewer = <T = any>({
461
590
  });
462
591
 
463
592
  const backdropStyle = useAnimatedStyle(() => {
593
+ const baseOpacity = triggerOpacity.value;
594
+
464
595
  if (!dismissOptions.fadeBackdrop || scale.value !== 1) {
465
- return { opacity: 1 };
596
+ return { opacity: baseOpacity };
466
597
  }
467
598
 
468
- const opacity = interpolate(translateY.value, [0, 200], [1, 0], 'clamp');
599
+ const dismissOpacity = interpolate(translateY.value, [0, 200], [1, 0], 'clamp');
469
600
 
470
- return { opacity };
601
+ return { opacity: baseOpacity * dismissOpacity };
471
602
  }, [dismissOptions.fadeBackdrop]);
472
603
 
473
604
  const onScrollBeginDrag = useCallback(() => {
@@ -476,7 +607,6 @@ export const useGestureViewer = <T = any>({
476
607
 
477
608
  return {
478
609
  dataLength,
479
- translateY,
480
610
  listRef,
481
611
  isZoomed,
482
612
  isRotated,
@@ -485,6 +615,7 @@ export const useGestureViewer = <T = any>({
485
615
 
486
616
  onMomentumScrollEnd,
487
617
  onScrollBeginDrag,
618
+ handleDismiss,
488
619
 
489
620
  animatedStyle,
490
621
  backdropStyle,