react-native-gesture-image-viewer 2.4.0 → 3.0.0-beta.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 (138) hide show
  1. package/README.md +5 -3
  2. package/lib/module/GestureViewer.js +96 -147
  3. package/lib/module/GestureViewer.js.map +1 -1
  4. package/lib/module/GestureViewerManager.js +49 -105
  5. package/lib/module/GestureViewerManager.js.map +1 -1
  6. package/lib/module/gestureViewerAnimation.js +18 -0
  7. package/lib/module/gestureViewerAnimation.js.map +1 -0
  8. package/lib/module/gestureViewerGestures.js +12 -0
  9. package/lib/module/gestureViewerGestures.js.map +1 -0
  10. package/lib/module/gestureViewerPaging.js +66 -0
  11. package/lib/module/gestureViewerPaging.js.map +1 -0
  12. package/lib/module/gestureViewerRenderWindow.js +29 -0
  13. package/lib/module/gestureViewerRenderWindow.js.map +1 -0
  14. package/lib/module/getWebContentProps.js +6 -0
  15. package/lib/module/getWebContentProps.js.map +1 -0
  16. package/lib/module/getWebContentProps.web.js +8 -0
  17. package/lib/module/getWebContentProps.web.js.map +1 -0
  18. package/lib/module/platformTapGestures.js +4 -0
  19. package/lib/module/platformTapGestures.js.map +1 -0
  20. package/lib/module/platformTapGestures.web.js +4 -0
  21. package/lib/module/platformTapGestures.web.js.map +1 -0
  22. package/lib/module/renderWindow.js +160 -0
  23. package/lib/module/renderWindow.js.map +1 -0
  24. package/lib/module/useGestureViewer.js +459 -211
  25. package/lib/module/useGestureViewer.js.map +1 -1
  26. package/lib/module/useGestureViewerController.js +3 -3
  27. package/lib/module/useGestureViewerController.js.map +1 -1
  28. package/lib/module/useGestureViewerManagerBridge.js +85 -0
  29. package/lib/module/useGestureViewerManagerBridge.js.map +1 -0
  30. package/lib/module/useGestureViewerPaging.js +168 -79
  31. package/lib/module/useGestureViewerPaging.js.map +1 -1
  32. package/lib/module/useWebClickHandler.js +7 -0
  33. package/lib/module/useWebClickHandler.js.map +1 -0
  34. package/lib/module/useWebClickHandler.web.js +85 -0
  35. package/lib/module/useWebClickHandler.web.js.map +1 -0
  36. package/lib/module/useWebSingleTapTimer.js +12 -0
  37. package/lib/module/useWebSingleTapTimer.js.map +1 -0
  38. package/lib/module/useWebSingleTapTimer.web.js +25 -0
  39. package/lib/module/useWebSingleTapTimer.web.js.map +1 -0
  40. package/lib/module/utils/index.js +0 -77
  41. package/lib/module/utils/index.js.map +1 -1
  42. package/lib/module/utils/tapZoom.js +4 -3
  43. package/lib/module/utils/tapZoom.js.map +1 -1
  44. package/lib/typescript/src/GestureViewer.d.ts +1 -1
  45. package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
  46. package/lib/typescript/src/GestureViewerManager.d.ts +20 -20
  47. package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
  48. package/lib/typescript/src/gestureViewerAnimation.d.ts +15 -0
  49. package/lib/typescript/src/gestureViewerAnimation.d.ts.map +1 -0
  50. package/lib/typescript/src/gestureViewerGestures.d.ts +10 -0
  51. package/lib/typescript/src/gestureViewerGestures.d.ts.map +1 -0
  52. package/lib/typescript/src/gestureViewerPaging.d.ts +13 -0
  53. package/lib/typescript/src/gestureViewerPaging.d.ts.map +1 -0
  54. package/lib/typescript/src/gestureViewerRenderWindow.d.ts +15 -0
  55. package/lib/typescript/src/gestureViewerRenderWindow.d.ts.map +1 -0
  56. package/lib/typescript/src/getWebContentProps.d.ts +3 -0
  57. package/lib/typescript/src/getWebContentProps.d.ts.map +1 -0
  58. package/lib/typescript/src/getWebContentProps.web.d.ts +5 -0
  59. package/lib/typescript/src/getWebContentProps.web.d.ts.map +1 -0
  60. package/lib/typescript/src/platformTapGestures.d.ts +2 -0
  61. package/lib/typescript/src/platformTapGestures.d.ts.map +1 -0
  62. package/lib/typescript/src/platformTapGestures.web.d.ts +2 -0
  63. package/lib/typescript/src/platformTapGestures.web.d.ts.map +1 -0
  64. package/lib/typescript/src/renderWindow.d.ts +44 -0
  65. package/lib/typescript/src/renderWindow.d.ts.map +1 -0
  66. package/lib/typescript/src/types.d.ts +36 -53
  67. package/lib/typescript/src/types.d.ts.map +1 -1
  68. package/lib/typescript/src/useGestureViewer.d.ts +11 -10
  69. package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
  70. package/lib/typescript/src/useGestureViewerController.d.ts +1 -1
  71. package/lib/typescript/src/useGestureViewerManagerBridge.d.ts +28 -0
  72. package/lib/typescript/src/useGestureViewerManagerBridge.d.ts.map +1 -0
  73. package/lib/typescript/src/useGestureViewerPaging.d.ts +33 -2
  74. package/lib/typescript/src/useGestureViewerPaging.d.ts.map +1 -1
  75. package/lib/typescript/src/useWebClickHandler.d.ts +35 -0
  76. package/lib/typescript/src/useWebClickHandler.d.ts.map +1 -0
  77. package/lib/typescript/src/useWebClickHandler.web.d.ts +36 -0
  78. package/lib/typescript/src/useWebClickHandler.web.d.ts.map +1 -0
  79. package/lib/typescript/src/useWebSingleTapTimer.d.ts +6 -0
  80. package/lib/typescript/src/useWebSingleTapTimer.d.ts.map +1 -0
  81. package/lib/typescript/src/useWebSingleTapTimer.web.d.ts +6 -0
  82. package/lib/typescript/src/useWebSingleTapTimer.web.d.ts.map +1 -0
  83. package/lib/typescript/src/utils/index.d.ts +0 -20
  84. package/lib/typescript/src/utils/index.d.ts.map +1 -1
  85. package/lib/typescript/src/utils/tapZoom.d.ts.map +1 -1
  86. package/package.json +2 -2
  87. package/src/GestureViewer.tsx +113 -208
  88. package/src/GestureViewerManager.ts +77 -122
  89. package/src/gestureViewerAnimation.ts +18 -0
  90. package/src/gestureViewerGestures.ts +20 -0
  91. package/src/gestureViewerPaging.ts +116 -0
  92. package/src/gestureViewerRenderWindow.ts +56 -0
  93. package/src/getWebContentProps.ts +5 -0
  94. package/src/getWebContentProps.web.ts +5 -0
  95. package/src/platformTapGestures.ts +1 -0
  96. package/src/platformTapGestures.web.ts +1 -0
  97. package/src/renderWindow.ts +253 -0
  98. package/src/types.ts +34 -100
  99. package/src/useGestureViewer.ts +665 -275
  100. package/src/useGestureViewerController.ts +3 -3
  101. package/src/useGestureViewerManagerBridge.ts +118 -0
  102. package/src/useGestureViewerPaging.ts +306 -112
  103. package/src/useWebClickHandler.ts +44 -0
  104. package/src/useWebClickHandler.web.ts +146 -0
  105. package/src/useWebSingleTapTimer.ts +10 -0
  106. package/src/useWebSingleTapTimer.web.ts +30 -0
  107. package/src/utils/index.ts +0 -102
  108. package/src/utils/tapZoom.ts +4 -3
  109. package/lib/module/WebPagingFixStyle.js +0 -25
  110. package/lib/module/WebPagingFixStyle.js.map +0 -1
  111. package/lib/module/scheduleInitialScroll.js +0 -21
  112. package/lib/module/scheduleInitialScroll.js.map +0 -1
  113. package/lib/module/useGestureViewerPaging.types.js +0 -4
  114. package/lib/module/useGestureViewerPaging.types.js.map +0 -1
  115. package/lib/module/useGestureViewerPaging.web.js +0 -291
  116. package/lib/module/useGestureViewerPaging.web.js.map +0 -1
  117. package/lib/module/utils/FlashList.js +0 -9
  118. package/lib/module/utils/FlashList.js.map +0 -1
  119. package/lib/module/utils/webScroll.js +0 -70
  120. package/lib/module/utils/webScroll.js.map +0 -1
  121. package/lib/typescript/src/WebPagingFixStyle.d.ts +0 -6
  122. package/lib/typescript/src/WebPagingFixStyle.d.ts.map +0 -1
  123. package/lib/typescript/src/scheduleInitialScroll.d.ts +0 -3
  124. package/lib/typescript/src/scheduleInitialScroll.d.ts.map +0 -1
  125. package/lib/typescript/src/useGestureViewerPaging.types.d.ts +0 -42
  126. package/lib/typescript/src/useGestureViewerPaging.types.d.ts.map +0 -1
  127. package/lib/typescript/src/useGestureViewerPaging.web.d.ts +0 -3
  128. package/lib/typescript/src/useGestureViewerPaging.web.d.ts.map +0 -1
  129. package/lib/typescript/src/utils/FlashList.d.ts +0 -2
  130. package/lib/typescript/src/utils/FlashList.d.ts.map +0 -1
  131. package/lib/typescript/src/utils/webScroll.d.ts +0 -18
  132. package/lib/typescript/src/utils/webScroll.d.ts.map +0 -1
  133. package/src/WebPagingFixStyle.tsx +0 -25
  134. package/src/scheduleInitialScroll.ts +0 -36
  135. package/src/useGestureViewerPaging.types.ts +0 -42
  136. package/src/useGestureViewerPaging.web.ts +0 -453
  137. package/src/utils/FlashList.ts +0 -7
  138. package/src/utils/webScroll.ts +0 -96
