react-native-gesture-image-viewer 2.0.0-beta.3 → 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.
Files changed (45) hide show
  1. package/README.md +43 -19
  2. package/lib/module/GestureTrigger.js +82 -0
  3. package/lib/module/GestureTrigger.js.map +1 -0
  4. package/lib/module/GestureViewer.js +14 -8
  5. package/lib/module/GestureViewer.js.map +1 -1
  6. package/lib/module/GestureViewerManager.js +5 -5
  7. package/lib/module/GestureViewerManager.js.map +1 -1
  8. package/lib/module/GestureViewerRegistry.js +11 -0
  9. package/lib/module/GestureViewerRegistry.js.map +1 -1
  10. package/lib/module/index.js +2 -0
  11. package/lib/module/index.js.map +1 -1
  12. package/lib/module/useGestureViewer.js +140 -71
  13. package/lib/module/useGestureViewer.js.map +1 -1
  14. package/lib/module/useGestureViewerController.js +17 -66
  15. package/lib/module/useGestureViewerController.js.map +1 -1
  16. package/lib/module/useGestureViewerState.js +78 -0
  17. package/lib/module/useGestureViewerState.js.map +1 -0
  18. package/lib/typescript/src/GestureTrigger.d.ts +55 -0
  19. package/lib/typescript/src/GestureTrigger.d.ts.map +1 -0
  20. package/lib/typescript/src/GestureViewer.d.ts +2 -2
  21. package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
  22. package/lib/typescript/src/GestureViewerManager.d.ts +5 -8
  23. package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
  24. package/lib/typescript/src/GestureViewerRegistry.d.ts +5 -0
  25. package/lib/typescript/src/GestureViewerRegistry.d.ts.map +1 -1
  26. package/lib/typescript/src/index.d.ts +4 -1
  27. package/lib/typescript/src/index.d.ts.map +1 -1
  28. package/lib/typescript/src/types.d.ts +116 -58
  29. package/lib/typescript/src/types.d.ts.map +1 -1
  30. package/lib/typescript/src/useGestureViewer.d.ts +9 -8
  31. package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
  32. package/lib/typescript/src/useGestureViewerController.d.ts +10 -21
  33. package/lib/typescript/src/useGestureViewerController.d.ts.map +1 -1
  34. package/lib/typescript/src/useGestureViewerState.d.ts +35 -0
  35. package/lib/typescript/src/useGestureViewerState.d.ts.map +1 -0
  36. package/package.json +1 -1
  37. package/src/GestureTrigger.tsx +86 -0
  38. package/src/GestureViewer.tsx +15 -9
  39. package/src/GestureViewerManager.ts +9 -9
  40. package/src/GestureViewerRegistry.ts +16 -0
  41. package/src/index.tsx +4 -1
  42. package/src/types.ts +116 -60
  43. package/src/useGestureViewer.ts +180 -77
  44. package/src/useGestureViewerController.ts +19 -76
  45. package/src/useGestureViewerState.ts +85 -0
