react-native-gesture-image-viewer 2.1.2 → 2.1.4

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 (67) hide show
  1. package/README.md +1 -1
  2. package/lib/module/GestureTrigger.js.map +1 -1
  3. package/lib/module/GestureViewer.js +23 -6
  4. package/lib/module/GestureViewer.js.map +1 -1
  5. package/lib/module/GestureViewerManager.js +26 -6
  6. package/lib/module/GestureViewerManager.js.map +1 -1
  7. package/lib/module/GestureViewerRegistry.js.map +1 -1
  8. package/lib/module/WebPagingFixStyle.js.map +1 -1
  9. package/lib/module/useGestureViewer.js +205 -229
  10. package/lib/module/useGestureViewer.js.map +1 -1
  11. package/lib/module/useGestureViewerController.js.map +1 -1
  12. package/lib/module/useGestureViewerEvent.js.map +1 -1
  13. package/lib/module/useGestureViewerPaging.js +68 -0
  14. package/lib/module/useGestureViewerPaging.js.map +1 -0
  15. package/lib/module/useGestureViewerPaging.types.js +4 -0
  16. package/lib/module/useGestureViewerPaging.types.js.map +1 -0
  17. package/lib/module/useGestureViewerPaging.web.js +241 -0
  18. package/lib/module/useGestureViewerPaging.web.js.map +1 -0
  19. package/lib/module/useGestureViewerState.js.map +1 -1
  20. package/lib/module/utils/index.js +23 -16
  21. package/lib/module/utils/index.js.map +1 -1
  22. package/lib/module/utils/tapZoom.js +34 -0
  23. package/lib/module/utils/tapZoom.js.map +1 -0
  24. package/lib/module/utils/webScroll.js +70 -0
  25. package/lib/module/utils/webScroll.js.map +1 -0
  26. package/lib/typescript/src/GestureTrigger.d.ts +1 -1
  27. package/lib/typescript/src/GestureTrigger.d.ts.map +1 -1
  28. package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
  29. package/lib/typescript/src/GestureViewerManager.d.ts +5 -0
  30. package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
  31. package/lib/typescript/src/GestureViewerRegistry.d.ts.map +1 -1
  32. package/lib/typescript/src/WebPagingFixStyle.d.ts.map +1 -1
  33. package/lib/typescript/src/types.d.ts.map +1 -1
  34. package/lib/typescript/src/useGestureViewer.d.ts +13 -11
  35. package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
  36. package/lib/typescript/src/useGestureViewerController.d.ts.map +1 -1
  37. package/lib/typescript/src/useGestureViewerEvent.d.ts.map +1 -1
  38. package/lib/typescript/src/useGestureViewerPaging.d.ts +3 -0
  39. package/lib/typescript/src/useGestureViewerPaging.d.ts.map +1 -0
  40. package/lib/typescript/src/useGestureViewerPaging.types.d.ts +32 -0
  41. package/lib/typescript/src/useGestureViewerPaging.types.d.ts.map +1 -0
  42. package/lib/typescript/src/useGestureViewerPaging.web.d.ts +3 -0
  43. package/lib/typescript/src/useGestureViewerPaging.web.d.ts.map +1 -0
  44. package/lib/typescript/src/useGestureViewerState.d.ts.map +1 -1
  45. package/lib/typescript/src/utils/index.d.ts +8 -1
  46. package/lib/typescript/src/utils/index.d.ts.map +1 -1
  47. package/lib/typescript/src/utils/tapZoom.d.ts +12 -0
  48. package/lib/typescript/src/utils/tapZoom.d.ts.map +1 -0
  49. package/lib/typescript/src/utils/webScroll.d.ts +18 -0
  50. package/lib/typescript/src/utils/webScroll.d.ts.map +1 -0
  51. package/package.json +86 -80
  52. package/src/GestureTrigger.tsx +9 -2
  53. package/src/GestureViewer.tsx +77 -28
  54. package/src/GestureViewerManager.ts +62 -12
  55. package/src/GestureViewerRegistry.ts +1 -0
  56. package/src/WebPagingFixStyle.tsx +1 -0
  57. package/src/types.ts +44 -32
  58. package/src/useGestureViewer.ts +292 -313
  59. package/src/useGestureViewerController.ts +1 -0
  60. package/src/useGestureViewerEvent.ts +5 -1
  61. package/src/useGestureViewerPaging.ts +112 -0
  62. package/src/useGestureViewerPaging.types.ts +34 -0
  63. package/src/useGestureViewerPaging.web.ts +390 -0
  64. package/src/useGestureViewerState.ts +1 -0
  65. package/src/utils/index.ts +48 -16
  66. package/src/utils/tapZoom.ts +44 -0
  67. package/src/utils/webScroll.ts +96 -0
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useMemo, useRef } from 'react';
2
+
2
3
  import type GestureViewerManager from './GestureViewerManager';