@@ -1,8 +1,9 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { Platform, type View, useWindowDimensions } from 'react-native';
1
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
+ import { type View, useWindowDimensions } from 'react-native';
3
3
  import { Gesture, type GestureType } from 'react-native-gesture-handler';
4
4
  import {
5
5
  Easing,
6
+ cancelAnimation,
6
7
  interpolate,
7
8
  useAnimatedReaction,
8
9
  useAnimatedStyle,
@@ -12,28 +13,48 @@ import {
12
13
  } from 'react-native-reanimated';
13
14
  import { scheduleOnRN } from 'react-native-worklets';
14
15
 
16
+ import {
17
+ PAGE_SPRING_CONFIG,
18
+ SWIPE_THRESHOLD_RATIO,
19
+ SWIPE_VELOCITY_THRESHOLD,
20
+ } from './gestureViewerAnimation';
15
21
  import type GestureViewerManager from './GestureViewerManager';
22
+ import { normalizeHorizontalSwipeThreshold } from './gestureViewerPaging';
16
23
  import { registry } from './GestureViewerRegistry';
17
- import { scheduleInitialScroll } from './scheduleInitialScroll';
24
+ import { resolveGestureViewerRenderWindow } from './gestureViewerRenderWindow';
25
+ import { enableNativeTapGestures } from './platformTapGestures';
26
+ import {
27
+ type NavigationOptions,
28
+ getLogicalIndex,
29
+ getPageStride,
30
+ getVirtualIndexForLogicalIndex,
31
+ normalizePageSpacing,
32
+ clampIndex,
33
+ resolveNavigation,
34
+ shouldRunNavigationDuringTransition,
35
+ } from './renderWindow';
18
36
  import type { GestureViewerProps, TriggerRect } from './types';
37
+ import { useGestureViewerManagerBridge } from './useGestureViewerManagerBridge';
19
38
  import { useGestureViewerPaging } from './useGestureViewerPaging';
20
- import { createBoundsConstraint, createScrollAction } from './utils';
39
+ import { type EmitSingleTap, useWebClickHandler } from './useWebClickHandler';
40
+ import { useWebSingleTapTimer } from './useWebSingleTapTimer';
41
+ import { createBoundsConstraint } from './utils';
21
42
  import { getDismissDistance, shouldDismissByDirection } from './utils/dismiss';
22
43
  import { applyTapZoomAtPoint } from './utils/tapZoom';
23
44
  import { calculateFocalPointTranslation, shouldAcceptFocalPoint } from './utils/zoom';
24
45
 
25
- type UseGestureViewerProps<ItemT, LC> = Omit<
26
- GestureViewerProps<ItemT, LC>,
27
- | 'renderItem'
28
- | 'renderContainer'
29
- | 'ListComponent'
30
- | 'listProps'
31
- | 'containerStyle'
32
- | 'backdropStyle'
33
- | 'enableSnapMode'
46
+ const NATIVE_TAP_MAX_DISTANCE = 10;
47
+
48
+ const isValidTriggerRect = (rect: TriggerRect | null): rect is TriggerRect => {
49
+ return !!rect && rect.width > 0 && rect.height > 0;
50
+ };
51
+
52
+ type UseGestureViewerProps<ItemT> = Omit<
53
+ GestureViewerProps<ItemT>,
54
+ 'renderItem' | 'renderContainer' | 'containerStyle' | 'backdropStyle'
34
55
  >;
35
56
 
36
- export const useGestureViewer = <ItemT, LC>({
57
+ export const useGestureViewer = <ItemT>({
37
58
  data,
38
59
  initialIndex = 0,
39
60
  onDismiss,
@@ -42,21 +63,35 @@ export const useGestureViewer = <ItemT, LC>({
42
63
  dismiss,
43
64
  enableDoubleTapZoom = true,
44
65
  enablePinchZoom = true,
45
- enableHorizontalSwipe = true,
66
+ horizontalSwipe,
46
67
  enablePanWhenZoomed = true,
47
68
  enableLoop = false,
48
69
  maxZoomScale = 2,
49
- itemSpacing = 0,
70
+ pageSpacing = 0,
71
+ windowSize = 3,
50
72
  height: customHeight,
51
73
  id = 'default',
52
74
  onDismissStart,
53
75
  triggerAnimation,
54
76
  autoPlay = false,
55
77
  autoPlayInterval = 3000,
56
- }: UseGestureViewerProps<ItemT, LC>) => {
78
+ }: UseGestureViewerProps<ItemT>) => {
57
79
  const { width: screenWidth, height: screenHeight } = useWindowDimensions();
58
80
  const width = customWidth || screenWidth;
59
81
  const height = customHeight || screenHeight;
82
+ const dataLength = data?.length || 0;
83
+ const normalizedPageSpacing = normalizePageSpacing(pageSpacing);
84
+ const pageStride = getPageStride(width, normalizedPageSpacing);
85
+ const initialCurrentIndex = clampIndex(initialIndex, dataLength);
86
+ const horizontalSwipeEnabled = horizontalSwipe?.enabled ?? true;
87
+ const horizontalSwipeDistanceThresholdRatio = normalizeHorizontalSwipeThreshold(
88
+ horizontalSwipe?.distanceThresholdRatio,
89
+ SWIPE_THRESHOLD_RATIO,
90
+ );
91
+ const horizontalSwipeVelocityThreshold = normalizeHorizontalSwipeThreshold(
92
+ horizontalSwipe?.velocityThreshold,
93
+ SWIPE_VELOCITY_THRESHOLD,
94
+ );
60
95
 
61
96
  const dismissGestureRef = useRef<GestureType>(undefined);
62
97
 
@@ -64,21 +99,30 @@ export const useGestureViewer = <ItemT, LC>({
64
99
  const [isRotated, setIsRotated] = useState(false);
65
100
  const [isPinching, setIsPinching] = useState(false);
66
101
  const [shouldStartTriggerAnimation, setShouldStartTriggerAnimation] = useState(false);
67
- const [manager, setManager] = useState<GestureViewerManager | null>(null);
68
- const [currentIndex, setCurrentIndex] = useState(initialIndex);
69
- const [activeTriggerNode, setActiveTriggerNode] = useState<View | null>(null);
102
+ const [currentIndex, setCurrentIndex] = useState(initialCurrentIndex);
103
+ const [centerVirtualIndex, setCenterVirtualIndex] = useState(initialCurrentIndex);
104
+ const [activeTriggerNode, setActiveTriggerNode] = useState<View | null>(() =>
105
+ registry.getActiveTriggerNode(id),
106
+ );
107
+ const [isTriggerOpening, setIsTriggerOpening] = useState(
108
+ () => !!registry.getActiveTriggerNode(id),
109
+ );
70
110
 
71
- const listRef = useRef<any>(null);
72
111
  const triggerRectRef = useRef<TriggerRect | null>(null);
73
- const pendingIndexRef = useRef(initialIndex);
112
+ const pendingIndexRef = useRef(initialCurrentIndex);
113
+ const currentIndexRef = useRef(initialCurrentIndex);
114
+ const centerVirtualIndexRef = useRef(initialCurrentIndex);
115
+ const previousInitialIndexRef = useRef(initialIndex);
116
+ const hasResolvedInitialIndexRef = useRef(dataLength > 0);
74
117
  const onAnimationCompleteRef = useRef(triggerAnimation?.onAnimationComplete);
75
118
  const onSingleTapRef = useRef(onSingleTap);
76
119
  const dataRef = useRef(data);
77
- const managerRef = useRef(manager);
78
-
79
- const isValidTriggerRect = useCallback((rect: TriggerRect | null): rect is TriggerRect => {
80
- return !!rect && rect.width > 0 && rect.height > 0;
81
- }, []);
120
+ const dataLengthRef = useRef(dataLength);
121
+ const enableLoopRef = useRef(enableLoop);
122
+ const managerRef = useRef<GestureViewerManager | null>(null);
123
+ const isZoomedRef = useRef(isZoomed);
124
+ const isRotatedRef = useRef(isRotated);
125
+ const isPinchingRef = useRef(isPinching);
82
126
 
83
127
  const initialTranslateY = useSharedValue(0);
84
128
  const initialTranslateX = useSharedValue(0);
@@ -87,22 +131,24 @@ export const useGestureViewer = <ItemT, LC>({
87
131
  const translateY = useSharedValue(0);
88
132
  const translateX = useSharedValue(0);
89
133
  const scale = useSharedValue(1);
90
- const backdropOpacity = useSharedValue(1);
91
134
  const rotation = useSharedValue(0);
92
135
 
93
136
  const triggerScale = useSharedValue(1);
94
137
  const triggerTranslateX = useSharedValue(0);
95
138
  const triggerTranslateY = useSharedValue(0);
96
- const triggerOpacity = useSharedValue(1);
139
+ const triggerOpacity = useSharedValue(activeTriggerNode ? 0 : 1);
140
+
141
+ const hasZoomChangeListeners = useSharedValue(false);
142
+ const hasRotationChangeListeners = useSharedValue(false);
97
143
 
98
144
  const lastFocalX = useSharedValue(0);
99
145
  const lastFocalY = useSharedValue(0);
100
146
  const startFocalX = useSharedValue(0);
101
147
  const startFocalY = useSharedValue(0);
102
148
  const hasActiveFocal = useSharedValue(false);
103
-
104
- const dataLength = data?.length || 0;
105
- const previousDataLengthRef = useRef(dataLength);
149
+ const nativeInteractionHadMultipleTouches = useSharedValue(false);
150
+ const suppressNativeTap = useSharedValue(false);
151
+ const { clearPendingWebSingleTap, scheduleWebSingleTap } = useWebSingleTapTimer();
106
152
 
107
153
  const animationConfig = useMemo(
108
154
  () => ({
@@ -130,28 +176,24 @@ export const useGestureViewer = <ItemT, LC>({
130
176
  ],
131
177
  );
132
178
 
133
- const adjustedInitialIndex = useMemo(() => {
134
- if (enableLoop && dataLength > 1) {
135
- return initialIndex + 1;
136
- }
137
-
138
- return initialIndex;
139
- }, [enableLoop, dataLength, initialIndex]);
179
+ const {
180
+ centerVirtualIndex: renderCenterVirtualIndex,
181
+ currentIndex: renderCurrentIndex,
182
+ slots: renderWindowSlots,
183
+ } = resolveGestureViewerRenderWindow({
184
+ centerVirtualIndex,
185
+ currentIndex,
186
+ data,
187
+ enableLoop,
188
+ isTriggerOpening,
189
+ windowSize,
190
+ });
140
191
 
141
192
  const constrainTranslation = useMemo(
142
193
  () => createBoundsConstraint({ height, width }),
143
194
  [width, height],
144
195
  );
145
196
 
146
- const scrollTo = useCallback(
147
- (index: number, animated: boolean) => {
148
- const scrollAction = createScrollAction(listRef.current, width + itemSpacing);
149
-
150
- return scrollAction.scrollTo(index, animated);
151
- },
152
- [width, itemSpacing],
153
- );
154
-
155
197
  const resetTransformState = useCallback(() => {
156
198
  translateX.set(withTiming(0));
157
199
  translateY.set(withTiming(0));
@@ -162,38 +204,180 @@ export const useGestureViewer = <ItemT, LC>({
162
204
  rotation.set(0);
163
205
  }, [initialTranslateX, initialTranslateY, rotation, scale, startScale, translateX, translateY]);
164
206
 
165
- const syncPendingIndex = useCallback(
166
- (nextIndex: number) => {
167
- if (nextIndex < 0 || nextIndex >= dataLength) {
207
+ const resetTransformStateImmediately = useCallback(() => {
208
+ cancelAnimation(translateX);
209
+ cancelAnimation(translateY);
210
+ cancelAnimation(initialTranslateX);
211
+ cancelAnimation(initialTranslateY);
212
+ cancelAnimation(startScale);
213
+ cancelAnimation(scale);
214
+ cancelAnimation(rotation);
215
+ translateX.set(0);
216
+ translateY.set(0);
217
+ initialTranslateX.set(0);
218
+ initialTranslateY.set(0);
219
+ startScale.set(1);
220
+ scale.set(1);
221
+ rotation.set(0);
222
+ }, [initialTranslateX, initialTranslateY, rotation, scale, startScale, translateX, translateY]);
223
+
224
+ const commitCurrentIndex = useCallback((nextIndex: number) => {
225
+ pendingIndexRef.current = nextIndex;
226
+ currentIndexRef.current = nextIndex;
227
+ setCurrentIndex(nextIndex);
228
+ managerRef.current?.notifyStateChange();
229
+ }, []);
230
+
231
+ const commitVirtualIndexOnly = useCallback(
232
+ (nextVirtualIndex: number) => {
233
+ const nextLogicalIndex = getLogicalIndex(
234
+ nextVirtualIndex,
235
+ dataLengthRef.current,
236
+ enableLoopRef.current,
237
+ );
238
+
239
+ if (nextLogicalIndex === null) {
168
240
  return;
169
241
  }
170
242
 
171
- pendingIndexRef.current = nextIndex;
243
+ centerVirtualIndexRef.current = nextVirtualIndex;
244
+ setCenterVirtualIndex(nextVirtualIndex);
245
+ commitCurrentIndex(nextLogicalIndex);
172
246
  },
173
- [dataLength],
247
+ [commitCurrentIndex],
174
248
  );
175
249
 
176
- const syncCurrentIndex = useCallback(
177
- (nextIndex: number) => {
178
- if (!manager || nextIndex < 0 || nextIndex >= dataLength) {
250
+ const {
251
+ animateToVirtualPage,
252
+ horizontalPagingGesture,
253
+ isPageTransitioningRef,
254
+ pageTransitionLocked,
255
+ snapToVirtualPage,
256
+ visualPage,
257
+ } = useGestureViewerPaging({
258
+ centerVirtualIndex,
259
+ clearPendingWebSingleTap,
260
+ commitVirtualIndexOnly,
261
+ currentIndex,
262
+ dataLength,
263
+ enableLoop,
264
+ horizontalSwipeDistanceThresholdRatio,
265
+ horizontalSwipeEnabled,
266
+ horizontalSwipeVelocityThreshold,
267
+ initialPage: initialCurrentIndex,
268
+ isPinching,
269
+ isRotated,
270
+ isTriggerOpening,
271
+ isZoomed,
272
+ pageStride,
273
+ suppressNativeTap,
274
+ width,
275
+ });
276
+
277
+ const navigateToIndex = useCallback(
278
+ (targetIndex: number, options?: NavigationOptions, preferredDirection?: -1 | 1) => {
279
+ const resolution = resolveNavigation({
280
+ currentIndex: currentIndexRef.current,
281
+ dataLength: dataLengthRef.current,
282
+ enableLoop: enableLoopRef.current,
283
+ preferredDirection,
284
+ targetIndex,
285
+ });
286
+
287
+ if (resolution.kind === 'noop') {
179
288
  return;
180
289
  }
181
290
 
182
- pendingIndexRef.current = nextIndex;
291
+ if (
292
+ isPageTransitioningRef.current &&
293
+ !shouldRunNavigationDuringTransition(resolution, options)
294
+ ) {
295
+ return;
296
+ }
297
+
298
+ const targetVirtualIndex =
299
+ resolution.kind === 'step'
300
+ ? centerVirtualIndexRef.current + resolution.direction
301
+ : getVirtualIndexForLogicalIndex(
302
+ resolution.targetIndex,
303
+ centerVirtualIndexRef.current,
304
+ dataLengthRef.current,
305
+ enableLoopRef.current,
306
+ );
183
307
 
184
- const managerCurrentIndex = manager.getState().currentIndex;
308
+ if (targetVirtualIndex === null) {
309
+ return;
310
+ }
185
311
 
186
- if (nextIndex === managerCurrentIndex) {
312
+ if (resolution.kind !== 'step' || options?.animated === false) {
313
+ resetTransformStateImmediately();
314
+ snapToVirtualPage(targetVirtualIndex);
187
315
  return;
188
316
  }
189
317
 
190
- manager.setCurrentIndex(nextIndex);
191
- manager.notifyStateChange();
192
318
  resetTransformState();
319
+ animateToVirtualPage(targetVirtualIndex);
193
320
  },
194
- [dataLength, manager, resetTransformState],
321
+ [
322
+ animateToVirtualPage,
323
+ isPageTransitioningRef,
324
+ resetTransformStateImmediately,
325
+ resetTransformState,
326
+ snapToVirtualPage,
327
+ ],
195
328
  );
196
329
 
330
+ const navigateByDirection = useCallback(
331
+ (direction: -1 | 1) => {
332
+ const currentDataLength = dataLengthRef.current;
333
+
334
+ if (currentDataLength <= 0) {
335
+ return;
336
+ }
337
+
338
+ const nextIndex = currentIndexRef.current + direction;
339
+
340
+ if (nextIndex >= 0 && nextIndex < currentDataLength) {
341
+ navigateToIndex(nextIndex, undefined, direction);
342
+ return;
343
+ }
344
+
345
+ if (!enableLoopRef.current || currentDataLength <= 1) {
346
+ return;
347
+ }
348
+
349
+ navigateToIndex(direction === 1 ? 0 : currentDataLength - 1, undefined, direction);
350
+ },
351
+ [navigateToIndex],
352
+ );
353
+
354
+ const navigateToNext = useCallback(() => {
355
+ navigateByDirection(1);
356
+ }, [navigateByDirection]);
357
+
358
+ const navigateToPrevious = useCallback(() => {
359
+ navigateByDirection(-1);
360
+ }, [navigateByDirection]);
361
+
362
+ useGestureViewerManagerBridge({
363
+ currentIndexRef,
364
+ dataLengthRef,
365
+ goToIndex: navigateToIndex,
366
+ goToNext: navigateToNext,
367
+ goToPrevious: navigateToPrevious,
368
+ hasRotationChangeListeners,
369
+ hasZoomChangeListeners,
370
+ height,
371
+ id,
372
+ managerRef,
373
+ maxZoomScale,
374
+ rotation,
375
+ scale,
376
+ translateX,
377
+ translateY,
378
+ width,
379
+ });
380
+
197
381
  const emitZoomChange = useCallback((currentScale: number, prevScale: number | null) => {
198
382
  managerRef.current?.emitZoomChange(currentScale, prevScale);
199
383
  }, []);
@@ -202,146 +386,203 @@ export const useGestureViewer = <ItemT, LC>({
202
386
  managerRef.current?.emitRotationChange(currentRotation, prevRotation);
203
387
  }, []);
204
388
 
205
- const onAnimationComplete = useCallback(() => {
389
+ const finishTriggerOpening = useCallback(() => {
390
+ setIsTriggerOpening(false);
206
391
  onAnimationCompleteRef.current?.();
207
392
  }, []);
208
393
 
209
394
  useAnimatedReaction(
210
395
  () => scale.get(),
211
396
  (currentScale, previousScale) => {
212
- if (currentScale !== previousScale) {
397
+ if (currentScale !== previousScale && hasZoomChangeListeners.get()) {
213
398
  scheduleOnRN(emitZoomChange, currentScale, previousScale);
214
399
  }
215
400
 
216
- scheduleOnRN(setIsZoomed, currentScale > 1);
401
+ const currentIsZoomed = currentScale > 1;
402
+ const previousIsZoomed = (previousScale ?? 1) > 1;
403
+
404
+ if (previousScale === null || currentIsZoomed !== previousIsZoomed) {
405
+ scheduleOnRN(setIsZoomed, currentIsZoomed);
406
+ }
217
407
  },
218
408
  );
219
409
 
220
410
  useAnimatedReaction(
221
411
  () => rotation.get(),
222
412
  (currentRotation, previousRotation) => {
223
- if (currentRotation !== previousRotation) {
413
+ if (currentRotation !== previousRotation && hasRotationChangeListeners.get()) {
224
414
  scheduleOnRN(emitRotationChange, currentRotation, previousRotation);
225
415
  }
226
416
 
227
- scheduleOnRN(setIsRotated, currentRotation % 360 !== 0);
417
+ const currentIsRotated = currentRotation % 360 !== 0;
418
+ const previousIsRotated = (previousRotation ?? 0) % 360 !== 0;
419
+
420
+ if (previousRotation === null || currentIsRotated !== previousIsRotated) {
421
+ scheduleOnRN(setIsRotated, currentIsRotated);
422
+ }
228
423
  },
229
424
  );
230
425
 
231
426
  useEffect(() => {
232
- let unsubscribe: (() => void) | undefined;
427
+ return registry.subscribeToActiveTrigger(id, (node) => {
428
+ setActiveTriggerNode(node);
233
429
 
234
- const unsubscribeFromRegistry = registry.subscribeToManager(id, (managerInstance) => {
235
- setManager(managerInstance);
236
- unsubscribe = managerInstance?.subscribe((state) => {
237
- pendingIndexRef.current = state.currentIndex;
238
- setCurrentIndex(state.currentIndex);
239
- });
430
+ if (node) {
431
+ setIsTriggerOpening(true);
432
+ }
240
433
  });
241
-
242
- return () => {
243
- unsubscribeFromRegistry();
244
- unsubscribe?.();
245
- };
246
434
  }, [id]);
247
435
 
248
436
  useEffect(() => {
249
- return registry.subscribeToActiveTrigger(id, setActiveTriggerNode);
250
- }, [id]);
437
+ dataRef.current = data;
438
+ }, [data]);
251
439
 
252
440
  useEffect(() => {
253
- pendingIndexRef.current = initialIndex;
441
+ isZoomedRef.current = isZoomed;
442
+ }, [isZoomed]);
254
443
 
255
- if (!manager) {
256
- return;
444
+ useEffect(() => {
445
+ isRotatedRef.current = isRotated;
446
+ }, [isRotated]);
447
+
448
+ useEffect(() => {
449
+ isPinchingRef.current = isPinching;
450
+ }, [isPinching]);
451
+
452
+ useEffect(() => {
453
+ onSingleTapRef.current = onSingleTap;
454
+ }, [onSingleTap]);
455
+
456
+ useEffect(() => {
457
+ onAnimationCompleteRef.current = triggerAnimation?.onAnimationComplete;
458
+ }, [triggerAnimation?.onAnimationComplete]);
459
+
460
+ useLayoutEffect(() => {
461
+ const initialIndexChanged = previousInitialIndexRef.current !== initialIndex;
462
+ const shouldResolveInitialIndex =
463
+ initialIndexChanged || (!hasResolvedInitialIndexRef.current && dataLength > 0);
464
+
465
+ previousInitialIndexRef.current = initialIndex;
466
+ dataLengthRef.current = dataLength;
467
+ enableLoopRef.current = enableLoop;
468
+
469
+ hasResolvedInitialIndexRef.current = dataLength > 0;
470
+
471
+ const nextIndex = shouldResolveInitialIndex
472
+ ? clampIndex(initialIndex, dataLength)
473
+ : clampIndex(currentIndexRef.current, dataLength);
474
+
475
+ const nextVirtualIndex =
476
+ getVirtualIndexForLogicalIndex(
477
+ nextIndex,
478
+ centerVirtualIndexRef.current,
479
+ dataLength,
480
+ enableLoop,
481
+ ) ?? nextIndex;
482
+
483
+ const previousIndex = currentIndexRef.current;
484
+ const previousVirtualIndex = centerVirtualIndexRef.current;
485
+ const shouldSyncVirtualPage =
486
+ isPageTransitioningRef.current || previousVirtualIndex !== nextVirtualIndex;
487
+ const shouldResetTransform =
488
+ initialIndexChanged || previousIndex !== nextIndex || shouldSyncVirtualPage;
489
+
490
+ if (shouldSyncVirtualPage) {
491
+ snapToVirtualPage(nextVirtualIndex);
492
+
493
+ if (dataLength === 0) {
494
+ centerVirtualIndexRef.current = nextVirtualIndex;
495
+ setCenterVirtualIndex(nextVirtualIndex);
496
+ }
497
+ }
498
+
499
+ const shouldCommitIndexOutsidePaging = !shouldSyncVirtualPage || dataLength === 0;
500
+
501
+ if (shouldCommitIndexOutsidePaging && previousIndex !== nextIndex) {
502
+ commitCurrentIndex(nextIndex);
503
+ }
504
+
505
+ if (shouldCommitIndexOutsidePaging && previousIndex === nextIndex) {
506
+ managerRef.current?.notifyStateChange();
257
507
  }
258
508
 
259
- manager.setDataLength(dataLength);
260
- manager.setEnableHorizontalSwipe(enableHorizontalSwipe);
261
- manager.setCurrentIndex(initialIndex);
262
- manager.setWidth(width + itemSpacing);
263
- manager.setHeight(height);
264
- manager.setZoomSharedValues(scale, translateX, translateY, maxZoomScale);
265
- manager.setResetTransformCallback(resetTransformState);
266
- manager.setRotation(rotation);
267
- manager.setEnableLoop(enableLoop);
268
- manager.notifyStateChange();
509
+ if (shouldResetTransform) {
510
+ resetTransformState();
511
+ }
269
512
  }, [
513
+ commitCurrentIndex,
270
514
  dataLength,
271
- enableHorizontalSwipe,
272
- initialIndex,
273
- manager,
274
- width,
275
- itemSpacing,
276
- maxZoomScale,
277
515
  enableLoop,
278
- scale,
279
- height,
516
+ initialIndex,
517
+ isPageTransitioningRef,
280
518
  resetTransformState,
281
- translateX,
282
- translateY,
283
- rotation,
519
+ snapToVirtualPage,
284
520
  ]);
285
521
 
286
522
  useEffect(() => {
287
- if (!manager || !listRef.current) {
288
- return;
289
- }
290
-
291
- manager.setListRef(listRef.current);
292
- }, [manager]);
523
+ return () => {
524
+ triggerRectRef.current = null;
525
+ clearPendingWebSingleTap();
526
+ };
527
+ }, [clearPendingWebSingleTap]);
293
528
 
294
529
  useEffect(() => {
295
- const hasDataLengthChanged = previousDataLengthRef.current !== dataLength;
296
- const hasValidInitialIndex = initialIndex >= 0 && initialIndex < dataLength;
297
-
298
- previousDataLengthRef.current = dataLength;
299
- translateY.set(0);
300
- translateX.set(0);
301
- scale.set(1);
302
- backdropOpacity.set(1);
303
- startScale.set(1);
304
- rotation.set(0);
305
-
306
530
  if (
307
- !hasValidInitialIndex ||
308
- (!hasDataLengthChanged && adjustedInitialIndex <= 0) ||
309
- !listRef.current
531
+ !autoPlay ||
532
+ dataLength <= 1 ||
533
+ isTriggerOpening ||
534
+ isZoomed ||
535
+ isRotated ||
536
+ isPinching ||
537
+ (!enableLoop && currentIndex >= dataLength - 1)
310
538
  ) {
311
539
  return;
312
540
  }
313
541
 
314
- return scheduleInitialScroll(() => {
315
- scrollTo(adjustedInitialIndex, false);
316
- });
317
- }, [
318
- adjustedInitialIndex,
319
- dataLength,
320
- initialIndex,
321
- translateY,
322
- backdropOpacity,
323
- translateX,
324
- scale,
325
- startScale,
326
- rotation,
327
- scrollTo,
328
- ]);
542
+ const interval = Math.max(250, Math.floor(autoPlayInterval || 0));
329
543
 
330
- useEffect(() => {
331
- onAnimationCompleteRef.current = triggerAnimation?.onAnimationComplete;
332
- }, [triggerAnimation?.onAnimationComplete]);
544
+ if (!Number.isFinite(interval)) {
545
+ return;
546
+ }
333
547
 
334
- useEffect(() => {
335
- dataRef.current = data;
336
- }, [data]);
548
+ const timer = setInterval(() => {
549
+ if (
550
+ isPageTransitioningRef.current ||
551
+ isZoomedRef.current ||
552
+ isRotatedRef.current ||
553
+ isPinchingRef.current
554
+ ) {
555
+ return;
556
+ }
337
557
 
338
- useEffect(() => {
339
- managerRef.current = manager;
340
- }, [manager]);
558
+ const nextIndex =
559
+ currentIndexRef.current >= dataLengthRef.current - 1
560
+ ? enableLoopRef.current
561
+ ? 0
562
+ : null
563
+ : currentIndexRef.current + 1;
341
564
 
342
- useEffect(() => {
343
- onSingleTapRef.current = onSingleTap;
344
- }, [onSingleTap]);
565
+ if (nextIndex === null) {
566
+ return;
567
+ }
568
+
569
+ navigateToIndex(nextIndex);
570
+ }, interval);
571
+
572
+ return () => clearInterval(timer);
573
+ }, [
574
+ autoPlay,
575
+ autoPlayInterval,
576
+ currentIndex,
577
+ dataLength,
578
+ enableLoop,
579
+ isPinching,
580
+ isRotated,
581
+ isTriggerOpening,
582
+ isZoomed,
583
+ isPageTransitioningRef,
584
+ navigateToIndex,
585
+ ]);
345
586
 
346
587
  useEffect(() => {
347
588
  if (shouldStartTriggerAnimation && triggerRectRef.current) {
@@ -360,7 +601,7 @@ export const useGestureViewer = <ItemT, LC>({
360
601
  triggerScale.set(
361
602
  withTiming(1, animationConfig, (finished) => {
362
603
  if (finished) {
363
- scheduleOnRN(onAnimationComplete);
604
+ scheduleOnRN(finishTriggerOpening);
364
605
  }
365
606
  }),
366
607
  );
@@ -385,11 +626,18 @@ export const useGestureViewer = <ItemT, LC>({
385
626
  triggerScale,
386
627
  triggerTranslateX,
387
628
  triggerTranslateY,
388
- onAnimationComplete,
629
+ finishTriggerOpening,
389
630
  ]);
390
631
 
391
632
  useEffect(() => {
392
- if (!activeTriggerNode || typeof activeTriggerNode.measure !== 'function') {
633
+ if (!activeTriggerNode) {
634
+ return;
635
+ }
636
+
637
+ if (typeof activeTriggerNode.measure !== 'function') {
638
+ setIsTriggerOpening(false);
639
+ triggerOpacity.set(1);
640
+ registry.clearActiveTriggerNode(id);
393
641
  return;
394
642
  }
395
643
 
@@ -402,6 +650,8 @@ export const useGestureViewer = <ItemT, LC>({
402
650
  } satisfies TriggerRect;
403
651
 
404
652
  if (!isValidTriggerRect(nextTriggerRect)) {
653
+ setIsTriggerOpening(false);
654
+ triggerOpacity.set(1);
405
655
  registry.clearActiveTriggerNode(id);
406
656
  return;
407
657
  }
@@ -411,13 +661,7 @@ export const useGestureViewer = <ItemT, LC>({
411
661
  setShouldStartTriggerAnimation(true);
412
662
  registry.clearActiveTriggerNode(id);
413
663
  });
414
- }, [activeTriggerNode, id, isValidTriggerRect, triggerOpacity]);
415
-
416
- useEffect(() => {
417
- return () => {
418
- triggerRectRef.current = null;
419
- };
420
- }, []);
664
+ }, [activeTriggerNode, id, triggerOpacity]);
421
665
 
422
666
  const animateDismissToRect = useCallback(
423
667
  (rect: TriggerRect) => {
@@ -455,6 +699,10 @@ export const useGestureViewer = <ItemT, LC>({
455
699
  }, [onDismiss]);
456
700
 
457
701
  const handleDismiss = useCallback(() => {
702
+ if (isPageTransitioningRef.current) {
703
+ return;
704
+ }
705
+
458
706
  onDismissStart?.();
459
707
 
460
708
  const dismissTargetIndex = pendingIndexRef.current;
@@ -490,23 +738,59 @@ export const useGestureViewer = <ItemT, LC>({
490
738
  }
491
739
 
492
740
  dismissWithoutTrigger();
493
- }, [animateDismissToRect, dismissWithoutTrigger, id, isValidTriggerRect, onDismissStart]);
741
+ }, [animateDismissToRect, dismissWithoutTrigger, id, isPageTransitioningRef, onDismissStart]);
494
742
 
495
743
  const dismissGesture = useMemo(() => {
496
- const canDismiss = !isZoomed && dismissOptions.enabled;
744
+ const canDismiss = !isTriggerOpening && !isPinching && !isZoomed && dismissOptions.enabled;
745
+ const resetDismissTranslation = () => {
746
+ 'worklet';
747
+ cancelAnimation(translateY);
748
+ translateY.set(0);
749
+ };
497
750
 
498
751
  return Gesture.Pan()
499
752
  .minDistance(10)
753
+ .maxPointers(1)
500
754
  .averageTouches(true)
501
755
  .activeCursor('grabbing')
502
756
  .activeOffsetY([-10, 10])
503
757
  .failOffsetX([-10, 10])
504
758
  .withRef(dismissGestureRef)
505
759
  .enabled(canDismiss)
760
+ .onTouchesDown((event, stateManager) => {
761
+ if (event.numberOfTouches === 1) {
762
+ nativeInteractionHadMultipleTouches.set(false);
763
+ return;
764
+ }
765
+
766
+ if (event.numberOfTouches > 1) {
767
+ nativeInteractionHadMultipleTouches.set(true);
768
+ suppressNativeTap.set(true);
769
+ resetDismissTranslation();
770
+ stateManager.fail();
771
+ }
772
+ })
773
+ .onStart(() => {
774
+ suppressNativeTap.set(true);
775
+ })
506
776
  .onUpdate((event) => {
777
+ if (nativeInteractionHadMultipleTouches.get() || pageTransitionLocked.get()) {
778
+ return;
779
+ }
780
+
507
781
  translateY.set(event.translationY / dismissOptions.resistance);
508
782
  })
509
783
  .onEnd((event) => {
784
+ if (nativeInteractionHadMultipleTouches.get()) {
785
+ resetDismissTranslation();
786
+ return;
787
+ }
788
+
789
+ if (pageTransitionLocked.get()) {
790
+ resetDismissTranslation();
791
+ return;
792
+ }
793
+
510
794
  if (
511
795
  canDismiss &&
512
796
  shouldDismissByDirection(
@@ -519,17 +803,141 @@ export const useGestureViewer = <ItemT, LC>({
519
803
  return;
520
804
  }
521
805
 
522
- translateY.set(
523
- withSpring(0, {
524
- damping: 50,
525
- energyThreshold: 6e-9,
526
- mass: 4,
527
- overshootClamping: false,
528
- stiffness: 600,
529
- }),
530
- );
806
+ translateY.set(withSpring(0, PAGE_SPRING_CONFIG));
807
+ })
808
+ .onFinalize((_event, success) => {
809
+ if ((!success || nativeInteractionHadMultipleTouches.get()) && scale.get() <= 1) {
810
+ resetDismissTranslation();
811
+ }
531
812
  });
532
- }, [translateY, dismissOptions, handleDismiss, isZoomed]);
813
+ }, [
814
+ dismissOptions,
815
+ handleDismiss,
816
+ isPinching,
817
+ isTriggerOpening,
818
+ isZoomed,
819
+ nativeInteractionHadMultipleTouches,
820
+ pageTransitionLocked,
821
+ scale,
822
+ suppressNativeTap,
823
+ translateY,
824
+ ]);
825
+
826
+ const getCurrentTapTarget = useCallback((): { index: number; item: ItemT } | null => {
827
+ const index = pendingIndexRef.current;
828
+
829
+ if (index < 0 || index >= dataRef.current.length || !(index in dataRef.current)) {
830
+ return null;
831
+ }
832
+
833
+ return { index, item: dataRef.current[index] as ItemT };
834
+ }, []);
835
+
836
+ const isWebInteractionLocked = useCallback(
837
+ () => isPageTransitioningRef.current || pageTransitionLocked.get(),
838
+ [isPageTransitioningRef, pageTransitionLocked],
839
+ );
840
+
841
+ const emitSingleTap = useCallback<EmitSingleTap<ItemT>>(
842
+ (x, y, tapTarget) => {
843
+ if (isPageTransitioningRef.current) {
844
+ return;
845
+ }
846
+
847
+ const target = tapTarget ?? getCurrentTapTarget();
848
+
849
+ if (!target) {
850
+ return;
851
+ }
852
+
853
+ const { index: targetIndex, item: targetItem } = target;
854
+
855
+ if (targetIndex < 0) {
856
+ return;
857
+ }
858
+
859
+ managerRef.current?.emitTap({ kind: 'single', x, y, index: targetIndex });
860
+ onSingleTapRef.current?.({ x, y, index: targetIndex, item: targetItem });
861
+ },
862
+ [getCurrentTapTarget, isPageTransitioningRef],
863
+ );
864
+
865
+ const singleTapGesture = useMemo(
866
+ () =>
867
+ Gesture.Tap()
868
+ .enabled(enableNativeTapGestures)
869
+ .numberOfTaps(1)
870
+ .maxDistance(NATIVE_TAP_MAX_DISTANCE)
871
+ .onTouchesDown((event, stateManager) => {
872
+ if (event.numberOfTouches === 1) {
873
+ nativeInteractionHadMultipleTouches.set(false);
874
+ suppressNativeTap.set(false);
875
+ return;
876
+ }
877
+
878
+ nativeInteractionHadMultipleTouches.set(true);
879
+ suppressNativeTap.set(true);
880
+ stateManager.fail();
881
+ })
882
+ .onEnd((event, success) => {
883
+ if (!success || pageTransitionLocked.get() || suppressNativeTap.get()) {
884
+ return;
885
+ }
886
+
887
+ scheduleOnRN(emitSingleTap, event.x, event.y);
888
+ }),
889
+ [emitSingleTap, nativeInteractionHadMultipleTouches, pageTransitionLocked, suppressNativeTap],
890
+ );
891
+
892
+ const doubleTapGesture = useMemo(
893
+ () =>
894
+ Gesture.Tap()
895
+ .enabled(enableDoubleTapZoom && enableNativeTapGestures)
896
+ .numberOfTaps(2)
897
+ .maxDistance(NATIVE_TAP_MAX_DISTANCE)
898
+ .onTouchesDown((event, stateManager) => {
899
+ if (event.numberOfTouches === 1) {
900
+ return;
901
+ }
902
+
903
+ nativeInteractionHadMultipleTouches.set(true);
904
+ suppressNativeTap.set(true);
905
+ stateManager.fail();
906
+ })
907
+ .onEnd((event) => {
908
+ if (pageTransitionLocked.get() || suppressNativeTap.get()) {
909
+ return;
910
+ }
911
+
912
+ applyTapZoomAtPoint({
913
+ x: event.x,
914
+ y: event.y,
915
+ width,
916
+ height,
917
+ maxZoomScale,
918
+ scale,
919
+ translateX,
920
+ translateY,
921
+ });
922
+ }),
923
+ [
924
+ enableDoubleTapZoom,
925
+ height,
926
+ maxZoomScale,
927
+ nativeInteractionHadMultipleTouches,
928
+ pageTransitionLocked,
929
+ scale,
930
+ suppressNativeTap,
931
+ translateX,
932
+ translateY,
933
+ width,
934
+ ],
935
+ );
936
+
937
+ const tapGesture = useMemo(
938
+ () => Gesture.Exclusive(doubleTapGesture, singleTapGesture),
939
+ [doubleTapGesture, singleTapGesture],
940
+ );
533
941
 
534
942
  const zoomPinchGesture = useMemo(
535
943
  () =>
@@ -537,11 +945,29 @@ export const useGestureViewer = <ItemT, LC>({
537
945
  .enabled(enablePinchZoom)
538
946
  .onTouchesDown((event) => {
539
947
  if (event.numberOfTouches === 2) {
948
+ nativeInteractionHadMultipleTouches.set(true);
949
+ suppressNativeTap.set(true);
540
950
  scheduleOnRN(setIsPinching, true);
541
951
  }
542
952
  })
543
953
  .onStart((event) => {
544
- startScale.set(scale.get());
954
+ if (pageTransitionLocked.get()) {
955
+ return;
956
+ }
957
+
958
+ const currentScale = scale.get();
959
+
960
+ nativeInteractionHadMultipleTouches.set(true);
961
+ suppressNativeTap.set(true);
962
+ startScale.set(currentScale);
963
+
964
+ if (currentScale <= 1) {
965
+ cancelAnimation(translateX);
966
+ cancelAnimation(translateY);
967
+ translateX.set(0);
968
+ translateY.set(0);
969
+ }
970
+
545
971
  initialTranslateX.set(translateX.get());
546
972
  initialTranslateY.set(translateY.get());
547
973
  startFocalX.set(event.focalX);
@@ -551,6 +977,10 @@ export const useGestureViewer = <ItemT, LC>({
551
977
  hasActiveFocal.set(false);
552
978
  })
553
979
  .onUpdate((event) => {
980
+ if (pageTransitionLocked.get()) {
981
+ return;
982
+ }
983
+
554
984
  const initialScale = startScale.get();
555
985
  const newScale = initialScale * event.scale;
556
986
 
@@ -612,6 +1042,10 @@ export const useGestureViewer = <ItemT, LC>({
612
1042
  translateY.set(constrainedTranslateY);
613
1043
  })
614
1044
  .onEnd(() => {
1045
+ if (pageTransitionLocked.get()) {
1046
+ return;
1047
+ }
1048
+
615
1049
  const currentScale = scale.get();
616
1050
 
617
1051
  if (currentScale > maxZoomScale) {
@@ -684,20 +1118,47 @@ export const useGestureViewer = <ItemT, LC>({
684
1118
  startFocalX,
685
1119
  startFocalY,
686
1120
  hasActiveFocal,
1121
+ nativeInteractionHadMultipleTouches,
1122
+ pageTransitionLocked,
1123
+ suppressNativeTap,
687
1124
  ],
688
1125
  );
689
1126
 
690
1127
  const zoomPanGesture = useMemo(
691
1128
  () =>
692
1129
  Gesture.Pan()
693
- .enabled(enablePanWhenZoomed && isZoomed)
1130
+ .enabled(enablePanWhenZoomed && !isPinching && isZoomed)
1131
+ .maxPointers(1)
694
1132
  .activeCursor('grabbing')
695
1133
  .averageTouches(true)
1134
+ .onTouchesDown((event, stateManager) => {
1135
+ if (event.numberOfTouches === 1) {
1136
+ nativeInteractionHadMultipleTouches.set(false);
1137
+ return;
1138
+ }
1139
+
1140
+ if (event.numberOfTouches > 1) {
1141
+ nativeInteractionHadMultipleTouches.set(true);
1142
+ suppressNativeTap.set(true);
1143
+ stateManager.fail();
1144
+ }
1145
+ })
696
1146
  .onBegin(() => {
1147
+ if (nativeInteractionHadMultipleTouches.get() || pageTransitionLocked.get()) {
1148
+ return;
1149
+ }
1150
+
697
1151
  initialTranslateX.set(translateX.get());
698
1152
  initialTranslateY.set(translateY.get());
699
1153
  })
1154
+ .onStart(() => {
1155
+ suppressNativeTap.set(true);
1156
+ })
700
1157
  .onUpdate((event) => {
1158
+ if (nativeInteractionHadMultipleTouches.get() || pageTransitionLocked.get()) {
1159
+ return;
1160
+ }
1161
+
701
1162
  const currentScale = scale.get();
702
1163
 
703
1164
  if (currentScale > 1) {
@@ -719,78 +1180,38 @@ export const useGestureViewer = <ItemT, LC>({
719
1180
  translateX,
720
1181
  translateY,
721
1182
  enablePanWhenZoomed,
1183
+ isPinching,
722
1184
  isZoomed,
723
1185
  scale,
724
1186
  initialTranslateX,
725
1187
  initialTranslateY,
726
1188
  constrainTranslation,
1189
+ nativeInteractionHadMultipleTouches,
1190
+ pageTransitionLocked,
1191
+ suppressNativeTap,
727
1192
  ],
728
1193
  );
729
1194
 
730
- const emitSingleTap = useCallback((x: number, y: number) => {
731
- const index = pendingIndexRef.current;
732
- const currentData = dataRef.current;
733
-
734
- if (index < 0 || index >= currentData.length) {
735
- return;
736
- }
737
-
738
- managerRef.current?.emitTap({ kind: 'single', x, y, index });
739
-
740
- const item = currentData[index];
741
-
742
- if (item === undefined) {
743
- return;
744
- }
745
-
746
- onSingleTapRef.current?.({ x, y, index, item });
747
- }, []);
748
-
749
- const singleTapGesture = useMemo(
750
- () =>
751
- Gesture.Tap()
752
- .enabled(Platform.OS !== 'web')
753
- .numberOfTaps(1)
754
- .onEnd((event, success) => {
755
- if (!success) {
756
- return;
757
- }
758
-
759
- scheduleOnRN(emitSingleTap, event.x, event.y);
760
- }),
761
- [emitSingleTap],
762
- );
763
-
764
- const doubleTapGesture = useMemo(
765
- () =>
766
- Gesture.Tap()
767
- .enabled(enableDoubleTapZoom && Platform.OS !== 'web')
768
- .numberOfTaps(2)
769
- .onEnd((event) => {
770
- applyTapZoomAtPoint({
771
- x: event.x,
772
- y: event.y,
773
- width,
774
- height,
775
- maxZoomScale,
776
- scale,
777
- translateX,
778
- translateY,
779
- });
780
- }),
781
- [enableDoubleTapZoom, height, maxZoomScale, scale, translateX, translateY, width],
782
- );
783
-
784
- const tapGesture = useMemo(
785
- () => Gesture.Exclusive(doubleTapGesture, singleTapGesture),
786
- [doubleTapGesture, singleTapGesture],
787
- );
788
-
789
1195
  const zoomGesture = useMemo(
790
- () => Gesture.Race(zoomPinchGesture, Gesture.Exclusive(zoomPanGesture, tapGesture)),
791
- [zoomPinchGesture, zoomPanGesture, tapGesture],
1196
+ () => Gesture.Exclusive(zoomPanGesture, tapGesture),
1197
+ [zoomPanGesture, tapGesture],
792
1198
  );
793
1199
 
1200
+ const onWebClick = useWebClickHandler({
1201
+ clearPendingWebSingleTap,
1202
+ emitSingleTap,
1203
+ enableDoubleTapZoom,
1204
+ getCurrentTapTarget,
1205
+ height,
1206
+ maxZoomScale,
1207
+ scale,
1208
+ scheduleWebSingleTap,
1209
+ isInteractionLocked: isWebInteractionLocked,
1210
+ translateX,
1211
+ translateY,
1212
+ width,
1213
+ });
1214
+
794
1215
  const animatedStyle = useAnimatedStyle(() => ({
795
1216
  opacity: triggerOpacity.get(),
796
1217
  transform: [
@@ -818,54 +1239,23 @@ export const useGestureViewer = <ItemT, LC>({
818
1239
  return { opacity: baseOpacity * dismissOpacity };
819
1240
  }, [dismissOptions.direction, dismissOptions.fadeBackdrop]);
820
1241
 
821
- const nativeScrollGesture = useMemo(() => {
822
- return Gesture.Native().requireExternalGestureToFail(dismissGestureRef);
823
- }, []);
824
-
825
- const { activeListIndex, onMomentumScrollEnd, onScroll, onScrollBeginDrag, onWebClick } =
826
- useGestureViewerPaging({
827
- adjustedInitialIndex,
828
- autoPlay,
829
- autoPlayInterval,
830
- currentIndex,
831
- dataLength,
832
- enableDoubleTapZoom,
833
- enableHorizontalSwipe,
834
- enableLoop,
835
- height,
836
- isRotated,
837
- isZoomed,
838
- itemSpacing,
839
- manager,
840
- maxZoomScale,
841
- onSingleTap: emitSingleTap,
842
- scale,
843
- scrollTo,
844
- syncCurrentIndex,
845
- syncPendingIndex,
846
- translateX,
847
- translateY,
848
- width,
849
- });
850
-
851
1242
  return {
852
- activeListIndex,
853
1243
  animatedStyle,
854
1244
  backdropStyle,
1245
+ centerVirtualIndex: renderCenterVirtualIndex,
1246
+ currentIndex: renderCurrentIndex,
855
1247
  dataLength,
856
1248
  dismissGesture,
857
1249
  handleDismiss,
1250
+ horizontalPagingGesture,
858
1251
  isPinching,
859
1252
  isRotated,
860
-
861
1253
  isZoomed,
862
- listRef,
863
- nativeScrollGesture,
864
1254
  onWebClick,
865
- onMomentumScrollEnd,
866
- onScroll,
867
-
868
- onScrollBeginDrag,
1255
+ pageStride,
1256
+ renderWindowSlots,
1257
+ visualPage,
869
1258
  zoomGesture,
1259
+ zoomPinchGesture,
870
1260
  };
871
1261
  };