@@ -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<
@@ -29,36 +29,32 @@ type UseGestureViewerProps<T = any> = Omit<
29
29
  export const useGestureViewer = <T = any>({
30
30
  data,
31
31
  initialIndex = 0,
32
- onIndexChange,
33
32
  onDismiss,
34
33
  width: customWidth,
35
- dismissThreshold = 80,
36
- resistance = 2,
37
- // swipeThreshold = 0.5,
38
- // velocityThreshold = 200,
39
- animateBackdrop = true,
40
- enableDismissGesture = true,
41
- enableSwipeGesture = true,
42
- enableZoomGesture = true,
43
- enableDoubleTapGesture = true,
44
- enableZoomPanGesture = true,
34
+ dismiss,
35
+ enableDoubleTapZoom = true,
36
+ enablePinchZoom = true,
37
+ enableHorizontalSwipe = true,
38
+ enablePanWhenZoomed = true,
45
39
  enableLoop = false,
46
40
  maxZoomScale = 2,
47
41
  itemSpacing = 0,
48
- useSnap = false,
42
+ enableSnapMode = false,
49
43
  id = 'default',
44
+ onDismissStart,
45
+ triggerAnimation,
50
46
  }: UseGestureViewerProps<T>) => {
51
47
  const { width: screenWidth, height: screenHeight } = useWindowDimensions();
52
- const width = useSnap ? customWidth || screenWidth : screenWidth;
48
+ const width = enableSnapMode ? customWidth || screenWidth : screenWidth;
53
49
 
54
50
  const [isZoomed, setIsZoomed] = useState(false);
55
51
  const [isRotated, setIsRotated] = useState(false);
52
+ const [shouldStartTriggerAnimation, setShouldStartTriggerAnimation] = useState(false);
56
53
  const [manager, setManager] = useState<GestureViewerManager | null>(null);
57
54
 
58
55
  const listRef = useRef<any>(null);
59
- const unsubscribeRef = useRef<(() => void) | null>(null);
60
- const onIndexChangeRef = useRef<((index: number) => void) | null>(null);
61
- const lastEmittedIndexRef = useRef<number>(null);
56
+ const triggerRectRef = useRef<TriggerRect | null>(null);
57
+ const onAnimationCompleteRef = useRef(triggerAnimation?.onAnimationComplete);
62
58
 
63
59
  const initialTranslateY = useSharedValue(0);
64
60
  const initialTranslateX = useSharedValue(0);
@@ -70,8 +66,31 @@ export const useGestureViewer = <T = any>({
70
66
  const backdropOpacity = useSharedValue(1);
71
67
  const rotation = useSharedValue(0);
72
68
 
69
+ const triggerScale = useSharedValue(1);
70
+ const triggerTranslateX = useSharedValue(0);
71
+ const triggerTranslateY = useSharedValue(0);
72
+ const triggerOpacity = useSharedValue(1);
73
+
73
74
  const dataLength = data?.length || 0;
74
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
+
85
+ const dismissOptions = useMemo(() => {
86
+ return {
87
+ enabled: dismiss?.enabled ?? true,
88
+ threshold: dismiss?.threshold ?? 80,
89
+ resistance: dismiss?.resistance ?? 2,
90
+ fadeBackdrop: dismiss?.fadeBackdrop ?? true,
91
+ };
92
+ }, [dismiss?.enabled, dismiss?.threshold, dismiss?.resistance, dismiss?.fadeBackdrop]);
93
+
75
94
  const adjustedInitialIndex = useMemo(() => {
76
95
  if (enableLoop && data.length > 1) {
77
96
  return initialIndex + 1;
@@ -96,22 +115,30 @@ export const useGestureViewer = <T = any>({
96
115
 
97
116
  const emitZoomChange = useCallback(
98
117
  (currentScale: number, prevScale: number | null) => {
99
- manager?.emitZoomChange(currentScale, prevScale);
118
+ if (manager) {
119
+ manager.emitZoomChange(currentScale, prevScale);
120
+ }
100
121
  },
101
122
  [manager],
102
123
  );
103
124
 
104
125
  const emitRotationChange = useCallback(
105
126
  (currentRotation: number, prevRotation: number | null) => {
106
- manager?.emitRotationChange(currentRotation, prevRotation);
127
+ if (manager) {
128
+ manager.emitRotationChange(currentRotation, prevRotation);
129
+ }
107
130
  },
108
131
  [manager],
109
132
  );
110
133
 
134
+ const onAnimationComplete = useCallback(() => {
135
+ onAnimationCompleteRef.current?.();
136
+ }, []);
137
+
111
138
  useAnimatedReaction(
112
139
  () => scale.value,
113
140
  (currentScale, previousScale) => {
114
- if (manager && currentScale !== previousScale) {
141
+ if (currentScale !== previousScale) {
115
142
  runOnJS(emitZoomChange)(currentScale, previousScale);
116
143
  }
117
144
 
@@ -122,7 +149,7 @@ export const useGestureViewer = <T = any>({
122
149
  useAnimatedReaction(
123
150
  () => rotation.value,
124
151
  (currentRotation, previousRotation) => {
125
- if (manager && currentRotation !== previousRotation) {
152
+ if (currentRotation !== previousRotation) {
126
153
  runOnJS(emitRotationChange)(currentRotation, previousRotation);
127
154
  }
128
155
 
@@ -131,41 +158,9 @@ export const useGestureViewer = <T = any>({
131
158
  );
132
159
 
133
160
  useEffect(() => {
134
- onIndexChangeRef.current = onIndexChange ?? null;
135
- });
161
+ const unsubscribe = registry.subscribeToManager(id, setManager);
136
162
 
137
- useEffect(() => {
138
- return () => {
139
- onIndexChangeRef.current = null;
140
- };
141
- }, []);
142
-
143
- useEffect(() => {
144
- const handleManagerChange = (manager: GestureViewerManager | null) => {
145
- unsubscribeRef.current?.();
146
- unsubscribeRef.current = null;
147
-
148
- setManager(manager);
149
-
150
- lastEmittedIndexRef.current = null;
151
-
152
- if (manager) {
153
- unsubscribeRef.current = manager.subscribe((state) => {
154
- if (lastEmittedIndexRef.current !== state.currentIndex) {
155
- lastEmittedIndexRef.current = state.currentIndex;
156
- onIndexChangeRef.current?.(state.currentIndex);
157
- }
158
- });
159
- return;
160
- }
161
- };
162
-
163
- const unsubscribeFromRegistry = registry.subscribeToManager(id, handleManagerChange);
164
-
165
- return () => {
166
- unsubscribeFromRegistry();
167
- unsubscribeRef.current?.();
168
- };
163
+ return unsubscribe;
169
164
  }, [id]);
170
165
 
171
166
  useEffect(() => {
@@ -174,7 +169,7 @@ export const useGestureViewer = <T = any>({
174
169
  }
175
170
 
176
171
  manager.setDataLength(dataLength);
177
- manager.setEnableSwipeGesture(enableSwipeGesture);
172
+ manager.setEnableHorizontalSwipe(enableHorizontalSwipe);
178
173
  manager.setCurrentIndex(initialIndex);
179
174
  manager.setWidth(width + itemSpacing);
180
175
  manager.setHeight(screenHeight);
@@ -184,7 +179,7 @@ export const useGestureViewer = <T = any>({
184
179
  manager.notifyStateChange();
185
180
  }, [
186
181
  dataLength,
187
- enableSwipeGesture,
182
+ enableHorizontalSwipe,
188
183
  initialIndex,
189
184
  manager,
190
185
  width,
@@ -227,9 +222,108 @@ export const useGestureViewer = <T = any>({
227
222
  };
228
223
  }, [adjustedInitialIndex, translateY, backdropOpacity, translateX, scale, startScale, rotation, scrollTo]);
229
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
+
230
324
  const onMomentumScrollEnd = useCallback(
231
325
  (event: NativeSyntheticEvent<NativeScrollEvent>) => {
232
- if (!enableSwipeGesture) {
326
+ if (!enableHorizontalSwipe) {
233
327
  return;
234
328
  }
235
329
 
@@ -248,7 +342,7 @@ export const useGestureViewer = <T = any>({
248
342
  scrollTo(jumpToIndex, false);
249
343
  }
250
344
 
251
- const currentIndex = manager?.getState() ?? 0;
345
+ const currentIndex = manager?.getState()?.currentIndex;
252
346
 
253
347
  if (realIndex !== currentIndex && realIndex >= 0 && realIndex < dataLength) {
254
348
  if (manager) {
@@ -271,7 +365,7 @@ export const useGestureViewer = <T = any>({
271
365
  dataLength,
272
366
  width,
273
367
  itemSpacing,
274
- enableSwipeGesture,
368
+ enableHorizontalSwipe,
275
369
  enableLoop,
276
370
  translateX,
277
371
  translateY,
@@ -284,19 +378,21 @@ export const useGestureViewer = <T = any>({
284
378
  );
285
379
 
286
380
  const dismissGesture = useMemo(() => {
381
+ const canDismiss = !isZoomed && dismissOptions.enabled;
382
+
287
383
  return Gesture.Pan()
288
384
  .minDistance(10)
289
385
  .averageTouches(true)
290
386
  .activeCursor('grabbing')
291
387
  .activeOffsetY([-10, 10])
292
388
  .failOffsetX([-10, 10])
293
- .enabled(!isZoomed)
389
+ .enabled(canDismiss)
294
390
  .onUpdate((event) => {
295
- translateY.value = event.translationY / resistance;
391
+ translateY.value = event.translationY / dismissOptions.resistance;
296
392
  })
297
393
  .onEnd((event) => {
298
- if (event.translationY > dismissThreshold && enableDismissGesture && onDismiss) {
299
- runOnJS(onDismiss)();
394
+ if (canDismiss && event.translationY > dismissOptions.threshold) {
395
+ runOnJS(handleDismiss)();
300
396
  return;
301
397
  }
302
398
 
@@ -308,11 +404,11 @@ export const useGestureViewer = <T = any>({
308
404
  energyThreshold: 6e-9,
309
405
  });
310
406
  });
311
- }, [translateY, dismissThreshold, enableDismissGesture, onDismiss, resistance, isZoomed]);
407
+ }, [translateY, dismissOptions, handleDismiss, isZoomed]);
312
408
 
313
409
  const zoomPinchGesture = useMemo(() => {
314
410
  return Gesture.Pinch()
315
- .enabled(enableZoomGesture)
411
+ .enabled(enablePinchZoom)
316
412
  .onBegin(() => {
317
413
  startScale.value = scale.value;
318
414
  initialTranslateX.value = translateX.value;
@@ -388,7 +484,7 @@ export const useGestureViewer = <T = any>({
388
484
  });
389
485
  }, [
390
486
  scale,
391
- enableZoomGesture,
487
+ enablePinchZoom,
392
488
  maxZoomScale,
393
489
  translateX,
394
490
  translateY,
@@ -402,7 +498,7 @@ export const useGestureViewer = <T = any>({
402
498
 
403
499
  const zoomPanGesture = useMemo(() => {
404
500
  return Gesture.Pan()
405
- .enabled(enableZoomPanGesture && isZoomed)
501
+ .enabled(enablePanWhenZoomed && isZoomed)
406
502
  .activeCursor('grabbing')
407
503
  .averageTouches(true)
408
504
  .onBegin(() => {
@@ -427,7 +523,7 @@ export const useGestureViewer = <T = any>({
427
523
  }, [
428
524
  translateX,
429
525
  translateY,
430
- enableZoomPanGesture,
526
+ enablePanWhenZoomed,
431
527
  isZoomed,
432
528
  scale,
433
529
  initialTranslateX,
@@ -437,7 +533,7 @@ export const useGestureViewer = <T = any>({
437
533
 
438
534
  const doubleTapGesture = useMemo(() => {
439
535
  return Gesture.Tap()
440
- .enabled(enableDoubleTapGesture)
536
+ .enabled(enableDoubleTapZoom)
441
537
  .numberOfTaps(2)
442
538
  .onEnd((event) => {
443
539
  const nextScale = scale.value > 1 ? 1 : maxZoomScale;
@@ -471,7 +567,7 @@ export const useGestureViewer = <T = any>({
471
567
  easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
472
568
  });
473
569
  });
474
- }, [scale, enableDoubleTapGesture, maxZoomScale, translateX, translateY, width, screenHeight]);
570
+ }, [scale, enableDoubleTapZoom, maxZoomScale, translateX, translateY, width, screenHeight]);
475
571
 
476
572
  const zoomGesture = useMemo(() => {
477
573
  return Gesture.Race(zoomPinchGesture, Gesture.Exclusive(zoomPanGesture, doubleTapGesture));
@@ -479,7 +575,12 @@ export const useGestureViewer = <T = any>({
479
575
 
480
576
  const animatedStyle = useAnimatedStyle(() => {
481
577
  return {
578
+ opacity: triggerOpacity.value,
482
579
  transform: [
580
+ { translateX: triggerTranslateX.value },
581
+ { translateY: triggerTranslateY.value },
582
+ { scale: triggerScale.value },
583
+
483
584
  { translateY: translateY.value },
484
585
  { translateX: translateX.value },
485
586
  { scale: scale.value },
@@ -489,14 +590,16 @@ export const useGestureViewer = <T = any>({
489
590
  });
490
591
 
491
592
  const backdropStyle = useAnimatedStyle(() => {
492
- if (!animateBackdrop || scale.value !== 1) {
493
- return { opacity: 1 };
593
+ const baseOpacity = triggerOpacity.value;
594
+
595
+ if (!dismissOptions.fadeBackdrop || scale.value !== 1) {
596
+ return { opacity: baseOpacity };
494
597
  }
495
598
 
496
- const opacity = interpolate(translateY.value, [0, 200], [1, 0], 'clamp');
599
+ const dismissOpacity = interpolate(translateY.value, [0, 200], [1, 0], 'clamp');
497
600
 
498
- return { opacity };
499
- }, [animateBackdrop]);
601
+ return { opacity: baseOpacity * dismissOpacity };
602
+ }, [dismissOptions.fadeBackdrop]);
500
603
 
501
604
  const onScrollBeginDrag = useCallback(() => {
502
605
  manager?.handleScrollBeginDrag();
@@ -504,7 +607,6 @@ export const useGestureViewer = <T = any>({
504
607
 
505
608
  return {
506
609
  dataLength,
507
- translateY,
508
610
  listRef,
509
611
  isZoomed,
510
612
  isRotated,
@@ -513,6 +615,7 @@ export const useGestureViewer = <T = any>({
513
615
 
514
616
  onMomentumScrollEnd,
515
617
  onScrollBeginDrag,
618
+ handleDismiss,
516
619
 
517
620
  animatedStyle,
518
621
  backdropStyle,
@@ -1,13 +1,13 @@
1
- import { useCallback, useMemo, useRef, useSyncExternalStore } from 'react';
1
+ import { useEffect, useMemo, useRef } from 'react';
2
2
  import type GestureViewerManager from './GestureViewerManager';
3
3
  import { registry } from './GestureViewerRegistry';
4
- import type { GestureViewerController, GestureViewerControllerState } from './types';
4
+ import type { GestureViewerController } from './types';
5
5
 
6
6
  /**
7
7
  * Hook to control the gesture viewer programmatically.
8
8
  *
9
9
  * @param id - Viewer instance identifier (default: 'default')
10
- * @returns Methods and state for controlling the viewer
10
+ * @returns Methods for controlling the viewer
11
11
  *
12
12
  * **Available methods:**
13
13
  * - `goToIndex(index: number)` - Navigate to specific index (0 to totalCount-1)
@@ -16,98 +16,46 @@ import type { GestureViewerController, GestureViewerControllerState } from './ty
16
16
  * - `zoomIn(multiplier?: number)` - Zoom in (default: 0.25)
17
17
  * - `zoomOut(multiplier?: number)` - Zoom out (default: 0.25)
18
18
  * - `resetZoom(scale?: number)` - Reset zoom level (default: 1.0)
19
- * - `rotate(angle?: 0|90|180|270|360, clockwise?: boolean)` - Rotate content (default: 90° clockwise)
20
- *
21
- * **Available state:**
22
- * - `currentIndex: number` - Current active index (read-only)
23
- * - `totalCount: number` - Total number of items (read-only)
19
+ * - `rotate(angle?: RotationAngle, clockwise?: boolean)` - Rotate content (default: 90° clockwise)
24
20
  *
25
21
  * @example
26
22
  * ```tsx
27
- * const { goToIndex, goToNext, goToPrevious, zoomIn, zoomOut, resetZoom, rotate, currentIndex, totalCount } = useGestureViewerController();
23
+ * const controller = useGestureViewerController();
28
24
  *
29
25
  * // Navigate to specific index
30
- * goToIndex(2);
26
+ * controller.goToIndex(2);
31
27
  *
32
28
  * // Go to next image
33
- * goToNext();
29
+ * controller.goToNext();
34
30
  *
35
31
  * // Zoom in by 25%
36
- * zoomIn();
32
+ * controller.zoomIn();
37
33
  *
38
34
  * // Zoom out by 50%
39
- * zoomOut(0.5);
35
+ * controller.zoomOut(0.5);
40
36
  *
41
37
  * // Reset to original size
42
- * resetZoom();
38
+ * controller.resetZoom();
43
39
  *
44
40
  * // Rotate 90 degrees clockwise
45
- * rotate();
41
+ * controller.rotate();
46
42
  *
47
43
  * // Rotate 180 degrees
48
- * rotate(180);
49
- *
50
- * // Check current state
51
- * console.log(`Image ${currentIndex + 1} of ${totalCount}`);
52
- *
53
- * // Check navigation availability
54
- * const canGoNext = currentIndex < totalCount - 1;
55
- * const canGoPrevious = currentIndex > 0;
44
+ * controller.rotate(180);
56
45
  * ```
57
46
  */
58
47
  export const useGestureViewerController = (id = 'default'): GestureViewerController => {
59
48
  const managerRef = useRef<GestureViewerManager | null>(null);
60
- const stateRef = useRef<GestureViewerControllerState>({ currentIndex: 0, totalCount: 0 });
61
-
62
- const updateState = useCallback((newState: GestureViewerControllerState, onStoreChange: () => void) => {
63
- if (
64
- stateRef.current.currentIndex !== newState.currentIndex ||
65
- stateRef.current.totalCount !== newState.totalCount
66
- ) {
67
- stateRef.current = {
68
- currentIndex: newState.currentIndex,
69
- totalCount: newState.totalCount,
70
- };
71
- onStoreChange();
72
- }
73
- }, []);
74
-
75
- const subscribe = useCallback(
76
- (onStoreChange: () => void) => {
77
- let unsubscribeFromManager: (() => void) | null = null;
78
-
79
- const unsubscribeFromRegistry = registry.subscribeToManager(id, (newManager) => {
80
- unsubscribeFromManager?.();
81
- unsubscribeFromManager = null;
82
- managerRef.current = newManager;
83
49
 
84
- if (newManager) {
85
- unsubscribeFromManager = newManager.subscribe((newState) => {
86
- updateState(newState, onStoreChange);
87
- });
50
+ useEffect(() => {
51
+ const unsubscribe = registry.subscribeToManager(id, (manager) => {
52
+ managerRef.current = manager;
53
+ });
88
54
 
89
- updateState(newManager.getState(), onStoreChange);
90
- return;
91
- }
55
+ return unsubscribe;
56
+ }, [id]);
92
57
 
93
- updateState({ currentIndex: 0, totalCount: 0 }, onStoreChange);
94
- });
95
-
96
- return () => {
97
- unsubscribeFromRegistry();
98
- unsubscribeFromManager?.();
99
- };
100
- },
101
- [id, updateState],
102
- );
103
-
104
- const getSnapshot = useCallback(() => {
105
- return stateRef.current;
106
- }, []);
107
-
108
- const state = useSyncExternalStore(subscribe, getSnapshot);
109
-
110
- const controller = useMemo<Omit<GestureViewerController, 'currentIndex' | 'totalCount'>>(
58
+ return useMemo<GestureViewerController>(
111
59
  () => ({
112
60
  goToIndex: (index) => {
113
61
  managerRef.current?.goToIndex(index);
@@ -133,9 +81,4 @@ export const useGestureViewerController = (id = 'default'): GestureViewerControl
133
81
  }),
134
82
  [],
135
83
  );
136
-
137
- return {
138
- ...controller,
139
- ...state,
140
- };
141
84
  };
@@ -0,0 +1,85 @@
1
+ import { useCallback, useRef, useSyncExternalStore } from 'react';
2
+ import { registry } from './GestureViewerRegistry';
3
+ import type { GestureViewerState } from './types';
4
+
5
+ /**
6
+ * Hook to access the current state of the gesture viewer.
7
+ *
8
+ * @param id - Viewer instance identifier (default: 'default')
9
+ * @returns Current state of the viewer
10
+ *
11
+ * **Available state:**
12
+ * - `currentIndex: number` - Current active index (read-only)
13
+ * - `totalCount: number` - Total number of items (read-only)
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const { currentIndex, totalCount } = useGestureViewerState();
18
+ *
19
+ * // Display current position
20
+ * return (
21
+ * <Text>
22
+ * {currentIndex + 1} / {totalCount}
23
+ * </Text>
24
+ * );
25
+ *
26
+ * // React to index changes
27
+ * useEffect(() => {
28
+ * console.log(`Moved to image ${currentIndex + 1}`);
29
+ * trackPageView(currentIndex);
30
+ * }, [currentIndex]);
31
+ *
32
+ * // Check navigation availability
33
+ * const canGoNext = currentIndex < totalCount - 1;
34
+ * const canGoPrevious = currentIndex > 0;
35
+ * ```
36
+ */
37
+ export const useGestureViewerState = (id = 'default'): GestureViewerState => {
38
+ const stateRef = useRef<GestureViewerState>({ currentIndex: 0, totalCount: 0 });
39
+
40
+ const updateState = useCallback((newState: GestureViewerState, onStoreChange: () => void) => {
41
+ if (
42
+ stateRef.current.currentIndex !== newState.currentIndex ||
43
+ stateRef.current.totalCount !== newState.totalCount
44
+ ) {
45
+ stateRef.current = { ...newState };
46
+ onStoreChange();
47
+ }
48
+ }, []);
49
+
50
+ const subscribe = useCallback(
51
+ (onStoreChange: () => void) => {
52
+ let unsubscribeFromManager: (() => void) | null = null;
53
+
54
+ const unsubscribeFromRegistry = registry.subscribeToManager(id, (newManager) => {
55
+ unsubscribeFromManager?.();
56
+ unsubscribeFromManager = null;
57
+
58
+ if (newManager) {
59
+ unsubscribeFromManager = newManager.subscribe((newState) => {
60
+ updateState(newState, onStoreChange);
61
+ });
62
+
63
+ updateState(newManager.getState(), onStoreChange);
64
+ return;
65
+ }
66
+
67
+ updateState({ currentIndex: 0, totalCount: 0 }, onStoreChange);
68
+ });
69
+
70
+ return () => {
71
+ unsubscribeFromRegistry();
72
+ unsubscribeFromManager?.();
73
+ };
74
+ },
75
+ [id, updateState],
76
+ );
77
+
78
+ const getSnapshot = useCallback(() => {
79
+ return stateRef.current;
80
+ }, []);
81
+
82
+ const state = useSyncExternalStore(subscribe, getSnapshot);
83
+
84
+ return state;
85
+ };