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
@@ -11,7 +11,7 @@ import type { GestureViewerController } from './types';
11
11
  * @returns Methods for controlling the viewer
12
12
  *
13
13
  * **Available methods:**
14
- * - `goToIndex(index: number)` - Navigate to specific index (0 to totalCount-1)
14
+ * - `goToIndex(index: number, options?: { animated?: boolean })` - Navigate to specific index (0 to totalCount-1)
15
15
  * - `goToPrevious()` - Navigate to previous item
16
16
  * - `goToNext()` - Navigate to next item
17
17
  * - `zoomIn(multiplier?: number)` - Zoom in (default: 0.25)
@@ -58,8 +58,8 @@ export const useGestureViewerController = (id = 'default'): GestureViewerControl
58
58
 
59
59
  return useMemo<GestureViewerController>(
60
60
  () => ({
61
- goToIndex: (index) => {
62
- managerRef.current?.goToIndex(index);
61
+ goToIndex: (index, options) => {
62
+ managerRef.current?.goToIndex(index, options);
63
63
  },
64
64
  goToPrevious: () => {
65
65
  managerRef.current?.goToPrevious();
@@ -0,0 +1,118 @@
1
+ import { useEffect, useState, type RefObject } from 'react';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+
4
+ import type GestureViewerManager from './GestureViewerManager';
5
+ import type { GestureViewerNavigationOptions } from './GestureViewerManager';
6
+ import { registry } from './GestureViewerRegistry';
7
+
8
+ type UseGestureViewerManagerBridgeOptions = {
9
+ currentIndexRef: RefObject<number>;
10
+ dataLengthRef: RefObject<number>;
11
+ goToIndex: (index: number, options?: GestureViewerNavigationOptions) => void;
12
+ goToNext: () => void;
13
+ goToPrevious: () => void;
14
+ hasRotationChangeListeners: SharedValue<boolean>;
15
+ hasZoomChangeListeners: SharedValue<boolean>;
16
+ height: number;
17
+ id: string;
18
+ managerRef: RefObject<GestureViewerManager | null>;
19
+ maxZoomScale: number;
20
+ rotation: SharedValue<number>;
21
+ scale: SharedValue<number>;
22
+ translateX: SharedValue<number>;
23
+ translateY: SharedValue<number>;
24
+ width: number;
25
+ };
26
+
27
+ /**
28
+ * Owns the registry-to-manager bridge side effects for one viewer instance.
29
+ */
30
+ export function useGestureViewerManagerBridge({
31
+ currentIndexRef,
32
+ dataLengthRef,
33
+ goToIndex,
34
+ goToNext,
35
+ goToPrevious,
36
+ hasRotationChangeListeners,
37
+ hasZoomChangeListeners,
38
+ height,
39
+ id,
40
+ managerRef,
41
+ maxZoomScale,
42
+ rotation,
43
+ scale,
44
+ translateX,
45
+ translateY,
46
+ width,
47
+ }: UseGestureViewerManagerBridgeOptions) {
48
+ const [manager, setManager] = useState<GestureViewerManager | null>(null);
49
+
50
+ useEffect(() => {
51
+ return registry.subscribeToManager(id, (managerInstance) => {
52
+ managerRef.current = managerInstance;
53
+ setManager(managerInstance);
54
+ });
55
+ }, [id, managerRef]);
56
+
57
+ useEffect(() => {
58
+ if (!manager) {
59
+ return;
60
+ }
61
+
62
+ manager.setWidth(width);
63
+ manager.setHeight(height);
64
+ manager.setZoomSharedValues(scale, translateX, translateY, maxZoomScale);
65
+ manager.setRotation(rotation);
66
+ }, [height, manager, maxZoomScale, rotation, scale, translateX, translateY, width]);
67
+
68
+ useEffect(() => {
69
+ if (!manager) {
70
+ hasZoomChangeListeners.set(false);
71
+ hasRotationChangeListeners.set(false);
72
+ return;
73
+ }
74
+
75
+ return manager.subscribeToEventListenerPresence((eventType, hasListeners) => {
76
+ if (eventType === 'zoomChange') {
77
+ hasZoomChangeListeners.set(hasListeners);
78
+ return;
79
+ }
80
+
81
+ if (eventType === 'rotationChange') {
82
+ hasRotationChangeListeners.set(hasListeners);
83
+ }
84
+ });
85
+ }, [hasRotationChangeListeners, hasZoomChangeListeners, manager]);
86
+
87
+ useEffect(() => {
88
+ if (!manager) {
89
+ return;
90
+ }
91
+
92
+ manager.setStateReader(() => ({
93
+ currentIndex: currentIndexRef.current,
94
+ totalCount: dataLengthRef.current,
95
+ }));
96
+ manager.notifyStateChange();
97
+
98
+ return () => {
99
+ manager.setStateReader(null);
100
+ };
101
+ }, [currentIndexRef, dataLengthRef, manager]);
102
+
103
+ useEffect(() => {
104
+ if (!manager) {
105
+ return;
106
+ }
107
+
108
+ manager.setNavigationAdapter({
109
+ goToIndex,
110
+ goToNext,
111
+ goToPrevious,
112
+ });
113
+
114
+ return () => {
115
+ manager.setNavigationAdapter(null);
116
+ };
117
+ }, [goToIndex, goToNext, goToPrevious, manager]);
118
+ }
@@ -1,144 +1,338 @@
1
- import { useCallback, useEffect, useState } from 'react';
2
- import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
1
+ import { useCallback, useMemo, useRef } from 'react';
2
+ import { Gesture } from 'react-native-gesture-handler';
3
+ import {
4
+ cancelAnimation,
5
+ type SharedValue,
6
+ useSharedValue,
7
+ withSpring,
8
+ withTiming,
9
+ } from 'react-native-reanimated';
10
+ import { scheduleOnRN } from 'react-native-worklets';
3
11
 
4
- import type {
5
- UseGestureViewerPagingArgs,
6
- UseGestureViewerPagingResult,
7
- } from './useGestureViewerPaging.types';
8
- import { getLoopAdjustedIndex } from './utils';
12
+ import {
13
+ EDGE_RESISTANCE,
14
+ PAGE_SPRING_CONFIG,
15
+ PAGE_TRANSITION_CONFIG,
16
+ } from './gestureViewerAnimation';
17
+ import {
18
+ applyHorizontalEdgeResistance,
19
+ resolveHorizontalPagingTarget,
20
+ resolveHorizontalSwipeDirection,
21
+ } from './gestureViewerPaging';
9
22
 
23
+ type UseGestureViewerPagingOptions = {
24
+ centerVirtualIndex: number;
25
+ clearPendingWebSingleTap: () => void;
26
+ commitVirtualIndexOnly: (targetVirtualIndex: number) => void;
27
+ currentIndex: number;
28
+ dataLength: number;
29
+ enableLoop: boolean;
30
+ horizontalSwipeDistanceThresholdRatio: number;
31
+ horizontalSwipeEnabled: boolean;
32
+ horizontalSwipeVelocityThreshold: number;
33
+ initialPage: number;
34
+ isPinching: boolean;
35
+ isRotated: boolean;
36
+ isTriggerOpening: boolean;
37
+ isZoomed: boolean;
38
+ pageStride: number;
39
+ suppressNativeTap: SharedValue<boolean>;
40
+ width: number;
41
+ };
42
+
43
+ /**
44
+ * Owns horizontal paging shared values, transition state, and paging commands.
45
+ */
10
46
  export function useGestureViewerPaging({
11
- adjustedInitialIndex,
12
- autoPlay,
13
- autoPlayInterval,
47
+ centerVirtualIndex,
48
+ clearPendingWebSingleTap,
49
+ commitVirtualIndexOnly,
14
50
  currentIndex,
15
51
  dataLength,
16
- enableHorizontalSwipe,
17
52
  enableLoop,
53
+ horizontalSwipeDistanceThresholdRatio,
54
+ horizontalSwipeEnabled,
55
+ horizontalSwipeVelocityThreshold,
56
+ initialPage,
57
+ isPinching,
18
58
  isRotated,
59
+ isTriggerOpening,
19
60
  isZoomed,
20
- itemSpacing,
21
- manager,
22
- scrollTo,
23
- syncCurrentIndex,
24
- syncPendingIndex,
61
+ pageStride,
62
+ suppressNativeTap,
25
63
  width,
26
- }: UseGestureViewerPagingArgs): UseGestureViewerPagingResult {
27
- const [activeListIndex, setActiveListIndex] = useState(adjustedInitialIndex);
28
- const activeResetItemSpacing = adjustedInitialIndex > 0 ? itemSpacing : 0;
29
- const activeResetWidth = adjustedInitialIndex > 0 ? width : 0;
30
-
31
- useEffect(() => {
32
- setActiveListIndex(adjustedInitialIndex);
33
- }, [adjustedInitialIndex, activeResetItemSpacing, activeResetWidth, dataLength]);
34
-
35
- useEffect(() => {
36
- if (
37
- !autoPlay ||
38
- !manager ||
39
- dataLength <= 1 ||
40
- isZoomed ||
41
- isRotated ||
42
- (!enableLoop && currentIndex === dataLength - 1)
43
- ) {
44
- return;
45
- }
46
-
47
- const intervalMs = Math.max(250, Math.floor(autoPlayInterval || 0));
48
-
49
- if (!Number.isFinite(intervalMs)) {
50
- return;
51
- }
52
-
53
- const interval = setInterval(() => {
54
- if (isZoomed || isRotated) {
55
- return;
56
- }
57
-
58
- manager.goToNext();
59
- }, intervalMs);
64
+ }: UseGestureViewerPagingOptions) {
65
+ const visualPage = useSharedValue(initialPage);
66
+ const pagingStartPage = useSharedValue(initialPage);
67
+ const pagingAnimationActive = useSharedValue(false);
68
+ const pagingGestureActive = useSharedValue(false);
69
+ const pageTransitionLocked = useSharedValue(false);
70
+ const isPageTransitioningRef = useRef(false);
60
71
 
61
- return () => clearInterval(interval);
62
- }, [
63
- autoPlay,
64
- autoPlayInterval,
65
- currentIndex,
66
- dataLength,
67
- enableLoop,
68
- isRotated,
69
- isZoomed,
70
- manager,
71
- ]);
72
+ const setPageTransitioning = useCallback(
73
+ (nextTransitioning: boolean) => {
74
+ isPageTransitioningRef.current = nextTransitioning;
75
+ pageTransitionLocked.set(nextTransitioning);
72
76
 
73
- const onMomentumScrollEnd = useCallback(
74
- (event: NativeSyntheticEvent<NativeScrollEvent>) => {
75
- if (!enableHorizontalSwipe) {
76
- return;
77
+ if (nextTransitioning) {
78
+ clearPendingWebSingleTap();
77
79
  }
80
+ },
81
+ [clearPendingWebSingleTap, pageTransitionLocked],
82
+ );
78
83
 
79
- const { contentOffset } = event.nativeEvent;
80
- const scrollIndex = Math.round(contentOffset.x / (width + itemSpacing));
81
- const { realIndex, needsJump, jumpToIndex } = getLoopAdjustedIndex(
82
- scrollIndex,
83
- dataLength,
84
- enableLoop,
85
- );
84
+ const clearPagingFlags = useCallback(() => {
85
+ pagingAnimationActive.set(false);
86
+ pagingGestureActive.set(false);
87
+ }, [pagingAnimationActive, pagingGestureActive]);
86
88
 
87
- const isLoopHandled = manager?.handleMomentumScrollEnd(scrollIndex);
89
+ const cancelPagingInteraction = useCallback(() => {
90
+ clearPendingWebSingleTap();
91
+ cancelAnimation(visualPage);
92
+ clearPagingFlags();
93
+ setPageTransitioning(false);
94
+ }, [clearPendingWebSingleTap, clearPagingFlags, setPageTransitioning, visualPage]);
88
95
 
89
- if (realIndex < 0 || realIndex >= dataLength) {
90
- return;
91
- }
96
+ const snapToVirtualPage = useCallback(
97
+ (targetVirtualIndex: number) => {
98
+ clearPendingWebSingleTap();
99
+ cancelAnimation(visualPage);
100
+ clearPagingFlags();
101
+ visualPage.set(targetVirtualIndex);
102
+ commitVirtualIndexOnly(targetVirtualIndex);
103
+ setPageTransitioning(false);
104
+ },
105
+ [
106
+ clearPendingWebSingleTap,
107
+ clearPagingFlags,
108
+ commitVirtualIndexOnly,
109
+ setPageTransitioning,
110
+ visualPage,
111
+ ],
112
+ );
92
113
 
93
- if (isLoopHandled) {
94
- syncCurrentIndex(realIndex);
95
- setActiveListIndex(jumpToIndex ?? scrollIndex);
96
- return;
97
- }
114
+ const completeAnimatedVirtualPage = useCallback(
115
+ (targetVirtualIndex: number) => {
116
+ commitVirtualIndexOnly(targetVirtualIndex);
117
+ setPageTransitioning(false);
118
+ },
119
+ [commitVirtualIndexOnly, setPageTransitioning],
120
+ );
98
121
 
99
- if (needsJump && jumpToIndex !== undefined) {
100
- scrollTo(jumpToIndex, false);
101
- }
122
+ const cancelAnimatedVirtualPage = useCallback(() => {
123
+ setPageTransitioning(false);
124
+ }, [setPageTransitioning]);
125
+
126
+ const animateToVirtualPage = useCallback(
127
+ (targetVirtualIndex: number) => {
128
+ cancelAnimation(visualPage);
129
+ clearPagingFlags();
130
+ pagingAnimationActive.set(true);
131
+ setPageTransitioning(true);
132
+ visualPage.set(
133
+ withTiming(targetVirtualIndex, PAGE_TRANSITION_CONFIG, (finished) => {
134
+ pagingAnimationActive.set(false);
135
+
136
+ if (finished) {
137
+ scheduleOnRN(completeAnimatedVirtualPage, targetVirtualIndex);
138
+ return;
139
+ }
102
140
 
103
- syncCurrentIndex(realIndex);
104
- setActiveListIndex(jumpToIndex ?? scrollIndex);
141
+ scheduleOnRN(cancelAnimatedVirtualPage);
142
+ }),
143
+ );
105
144
  },
106
145
  [
107
- dataLength,
108
- enableHorizontalSwipe,
109
- enableLoop,
110
- itemSpacing,
111
- manager,
112
- scrollTo,
113
- syncCurrentIndex,
114
- width,
146
+ cancelAnimatedVirtualPage,
147
+ clearPagingFlags,
148
+ completeAnimatedVirtualPage,
149
+ pagingAnimationActive,
150
+ setPageTransitioning,
151
+ visualPage,
115
152
  ],
116
153
  );
117
154
 
118
- const onScroll = useCallback(
119
- (event: NativeSyntheticEvent<NativeScrollEvent>) => {
120
- if (!enableHorizontalSwipe) {
155
+ const horizontalPagingGesture = useMemo(() => {
156
+ const canSwipe =
157
+ horizontalSwipeEnabled &&
158
+ dataLength > 1 &&
159
+ !isTriggerOpening &&
160
+ !isZoomed &&
161
+ !isRotated &&
162
+ !isPinching &&
163
+ pageStride > 0;
164
+ const settleToCenter = () => {
165
+ 'worklet';
166
+ pagingAnimationActive.set(true);
167
+ visualPage.set(
168
+ withSpring(centerVirtualIndex, PAGE_SPRING_CONFIG, (finished) => {
169
+ pagingAnimationActive.set(false);
170
+
171
+ if (finished) {
172
+ scheduleOnRN(setPageTransitioning, false);
173
+ }
174
+ }),
175
+ );
176
+ };
177
+ const releasePagingForPinch = () => {
178
+ 'worklet';
179
+ if (!pagingGestureActive.get()) {
121
180
  return;
122
181
  }
123
182
 
124
- const { contentOffset } = event.nativeEvent;
125
- const scrollIndex = Math.round(contentOffset.x / (width + itemSpacing));
126
- const { realIndex } = getLoopAdjustedIndex(scrollIndex, dataLength, enableLoop);
183
+ cancelAnimation(visualPage);
184
+ pagingAnimationActive.set(false);
185
+ pagingGestureActive.set(false);
186
+ pageTransitionLocked.set(false);
187
+ visualPage.set(withSpring(centerVirtualIndex, PAGE_SPRING_CONFIG));
188
+ scheduleOnRN(setPageTransitioning, false);
189
+ };
127
190
 
128
- syncPendingIndex(realIndex);
129
- },
130
- [dataLength, enableHorizontalSwipe, enableLoop, itemSpacing, syncPendingIndex, width],
131
- );
191
+ return Gesture.Pan()
192
+ .minDistance(10)
193
+ .maxPointers(1)
194
+ .averageTouches(true)
195
+ .activeCursor('grabbing')
196
+ .activeOffsetX([-10, 10])
197
+ .failOffsetY([-10, 10])
198
+ .enabled(canSwipe)
199
+ .onTouchesDown((event, stateManager) => {
200
+ if (event.numberOfTouches > 1) {
201
+ suppressNativeTap.set(true);
202
+ releasePagingForPinch();
203
+ stateManager.fail();
204
+ }
205
+ })
206
+ .onStart(() => {
207
+ if (pageTransitionLocked.get()) {
208
+ return;
209
+ }
210
+
211
+ // Pan enters BEGAN for ordinary taps, so lock paging only after swipe activation.
212
+ cancelAnimation(visualPage);
213
+ pagingAnimationActive.set(false);
214
+ pagingGestureActive.set(true);
215
+ pagingStartPage.set(visualPage.get());
216
+ suppressNativeTap.set(true);
217
+ scheduleOnRN(setPageTransitioning, true);
218
+ })
219
+ .onUpdate((event) => {
220
+ if (!pagingGestureActive.get()) {
221
+ return;
222
+ }
223
+
224
+ const dragPageDelta = -event.translationX / pageStride;
225
+ const basePage = pagingStartPage.get();
226
+ const nextPage = applyHorizontalEdgeResistance(
227
+ basePage + dragPageDelta,
228
+ currentIndex,
229
+ dataLength,
230
+ centerVirtualIndex,
231
+ enableLoop,
232
+ EDGE_RESISTANCE,
233
+ );
234
+
235
+ visualPage.set(nextPage);
236
+ })
237
+ .onEnd((event) => {
238
+ if (!pagingGestureActive.get()) {
239
+ return;
240
+ }
241
+
242
+ pagingGestureActive.set(false);
243
+
244
+ const direction = resolveHorizontalSwipeDirection(
245
+ event.translationX,
246
+ event.velocityX,
247
+ width,
248
+ horizontalSwipeDistanceThresholdRatio,
249
+ horizontalSwipeVelocityThreshold,
250
+ );
251
+
252
+ if (direction === 0) {
253
+ settleToCenter();
254
+ return;
255
+ }
256
+
257
+ const pagingTarget = resolveHorizontalPagingTarget(
258
+ centerVirtualIndex,
259
+ currentIndex,
260
+ dataLength,
261
+ direction,
262
+ enableLoop,
263
+ );
264
+
265
+ if (pagingTarget.kind === 'settle') {
266
+ settleToCenter();
267
+ return;
268
+ }
269
+
270
+ const targetVirtualIndex = pagingTarget.targetVirtualIndex;
132
271
 
133
- const onScrollBeginDrag = useCallback(() => {
134
- manager?.handleScrollBeginDrag();
135
- }, [manager]);
272
+ pagingAnimationActive.set(true);
273
+ visualPage.set(
274
+ withTiming(targetVirtualIndex, PAGE_TRANSITION_CONFIG, (finished) => {
275
+ pagingAnimationActive.set(false);
276
+
277
+ if (finished) {
278
+ scheduleOnRN(completeAnimatedVirtualPage, targetVirtualIndex);
279
+ return;
280
+ }
281
+
282
+ scheduleOnRN(cancelAnimatedVirtualPage);
283
+ }),
284
+ );
285
+ })
286
+ .onFinalize(() => {
287
+ if (pagingAnimationActive.get()) {
288
+ return;
289
+ }
290
+
291
+ if (!pagingGestureActive.get()) {
292
+ return;
293
+ }
294
+
295
+ pagingGestureActive.set(false);
296
+
297
+ if (Math.abs(visualPage.get() - centerVirtualIndex) > 0.001) {
298
+ settleToCenter();
299
+ return;
300
+ }
301
+
302
+ scheduleOnRN(setPageTransitioning, false);
303
+ });
304
+ }, [
305
+ cancelAnimatedVirtualPage,
306
+ centerVirtualIndex,
307
+ completeAnimatedVirtualPage,
308
+ currentIndex,
309
+ dataLength,
310
+ enableLoop,
311
+ horizontalSwipeDistanceThresholdRatio,
312
+ horizontalSwipeEnabled,
313
+ horizontalSwipeVelocityThreshold,
314
+ isTriggerOpening,
315
+ isPinching,
316
+ isRotated,
317
+ isZoomed,
318
+ pageTransitionLocked,
319
+ pageStride,
320
+ pagingAnimationActive,
321
+ pagingGestureActive,
322
+ pagingStartPage,
323
+ setPageTransitioning,
324
+ suppressNativeTap,
325
+ visualPage,
326
+ width,
327
+ ]);
136
328
 
137
329
  return {
138
- activeListIndex,
139
- onMomentumScrollEnd,
140
- onScroll,
141
- onScrollBeginDrag,
142
- onWebClick: undefined,
330
+ animateToVirtualPage,
331
+ cancelPagingInteraction,
332
+ horizontalPagingGesture,
333
+ isPageTransitioningRef,
334
+ pageTransitionLocked,
335
+ snapToVirtualPage,
336
+ visualPage,
143
337
  };
144
338
  }
@@ -0,0 +1,44 @@
1
+ import { useCallback } from 'react';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+
4
+ import type { ScheduleWebSingleTap } from './useWebSingleTapTimer';
5
+
6
+ export type WebTapTarget<ItemT> = {
7
+ index: number;
8
+ item: ItemT;
9
+ };
10
+
11
+ export type WebClickEvent = {
12
+ clientX: number;
13
+ clientY: number;
14
+ currentTarget: {
15
+ getBoundingClientRect: () => {
16
+ left: number;
17
+ top: number;
18
+ };
19
+ };
20
+ detail: number;
21
+ };
22
+
23
+ export type WebClickHandler = (event: WebClickEvent) => void;
24
+
25
+ export type EmitSingleTap<ItemT> = (x: number, y: number, tapTarget?: WebTapTarget<ItemT>) => void;
26
+
27
+ export type WebClickHandlerConfig<ItemT> = {
28
+ clearPendingWebSingleTap: () => void;
29
+ emitSingleTap: EmitSingleTap<ItemT>;
30
+ enableDoubleTapZoom: boolean;
31
+ getCurrentTapTarget: () => WebTapTarget<ItemT> | null;
32
+ height: number;
33
+ isInteractionLocked: () => boolean;
34
+ maxZoomScale: number;
35
+ scale: SharedValue<number>;
36
+ scheduleWebSingleTap: ScheduleWebSingleTap;
37
+ translateX: SharedValue<number>;
38
+ translateY: SharedValue<number>;
39
+ width: number;
40
+ };
41
+
42
+ export function useWebClickHandler<ItemT>(_config: WebClickHandlerConfig<ItemT>): WebClickHandler {
43
+ return useCallback(() => {}, []);
44
+ }