3
4
  import { registry } from './GestureViewerRegistry';
4
5
  import type { GestureViewerController } from './types';
@@ -1,4 +1,5 @@
1
1
  import { useEffect } from 'react';
2
+
2
3
  import type GestureViewerManager from './GestureViewerManager';
3
4
  import { registry } from './GestureViewerRegistry';
4
5
  import type { GestureViewerEventCallback, GestureViewerEventType } from './types';
@@ -71,7 +72,10 @@ export function useGestureViewerEvent<T extends GestureViewerEventType>(
71
72
  callback?: GestureViewerEventCallback<T>,
72
73
  ) {
73
74
  const id = typeof idOrEventType === 'string' && callback ? idOrEventType : 'default';
74
- const eventType = typeof idOrEventType === 'string' && callback ? (eventTypeOrCallback as T) : (idOrEventType as T);
75
+ const eventType =
76
+ typeof idOrEventType === 'string' && callback
77
+ ? (eventTypeOrCallback as T)
78
+ : (idOrEventType as T);
75
79
  const finalCallback = callback || (eventTypeOrCallback as GestureViewerEventCallback<T>);
76
80
 
77
81
  useEffect(() => {
@@ -0,0 +1,112 @@
1
+ import { useCallback, useEffect } from 'react';
2
+ import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
3
+
4
+ import type {
5
+ UseGestureViewerPagingArgs,
6
+ UseGestureViewerPagingResult,
7
+ } from './useGestureViewerPaging.types';
8
+ import { getLoopAdjustedIndex } from './utils';
9
+
10
+ export function useGestureViewerPaging({
11
+ autoPlay,
12
+ autoPlayInterval,
13
+ currentIndex,
14
+ dataLength,
15
+ enableHorizontalSwipe,
16
+ enableLoop,
17
+ isRotated,
18
+ isZoomed,
19
+ itemSpacing,
20
+ manager,
21
+ scrollTo,
22
+ syncCurrentIndex,
23
+ width,
24
+ }: UseGestureViewerPagingArgs): UseGestureViewerPagingResult {
25
+ useEffect(() => {
26
+ if (
27
+ !autoPlay ||
28
+ !manager ||
29
+ dataLength <= 1 ||
30
+ isZoomed ||
31
+ isRotated ||
32
+ (!enableLoop && currentIndex === dataLength - 1)
33
+ ) {
34
+ return;
35
+ }
36
+
37
+ const intervalMs = Math.max(250, Math.floor(autoPlayInterval || 0));
38
+
39
+ if (!Number.isFinite(intervalMs)) {
40
+ return;
41
+ }
42
+
43
+ const interval = setInterval(() => {
44
+ if (isZoomed || isRotated) {
45
+ return;
46
+ }
47
+
48
+ manager.goToNext();
49
+ }, intervalMs);
50
+
51
+ return () => clearInterval(interval);
52
+ }, [
53
+ autoPlay,
54
+ autoPlayInterval,
55
+ currentIndex,
56
+ dataLength,
57
+ enableLoop,
58
+ isRotated,
59
+ isZoomed,
60
+ manager,
61
+ ]);
62
+
63
+ const onMomentumScrollEnd = useCallback(
64
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
65
+ if (!enableHorizontalSwipe) {
66
+ return;
67
+ }
68
+
69
+ const { contentOffset } = event.nativeEvent;
70
+ const scrollIndex = Math.round(contentOffset.x / (width + itemSpacing));
71
+
72
+ const isLoopHandled = manager?.handleMomentumScrollEnd(scrollIndex);
73
+
74
+ if (isLoopHandled) {
75
+ return;
76
+ }
77
+
78
+ const { realIndex, needsJump, jumpToIndex } = getLoopAdjustedIndex(
79
+ scrollIndex,
80
+ dataLength,
81
+ enableLoop,
82
+ );
83
+
84
+ if (needsJump && jumpToIndex !== undefined) {
85
+ scrollTo(jumpToIndex, false);
86
+ }
87
+
88
+ syncCurrentIndex(realIndex);
89
+ },
90
+ [
91
+ dataLength,
92
+ enableHorizontalSwipe,
93
+ enableLoop,
94
+ itemSpacing,
95
+ manager,
96
+ scrollTo,
97
+ syncCurrentIndex,
98
+ width,
99
+ ],
100
+ );
101
+
102
+ const onScrollBeginDrag = useCallback(() => {
103
+ manager?.handleScrollBeginDrag();
104
+ }, [manager]);
105
+
106
+ return {
107
+ onMomentumScrollEnd,
108
+ onScroll: undefined,
109
+ onScrollBeginDrag,
110
+ onWebDoubleClick: undefined,
111
+ };
112
+ }
@@ -0,0 +1,34 @@
1
+ import type { ScrollViewProps } from 'react-native';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+
4
+ import type GestureViewerManager from './GestureViewerManager';
5
+
6
+ export type UseGestureViewerPagingArgs = {
7
+ adjustedInitialIndex: number;
8
+ autoPlay: boolean;
9
+ autoPlayInterval: number;
10
+ currentIndex: number;
11
+ dataLength: number;
12
+ enableDoubleTapZoom: boolean;
13
+ enableHorizontalSwipe: boolean;
14
+ enableLoop: boolean;
15
+ height: number;
16
+ isRotated: boolean;
17
+ isZoomed: boolean;
18
+ itemSpacing: number;
19
+ manager: GestureViewerManager | null;
20
+ maxZoomScale: number;
21
+ scale: SharedValue<number>;
22
+ scrollTo: (index: number, animated: boolean) => void;
23
+ syncCurrentIndex: (nextIndex: number) => void;
24
+ translateX: SharedValue<number>;
25
+ translateY: SharedValue<number>;
26
+ width: number;
27
+ };
28
+
29
+ export type UseGestureViewerPagingResult = {
30
+ onMomentumScrollEnd?: ScrollViewProps['onMomentumScrollEnd'];
31
+ onScroll?: ScrollViewProps['onScroll'];
32
+ onScrollBeginDrag?: ScrollViewProps['onScrollBeginDrag'];
33
+ onWebDoubleClick?: (event: any) => void;
34
+ };
@@ -0,0 +1,390 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
3
+
4
+ import type {
5
+ UseGestureViewerPagingArgs,
6
+ UseGestureViewerPagingResult,
7
+ } from './useGestureViewerPaging.types';
8
+ import { applyTapZoomAtPoint } from './utils/tapZoom';
9
+ import {
10
+ getWebAutoPlayTargetPhysicalIndex,
11
+ getWebScrollPhysicalIndex,
12
+ resolveWebScrollFinalState,
13
+ } from './utils/webScroll';
14
+
15
+ type WebScrollActor = 'idle' | 'user' | 'autoplay';
16
+
17
+ type WebScrollRuntime = {
18
+ actor: WebScrollActor;
19
+ isAutoplayPausedByUser: boolean;
20
+ lastSettledPhysicalIndex: number;
21
+ latestOffsetX: number;
22
+ latestRawPhysicalIndex: number;
23
+ lastProgrammaticScrollVersion: number;
24
+ settleTimer: ReturnType<typeof setTimeout> | null;
25
+ resumeAutoplayTimer: ReturnType<typeof setTimeout> | null;
26
+ };
27
+
28
+ export function useGestureViewerPaging({
29
+ adjustedInitialIndex,
30
+ autoPlay,
31
+ autoPlayInterval,
32
+ currentIndex,
33
+ dataLength,
34
+ enableDoubleTapZoom,
35
+ enableHorizontalSwipe,
36
+ enableLoop,
37
+ height,
38
+ isRotated,
39
+ isZoomed,
40
+ itemSpacing,
41
+ manager,
42
+ maxZoomScale,
43
+ scale,
44
+ scrollTo,
45
+ syncCurrentIndex,
46
+ translateX,
47
+ translateY,
48
+ width,
49
+ }: UseGestureViewerPagingArgs): UseGestureViewerPagingResult {
50
+ const webScrollRuntimeRef = useRef<WebScrollRuntime>({
51
+ actor: 'idle',
52
+ isAutoplayPausedByUser: false,
53
+ lastSettledPhysicalIndex: adjustedInitialIndex,
54
+ latestOffsetX: adjustedInitialIndex * (width + itemSpacing),
55
+ latestRawPhysicalIndex: adjustedInitialIndex,
56
+ lastProgrammaticScrollVersion: 0,
57
+ settleTimer: null,
58
+ resumeAutoplayTimer: null,
59
+ });
60
+
61
+ const clearWebSettleTimer = useCallback(() => {
62
+ const runtime = webScrollRuntimeRef.current;
63
+
64
+ if (runtime.settleTimer) {
65
+ clearTimeout(runtime.settleTimer);
66
+ runtime.settleTimer = null;
67
+ }
68
+ }, []);
69
+
70
+ const clearWebAutoplayResumeTimer = useCallback(() => {
71
+ const runtime = webScrollRuntimeRef.current;
72
+
73
+ if (runtime.resumeAutoplayTimer) {
74
+ clearTimeout(runtime.resumeAutoplayTimer);
75
+ runtime.resumeAutoplayTimer = null;
76
+ }
77
+ }, []);
78
+
79
+ const scheduleWebAutoplayResume = useCallback(() => {
80
+ const runtime = webScrollRuntimeRef.current;
81
+
82
+ clearWebAutoplayResumeTimer();
83
+ runtime.resumeAutoplayTimer = setTimeout(() => {
84
+ runtime.isAutoplayPausedByUser = false;
85
+ runtime.resumeAutoplayTimer = null;
86
+ }, 800);
87
+ }, [clearWebAutoplayResumeTimer]);
88
+
89
+ const pauseWebAutoplayWithoutPagingInteraction = useCallback(() => {
90
+ const runtime = webScrollRuntimeRef.current;
91
+
92
+ runtime.isAutoplayPausedByUser = true;
93
+ runtime.actor = 'idle';
94
+ clearWebAutoplayResumeTimer();
95
+ }, [clearWebAutoplayResumeTimer]);
96
+
97
+ const beginWebUserInteraction = useCallback(
98
+ (force = false) => {
99
+ const runtime = webScrollRuntimeRef.current;
100
+
101
+ if (!force && runtime.actor === 'autoplay') {
102
+ return;
103
+ }
104
+
105
+ runtime.actor = 'user';
106
+ runtime.isAutoplayPausedByUser = true;
107
+ clearWebAutoplayResumeTimer();
108
+ },
109
+ [clearWebAutoplayResumeTimer],
110
+ );
111
+
112
+ const beginWebAutoplayScroll = useCallback(() => {
113
+ const runtime = webScrollRuntimeRef.current;
114
+
115
+ runtime.actor = 'autoplay';
116
+ runtime.isAutoplayPausedByUser = false;
117
+ runtime.lastProgrammaticScrollVersion = manager?.getProgrammaticScrollVersion() ?? 0;
118
+ clearWebAutoplayResumeTimer();
119
+ }, [clearWebAutoplayResumeTimer, manager]);
120
+
121
+ const syncProgrammaticActorFromManager = useCallback(() => {
122
+ const runtime = webScrollRuntimeRef.current;
123
+ const nextVersion = manager?.getProgrammaticScrollVersion() ?? 0;
124
+
125
+ if (nextVersion === runtime.lastProgrammaticScrollVersion) {
126
+ return;
127
+ }
128
+
129
+ runtime.actor = 'autoplay';
130
+ runtime.isAutoplayPausedByUser = false;
131
+ runtime.lastProgrammaticScrollVersion = nextVersion;
132
+ clearWebAutoplayResumeTimer();
133
+ }, [clearWebAutoplayResumeTimer, manager]);
134
+
135
+ const finalizeWebScroll = useCallback(
136
+ (offsetX: number, source: 'scroll' | 'momentum' = 'scroll') => {
137
+ if (!enableHorizontalSwipe || dataLength <= 0) {
138
+ return;
139
+ }
140
+
141
+ const runtime = webScrollRuntimeRef.current;
142
+ const settledByActor = runtime.actor;
143
+
144
+ clearWebSettleTimer();
145
+ runtime.latestOffsetX = offsetX;
146
+
147
+ const settledState = resolveWebScrollFinalState({
148
+ dataLength,
149
+ enableLoop,
150
+ lastSettledPhysicalIndex: runtime.lastSettledPhysicalIndex,
151
+ offsetX,
152
+ pageWidth: width + itemSpacing,
153
+ });
154
+
155
+ if (!settledState) {
156
+ return;
157
+ }
158
+
159
+ const { logicalIndex, rawPhysicalIndex, settledPhysicalIndex } = settledState;
160
+
161
+ if (source === 'scroll' && runtime.latestRawPhysicalIndex !== rawPhysicalIndex) {
162
+ return;
163
+ }
164
+
165
+ if (rawPhysicalIndex !== settledPhysicalIndex) {
166
+ const crossedLoopBoundary =
167
+ enableLoop &&
168
+ dataLength > 1 &&
169
+ (rawPhysicalIndex === 0 || rawPhysicalIndex === dataLength + 1);
170
+
171
+ scrollTo(settledPhysicalIndex, !crossedLoopBoundary);
172
+ }
173
+
174
+ runtime.lastSettledPhysicalIndex = settledPhysicalIndex;
175
+ runtime.latestOffsetX = settledPhysicalIndex * (width + itemSpacing);
176
+ runtime.actor = 'idle';
177
+ manager?.cancelPendingLoopTransition();
178
+ syncCurrentIndex(logicalIndex);
179
+
180
+ if (settledByActor === 'user') {
181
+ scheduleWebAutoplayResume();
182
+ }
183
+ },
184
+ [
185
+ clearWebSettleTimer,
186
+ dataLength,
187
+ enableHorizontalSwipe,
188
+ enableLoop,
189
+ itemSpacing,
190
+ manager,
191
+ scheduleWebAutoplayResume,
192
+ scrollTo,
193
+ syncCurrentIndex,
194
+ width,
195
+ ],
196
+ );
197
+
198
+ const scheduleWebScrollSettle = useCallback(
199
+ (offsetX: number) => {
200
+ const runtime = webScrollRuntimeRef.current;
201
+
202
+ runtime.latestOffsetX = offsetX;
203
+ runtime.latestRawPhysicalIndex = getWebScrollPhysicalIndex(offsetX, width + itemSpacing);
204
+ clearWebSettleTimer();
205
+ runtime.settleTimer = setTimeout(() => {
206
+ finalizeWebScroll(runtime.latestOffsetX, 'scroll');
207
+ }, 180);
208
+ },
209
+ [clearWebSettleTimer, finalizeWebScroll, itemSpacing, width],
210
+ );
211
+
212
+ useEffect(() => {
213
+ const runtime = webScrollRuntimeRef.current;
214
+
215
+ runtime.actor = 'idle';
216
+ runtime.isAutoplayPausedByUser = false;
217
+ runtime.lastSettledPhysicalIndex = adjustedInitialIndex;
218
+ runtime.latestOffsetX = adjustedInitialIndex * (width + itemSpacing);
219
+ runtime.latestRawPhysicalIndex = adjustedInitialIndex;
220
+ runtime.lastProgrammaticScrollVersion = manager?.getProgrammaticScrollVersion() ?? 0;
221
+ clearWebSettleTimer();
222
+ clearWebAutoplayResumeTimer();
223
+ }, [
224
+ adjustedInitialIndex,
225
+ clearWebAutoplayResumeTimer,
226
+ clearWebSettleTimer,
227
+ itemSpacing,
228
+ manager,
229
+ width,
230
+ ]);
231
+
232
+ useEffect(() => {
233
+ return () => {
234
+ clearWebSettleTimer();
235
+ clearWebAutoplayResumeTimer();
236
+ };
237
+ }, [clearWebAutoplayResumeTimer, clearWebSettleTimer]);
238
+
239
+ useEffect(() => {
240
+ if (
241
+ !autoPlay ||
242
+ dataLength <= 1 ||
243
+ isZoomed ||
244
+ isRotated ||
245
+ (!enableLoop && currentIndex === dataLength - 1)
246
+ ) {
247
+ return;
248
+ }
249
+
250
+ const intervalMs = Math.max(250, Math.floor(autoPlayInterval || 0));
251
+
252
+ if (!Number.isFinite(intervalMs)) {
253
+ return;
254
+ }
255
+
256
+ const interval = setInterval(() => {
257
+ if (isZoomed || isRotated) {
258
+ return;
259
+ }
260
+
261
+ const runtime = webScrollRuntimeRef.current;
262
+
263
+ if (runtime.actor !== 'idle' || runtime.isAutoplayPausedByUser || runtime.settleTimer) {
264
+ return;
265
+ }
266
+
267
+ const targetPhysicalIndex = getWebAutoPlayTargetPhysicalIndex({
268
+ currentIndex,
269
+ dataLength,
270
+ enableLoop,
271
+ });
272
+
273
+ if (targetPhysicalIndex === null) {
274
+ return;
275
+ }
276
+
277
+ beginWebAutoplayScroll();
278
+ runtime.latestOffsetX = targetPhysicalIndex * (width + itemSpacing);
279
+ scrollTo(targetPhysicalIndex, true);
280
+ scheduleWebScrollSettle(runtime.latestOffsetX);
281
+ }, intervalMs);
282
+
283
+ return () => clearInterval(interval);
284
+ }, [
285
+ autoPlay,
286
+ autoPlayInterval,
287
+ beginWebAutoplayScroll,
288
+ currentIndex,
289
+ dataLength,
290
+ enableLoop,
291
+ isRotated,
292
+ isZoomed,
293
+ itemSpacing,
294
+ scheduleWebScrollSettle,
295
+ scrollTo,
296
+ width,
297
+ ]);
298
+
299
+ const onScroll = useCallback(
300
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
301
+ if (!enableHorizontalSwipe) {
302
+ return;
303
+ }
304
+
305
+ syncProgrammaticActorFromManager();
306
+ beginWebUserInteraction();
307
+ scheduleWebScrollSettle(event.nativeEvent.contentOffset.x);
308
+ },
309
+ [
310
+ beginWebUserInteraction,
311
+ enableHorizontalSwipe,
312
+ scheduleWebScrollSettle,
313
+ syncProgrammaticActorFromManager,
314
+ ],
315
+ );
316
+
317
+ const onMomentumScrollEnd = useCallback(
318
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
319
+ if (!enableHorizontalSwipe) {
320
+ return;
321
+ }
322
+
323
+ finalizeWebScroll(event.nativeEvent.contentOffset.x, 'momentum');
324
+ },
325
+ [enableHorizontalSwipe, finalizeWebScroll],
326
+ );
327
+
328
+ const onScrollBeginDrag = useCallback(() => {
329
+ beginWebUserInteraction(true);
330
+ clearWebSettleTimer();
331
+ manager?.handleScrollBeginDrag();
332
+ }, [beginWebUserInteraction, clearWebSettleTimer, manager]);
333
+
334
+ const onWebDoubleClick = useCallback(
335
+ (event: any) => {
336
+ if (!enableDoubleTapZoom || event?.nativeEvent?.detail !== 2) {
337
+ return;
338
+ }
339
+
340
+ pauseWebAutoplayWithoutPagingInteraction();
341
+ scheduleWebAutoplayResume();
342
+
343
+ const nativeEvent = event?.nativeEvent;
344
+ const eventTarget = event?.currentTarget ?? nativeEvent?.currentTarget ?? nativeEvent?.target;
345
+ let locationX = nativeEvent?.locationX;
346
+ let locationY = nativeEvent?.locationY;
347
+
348
+ if (
349
+ (!Number.isFinite(locationX) || !Number.isFinite(locationY)) &&
350
+ typeof eventTarget?.getBoundingClientRect === 'function' &&
351
+ Number.isFinite(nativeEvent?.clientX) &&
352
+ Number.isFinite(nativeEvent?.clientY)
353
+ ) {
354
+ const rect = eventTarget.getBoundingClientRect();
355
+
356
+ locationX = nativeEvent.clientX - rect.left;
357
+ locationY = nativeEvent.clientY - rect.top;
358
+ }
359
+
360
+ applyTapZoomAtPoint({
361
+ x: Number.isFinite(locationX) ? locationX : width / 2,
362
+ y: Number.isFinite(locationY) ? locationY : height / 2,
363
+ width,
364
+ height,
365
+ maxZoomScale,
366
+ scale,
367
+ translateX,
368
+ translateY,
369
+ });
370
+ },
371
+ [
372
+ enableDoubleTapZoom,
373
+ height,
374
+ maxZoomScale,
375
+ pauseWebAutoplayWithoutPagingInteraction,
376
+ scheduleWebAutoplayResume,
377
+ scale,
378
+ translateX,
379
+ translateY,
380
+ width,
381
+ ],
382
+ );
383
+
384
+ return {
385
+ onMomentumScrollEnd,
386
+ onScroll,
387
+ onScrollBeginDrag,
388
+ onWebDoubleClick,
389
+ };
390
+ }
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useRef, useSyncExternalStore } from 'react';
2
+
2
3
  import { registry } from './GestureViewerRegistry';
3
4
  import type { GestureViewerState } from './types';
4
5
 
@@ -1,13 +1,25 @@
1
- import { FlatList as RNFlatList, ScrollView as RNScrollView } from 'react-native';
2
- import { FlatList as GestureFlatList, ScrollView as GestureScrollView } from 'react-native-gesture-handler';
1
+ import {
2
+ FlatList as RNFlatList,
3
+ ScrollView as RNScrollView,
4
+ type PlatformOSType,
5
+ } from 'react-native';
6
+ import {
7
+ FlatList as GestureFlatList,
8
+ ScrollView as GestureScrollView,
9
+ } from 'react-native-gesture-handler';
10
+
3
11
  import type { FlatListComponent, ScrollViewComponent } from '../types';
12
+
4
13
  import { FlashList } from './FlashList';
5
14
 
6
- export const isScrollViewLike = (component: React.ComponentType<any>): component is ScrollViewComponent => {
7
- return component === RNScrollView || component === GestureScrollView;
8
- };
15
+ export const isScrollViewLike = (
16
+ component: React.ComponentType<any>,
17
+ ): component is ScrollViewComponent =>
18
+ component === RNScrollView || component === GestureScrollView;
9
19
 
10
- export const isFlatListLike = (component: React.ComponentType<any>): component is FlatListComponent<any> => {
20
+ export const isFlatListLike = (
21
+ component: React.ComponentType<any>,
22
+ ): component is FlatListComponent<any> => {
11
23
  if (component === RNFlatList || component === GestureFlatList || isFlashListLike(component)) {
12
24
  return true;
13
25
  }
@@ -23,9 +35,29 @@ export const isFlashListLike = (component: React.ComponentType<any>): boolean =>
23
35
  return component?.displayName === 'FlashList' || component?.name === 'FlashList';
24
36
  };
25
37
 
38
+ /**
39
+ * iOS needs the extra Native gesture wrapper so the dismiss pan can resolve before the scrollable claims touches.
40
+ * We intentionally skip Android because the same require-to-fail relation regressed horizontal paging there,
41
+ * and we also avoid wrapping RNGH-provided scrollables to prevent double-applying Gesture.Native() to components that already participate in RNGH.
42
+ */
43
+ export const shouldUseNativeScrollGesture = (
44
+ platformOS: PlatformOSType,
45
+ component: React.ComponentType<any>,
46
+ ): boolean => {
47
+ return platformOS === 'ios' && component !== GestureScrollView && component !== GestureFlatList;
48
+ };
49
+
26
50
  export const createBoundsConstraint =
27
51
  ({ width, height }: { width: number; height: number }) =>
28
- ({ scale, translateX, translateY }: { translateX: number; translateY: number; scale: number }) => {
52
+ ({
53
+ scale,
54
+ translateX,
55
+ translateY,
56
+ }: {
57
+ translateX: number;
58
+ translateY: number;
59
+ scale: number;
60
+ }) => {
29
61
  'worklet';
30
62
 
31
63
  if (scale <= 1) {
@@ -51,7 +83,7 @@ export const createLoopData = <T>(dataRef: React.RefObject<T[]>, enableLoop: boo
51
83
  return data;
52
84
  }
53
85
 
54
- const lastItem = data[data.length - 1];
86
+ const lastItem = data.at(-1);
55
87
  const firstItem = data[0];
56
88
 
57
89
  if (lastItem === undefined || firstItem === undefined) {
@@ -67,32 +99,32 @@ export const getLoopAdjustedIndex = (
67
99
  enableLoop: boolean,
68
100
  ): { realIndex: number; needsJump: boolean; jumpToIndex?: number } => {
69
101
  if (!enableLoop || originalDataLength <= 1) {
70
- return { realIndex: scrollIndex, needsJump: false };
102
+ return { needsJump: false, realIndex: scrollIndex };
71
103
  }
72
104
 
73
105
  if (scrollIndex === 0) {
74
106
  return {
75
- realIndex: originalDataLength - 1,
76
- needsJump: true,
77
107
  jumpToIndex: originalDataLength,
108
+ needsJump: true,
109
+ realIndex: originalDataLength - 1,
78
110
  };
79
111
  } else if (scrollIndex === originalDataLength + 1) {
80
112
  return {
81
- realIndex: 0,
82
- needsJump: true,
83
113
  jumpToIndex: 1,
114
+ needsJump: true,
115
+ realIndex: 0,
84
116
  };
85
117
  }
86
118
 
87
- return { realIndex: scrollIndex - 1, needsJump: false };
119
+ return { needsJump: false, realIndex: scrollIndex - 1 };
88
120
  };
89
121
 
90
122
  export const createScrollAction = (listRef: any, width: number) => ({
91
123
  scrollTo: (index: number, animated: boolean) => {
92
124
  if (listRef?.scrollToIndex) {
93
- listRef.scrollToIndex({ index, animated });
125
+ listRef.scrollToIndex({ animated, index });
94
126
  } else if (listRef?.scrollTo) {
95
- listRef.scrollTo({ x: index * width, animated });
127
+ listRef.scrollTo({ animated, x: index * width });
96
128
  }
97
129
  },
98
130
  });