react-native-gesture-image-viewer 2.2.0 → 2.3.0
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.
- package/README.md +6 -1
- package/lib/module/GestureViewer.js +2 -2
- package/lib/module/GestureViewer.js.map +1 -1
- package/lib/module/GestureViewerManager.js +3 -0
- package/lib/module/GestureViewerManager.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/useGestureViewer.js +58 -15
- package/lib/module/useGestureViewer.js.map +1 -1
- package/lib/module/useGestureViewerEvent.js +15 -1
- package/lib/module/useGestureViewerEvent.js.map +1 -1
- package/lib/module/useGestureViewerPaging.js +1 -1
- package/lib/module/useGestureViewerPaging.js.map +1 -1
- package/lib/module/useGestureViewerPaging.web.js +47 -26
- package/lib/module/useGestureViewerPaging.web.js.map +1 -1
- package/lib/module/utils/dismiss.js +25 -0
- package/lib/module/utils/dismiss.js.map +1 -0
- package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
- package/lib/typescript/src/GestureViewerManager.d.ts +2 -1
- package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +33 -4
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useGestureViewer.d.ts +2 -2
- package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
- package/lib/typescript/src/useGestureViewerEvent.d.ts +15 -1
- package/lib/typescript/src/useGestureViewerEvent.d.ts.map +1 -1
- package/lib/typescript/src/useGestureViewerPaging.types.d.ts +9 -1
- package/lib/typescript/src/useGestureViewerPaging.types.d.ts.map +1 -1
- package/lib/typescript/src/useGestureViewerPaging.web.d.ts +1 -1
- package/lib/typescript/src/useGestureViewerPaging.web.d.ts.map +1 -1
- package/lib/typescript/src/utils/dismiss.d.ts +4 -0
- package/lib/typescript/src/utils/dismiss.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/GestureViewer.tsx +4 -2
- package/src/GestureViewerManager.ts +4 -0
- package/src/index.tsx +1 -0
- package/src/types.ts +31 -4
- package/src/useGestureViewer.ts +108 -47
- package/src/useGestureViewerEvent.ts +15 -1
- package/src/useGestureViewerPaging.ts +1 -1
- package/src/useGestureViewerPaging.types.ts +7 -1
- package/src/useGestureViewerPaging.web.ts +56 -32
- package/src/utils/dismiss.ts +36 -0
package/src/types.ts
CHANGED
|
@@ -75,6 +75,15 @@ export type TriggerRect = {
|
|
|
75
75
|
height: number;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
+
export type GestureViewerSingleTapEvent<ItemT> = {
|
|
79
|
+
x: number;
|
|
80
|
+
y: number;
|
|
81
|
+
index: number;
|
|
82
|
+
item: ItemT;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type GestureViewerDismissDirection = 'down' | 'up' | 'both';
|
|
86
|
+
|
|
78
87
|
export interface TriggerAnimationConfig extends WithTimingConfig {
|
|
79
88
|
/**
|
|
80
89
|
* Animation duration in milliseconds
|
|
@@ -121,9 +130,18 @@ export interface GestureViewerProps<ItemT, LC> {
|
|
|
121
130
|
* A callback function that is called to render the item.
|
|
122
131
|
*/
|
|
123
132
|
renderItem: (item: ItemT, index: number) => React.ReactElement;
|
|
133
|
+
/**
|
|
134
|
+
* A callback function that is called when a single tap is confirmed on the viewer content.
|
|
135
|
+
* @remarks
|
|
136
|
+
* - The callback runs only after the tap is resolved as a single tap, so it may be slightly delayed when double-tap zoom is enabled.
|
|
137
|
+
* - It is not called for swipe, pinch, dismiss, or double-tap zoom gestures.
|
|
138
|
+
* - Prefer this callback over overlaying a pressable in `renderContainer` for fullscreen tap handling.
|
|
139
|
+
*/
|
|
140
|
+
onSingleTap?: (event: GestureViewerSingleTapEvent<ItemT>) => void;
|
|
124
141
|
/**
|
|
125
142
|
* A callback function that is called to render the container.
|
|
126
143
|
* @remarks Useful for composing additional UI (e.g., close button, toolbars) around the viewer.
|
|
144
|
+
* Prefer `onSingleTap` for fullscreen tap handling instead of overlaying a pressable over the viewer content.
|
|
127
145
|
* The second argument provides control helpers such as `dismiss()` to close the viewer.
|
|
128
146
|
*
|
|
129
147
|
* @param children - The viewer content to be rendered inside your container.
|
|
@@ -183,8 +201,8 @@ export interface GestureViewerProps<ItemT, LC> {
|
|
|
183
201
|
*/
|
|
184
202
|
autoPlayInterval?: number;
|
|
185
203
|
/**
|
|
186
|
-
* Dismiss gesture options.
|
|
187
|
-
* @remarks Useful for closing modals with
|
|
204
|
+
* Dismiss gesture options.
|
|
205
|
+
* @remarks Useful for closing modals with configurable vertical swipe gestures.
|
|
188
206
|
*/
|
|
189
207
|
dismiss?: {
|
|
190
208
|
/**
|
|
@@ -192,6 +210,12 @@ export interface GestureViewerProps<ItemT, LC> {
|
|
|
192
210
|
* @defaultValue true
|
|
193
211
|
*/
|
|
194
212
|
enabled?: boolean;
|
|
213
|
+
/**
|
|
214
|
+
* Controls which vertical swipe direction can trigger `onDismiss`.
|
|
215
|
+
* @remarks Use `down` for backward-compatible behavior, `up` for upward-only dismiss, or `both` for either direction.
|
|
216
|
+
* @defaultValue 'down'
|
|
217
|
+
*/
|
|
218
|
+
direction?: GestureViewerDismissDirection;
|
|
195
219
|
/**
|
|
196
220
|
* `threshold` controls when `onDismiss` is called by applying a threshold value during vertical gestures.
|
|
197
221
|
* @defaultValue 80
|
|
@@ -203,7 +227,7 @@ export interface GestureViewerProps<ItemT, LC> {
|
|
|
203
227
|
*/
|
|
204
228
|
resistance?: number;
|
|
205
229
|
/**
|
|
206
|
-
* By default, the background `opacity` gradually decreases
|
|
230
|
+
* By default, the background `opacity` gradually decreases as you drag in the configured dismiss direction.
|
|
207
231
|
* @remarks When `false`, this animation is disabled.
|
|
208
232
|
* @defaultValue true
|
|
209
233
|
*/
|
|
@@ -425,11 +449,14 @@ export type GestureViewerState = {
|
|
|
425
449
|
readonly totalCount: number;
|
|
426
450
|
};
|
|
427
451
|
|
|
428
|
-
export type GestureViewerEventType = 'zoomChange' | 'rotationChange';
|
|
452
|
+
export type GestureViewerEventType = 'zoomChange' | 'rotationChange' | 'tap';
|
|
453
|
+
|
|
454
|
+
export type SingleTapEventData = { kind: 'single'; x: number; y: number; index: number };
|
|
429
455
|
|
|
430
456
|
export type GestureViewerEventData = {
|
|
431
457
|
zoomChange: { scale: number; previousScale: number | null };
|
|
432
458
|
rotationChange: { rotation: number; previousRotation: number | null };
|
|
459
|
+
tap: SingleTapEventData;
|
|
433
460
|
};
|
|
434
461
|
|
|
435
462
|
export type GestureViewerEventCallback<T extends GestureViewerEventType> = (
|
package/src/useGestureViewer.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { registry } from './GestureViewerRegistry';
|
|
|
17
17
|
import type { GestureViewerProps, TriggerRect } from './types';
|
|
18
18
|
import { useGestureViewerPaging } from './useGestureViewerPaging';
|
|
19
19
|
import { createBoundsConstraint, createScrollAction } from './utils';
|
|
20
|
+
import { getDismissDistance, shouldDismissByDirection } from './utils/dismiss';
|
|
20
21
|
import { applyTapZoomAtPoint } from './utils/tapZoom';
|
|
21
22
|
|
|
22
23
|
type UseGestureViewerProps<ItemT, LC> = Omit<
|
|
@@ -34,6 +35,7 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
34
35
|
data,
|
|
35
36
|
initialIndex = 0,
|
|
36
37
|
onDismiss,
|
|
38
|
+
onSingleTap,
|
|
37
39
|
width: customWidth,
|
|
38
40
|
dismiss,
|
|
39
41
|
enableDoubleTapZoom = true,
|
|
@@ -68,6 +70,9 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
68
70
|
const triggerRectRef = useRef<TriggerRect | null>(null);
|
|
69
71
|
const pendingIndexRef = useRef(initialIndex);
|
|
70
72
|
const onAnimationCompleteRef = useRef(triggerAnimation?.onAnimationComplete);
|
|
73
|
+
const onSingleTapRef = useRef(onSingleTap);
|
|
74
|
+
const dataRef = useRef(data);
|
|
75
|
+
const managerRef = useRef(manager);
|
|
71
76
|
|
|
72
77
|
const isValidTriggerRect = useCallback((rect: TriggerRect | null): rect is TriggerRect => {
|
|
73
78
|
return !!rect && rect.width > 0 && rect.height > 0;
|
|
@@ -104,12 +109,19 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
104
109
|
|
|
105
110
|
const dismissOptions = useMemo(
|
|
106
111
|
() => ({
|
|
112
|
+
direction: dismiss?.direction ?? 'down',
|
|
107
113
|
enabled: dismiss?.enabled ?? true,
|
|
108
114
|
fadeBackdrop: dismiss?.fadeBackdrop ?? true,
|
|
109
115
|
resistance: dismiss?.resistance ?? 2,
|
|
110
116
|
threshold: dismiss?.threshold ?? 80,
|
|
111
117
|
}),
|
|
112
|
-
[
|
|
118
|
+
[
|
|
119
|
+
dismiss?.direction,
|
|
120
|
+
dismiss?.enabled,
|
|
121
|
+
dismiss?.threshold,
|
|
122
|
+
dismiss?.resistance,
|
|
123
|
+
dismiss?.fadeBackdrop,
|
|
124
|
+
],
|
|
113
125
|
);
|
|
114
126
|
|
|
115
127
|
const adjustedInitialIndex = useMemo(() => {
|
|
@@ -176,23 +188,13 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
176
188
|
[dataLength, manager, resetTransformState],
|
|
177
189
|
);
|
|
178
190
|
|
|
179
|
-
const emitZoomChange = useCallback(
|
|
180
|
-
(currentScale
|
|
181
|
-
|
|
182
|
-
manager.emitZoomChange(currentScale, prevScale);
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
[manager],
|
|
186
|
-
);
|
|
191
|
+
const emitZoomChange = useCallback((currentScale: number, prevScale: number | null) => {
|
|
192
|
+
managerRef.current?.emitZoomChange(currentScale, prevScale);
|
|
193
|
+
}, []);
|
|
187
194
|
|
|
188
|
-
const emitRotationChange = useCallback(
|
|
189
|
-
(currentRotation
|
|
190
|
-
|
|
191
|
-
manager.emitRotationChange(currentRotation, prevRotation);
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
[manager],
|
|
195
|
-
);
|
|
195
|
+
const emitRotationChange = useCallback((currentRotation: number, prevRotation: number | null) => {
|
|
196
|
+
managerRef.current?.emitRotationChange(currentRotation, prevRotation);
|
|
197
|
+
}, []);
|
|
196
198
|
|
|
197
199
|
const onAnimationComplete = useCallback(() => {
|
|
198
200
|
onAnimationCompleteRef.current?.();
|
|
@@ -317,6 +319,18 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
317
319
|
onAnimationCompleteRef.current = triggerAnimation?.onAnimationComplete;
|
|
318
320
|
}, [triggerAnimation?.onAnimationComplete]);
|
|
319
321
|
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
dataRef.current = data;
|
|
324
|
+
}, [data]);
|
|
325
|
+
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
managerRef.current = manager;
|
|
328
|
+
}, [manager]);
|
|
329
|
+
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
onSingleTapRef.current = onSingleTap;
|
|
332
|
+
}, [onSingleTap]);
|
|
333
|
+
|
|
320
334
|
useEffect(() => {
|
|
321
335
|
if (shouldStartTriggerAnimation && triggerRectRef.current) {
|
|
322
336
|
const startX = triggerRectRef.current.x + triggerRectRef.current.width / 2 - width / 2;
|
|
@@ -475,7 +489,14 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
475
489
|
translateY.value = event.translationY / dismissOptions.resistance;
|
|
476
490
|
})
|
|
477
491
|
.onEnd((event) => {
|
|
478
|
-
if (
|
|
492
|
+
if (
|
|
493
|
+
canDismiss &&
|
|
494
|
+
shouldDismissByDirection(
|
|
495
|
+
event.translationY,
|
|
496
|
+
dismissOptions.threshold,
|
|
497
|
+
dismissOptions.direction,
|
|
498
|
+
)
|
|
499
|
+
) {
|
|
479
500
|
scheduleOnRN(handleDismiss);
|
|
480
501
|
return;
|
|
481
502
|
}
|
|
@@ -647,6 +668,40 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
647
668
|
],
|
|
648
669
|
);
|
|
649
670
|
|
|
671
|
+
const emitSingleTap = useCallback((x: number, y: number) => {
|
|
672
|
+
const index = pendingIndexRef.current;
|
|
673
|
+
const currentData = dataRef.current;
|
|
674
|
+
|
|
675
|
+
if (index < 0 || index >= currentData.length) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
managerRef.current?.emitTap({ kind: 'single', x, y, index });
|
|
680
|
+
|
|
681
|
+
const item = currentData[index];
|
|
682
|
+
|
|
683
|
+
if (item === undefined) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
onSingleTapRef.current?.({ x, y, index, item });
|
|
688
|
+
}, []);
|
|
689
|
+
|
|
690
|
+
const singleTapGesture = useMemo(
|
|
691
|
+
() =>
|
|
692
|
+
Gesture.Tap()
|
|
693
|
+
.enabled(Platform.OS !== 'web')
|
|
694
|
+
.numberOfTaps(1)
|
|
695
|
+
.onEnd((event, success) => {
|
|
696
|
+
if (!success) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
scheduleOnRN(emitSingleTap, event.x, event.y);
|
|
701
|
+
}),
|
|
702
|
+
[emitSingleTap],
|
|
703
|
+
);
|
|
704
|
+
|
|
650
705
|
const doubleTapGesture = useMemo(
|
|
651
706
|
() =>
|
|
652
707
|
Gesture.Tap()
|
|
@@ -667,9 +722,14 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
667
722
|
[enableDoubleTapZoom, height, maxZoomScale, scale, translateX, translateY, width],
|
|
668
723
|
);
|
|
669
724
|
|
|
725
|
+
const tapGesture = useMemo(
|
|
726
|
+
() => Gesture.Exclusive(doubleTapGesture, singleTapGesture),
|
|
727
|
+
[doubleTapGesture, singleTapGesture],
|
|
728
|
+
);
|
|
729
|
+
|
|
670
730
|
const zoomGesture = useMemo(
|
|
671
|
-
() => Gesture.Race(zoomPinchGesture, Gesture.Exclusive(zoomPanGesture,
|
|
672
|
-
[zoomPinchGesture, zoomPanGesture,
|
|
731
|
+
() => Gesture.Race(zoomPinchGesture, Gesture.Exclusive(zoomPanGesture, tapGesture)),
|
|
732
|
+
[zoomPinchGesture, zoomPanGesture, tapGesture],
|
|
673
733
|
);
|
|
674
734
|
|
|
675
735
|
const animatedStyle = useAnimatedStyle(() => ({
|
|
@@ -693,39 +753,40 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
693
753
|
return { opacity: baseOpacity };
|
|
694
754
|
}
|
|
695
755
|
|
|
696
|
-
const
|
|
756
|
+
const dismissDistance = getDismissDistance(translateY.value, dismissOptions.direction);
|
|
757
|
+
const dismissOpacity = interpolate(dismissDistance, [0, 200], [1, 0], 'clamp');
|
|
697
758
|
|
|
698
759
|
return { opacity: baseOpacity * dismissOpacity };
|
|
699
|
-
}, [dismissOptions.fadeBackdrop]);
|
|
760
|
+
}, [dismissOptions.direction, dismissOptions.fadeBackdrop]);
|
|
700
761
|
|
|
701
762
|
const nativeScrollGesture = useMemo(() => {
|
|
702
763
|
return Gesture.Native().requireExternalGestureToFail(dismissGestureRef);
|
|
703
764
|
}, []);
|
|
704
765
|
|
|
705
|
-
const { onMomentumScrollEnd, onScroll, onScrollBeginDrag,
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
766
|
+
const { onMomentumScrollEnd, onScroll, onScrollBeginDrag, onWebClick } = useGestureViewerPaging({
|
|
767
|
+
adjustedInitialIndex,
|
|
768
|
+
autoPlay,
|
|
769
|
+
autoPlayInterval,
|
|
770
|
+
currentIndex,
|
|
771
|
+
dataLength,
|
|
772
|
+
enableDoubleTapZoom,
|
|
773
|
+
enableHorizontalSwipe,
|
|
774
|
+
enableLoop,
|
|
775
|
+
height,
|
|
776
|
+
isRotated,
|
|
777
|
+
isZoomed,
|
|
778
|
+
itemSpacing,
|
|
779
|
+
manager,
|
|
780
|
+
maxZoomScale,
|
|
781
|
+
onSingleTap: emitSingleTap,
|
|
782
|
+
scale,
|
|
783
|
+
scrollTo,
|
|
784
|
+
syncCurrentIndex,
|
|
785
|
+
syncPendingIndex,
|
|
786
|
+
translateX,
|
|
787
|
+
translateY,
|
|
788
|
+
width,
|
|
789
|
+
});
|
|
729
790
|
|
|
730
791
|
return {
|
|
731
792
|
animatedStyle,
|
|
@@ -739,7 +800,7 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
739
800
|
isZoomed,
|
|
740
801
|
listRef,
|
|
741
802
|
nativeScrollGesture,
|
|
742
|
-
|
|
803
|
+
onWebClick,
|
|
743
804
|
onMomentumScrollEnd,
|
|
744
805
|
onScroll,
|
|
745
806
|
|
|
@@ -8,7 +8,7 @@ import type { GestureViewerEventCallback, GestureViewerEventType } from './types
|
|
|
8
8
|
* Hook for subscribing to GestureViewer events on the default instance.
|
|
9
9
|
*
|
|
10
10
|
* This hook allows you to listen to specific events from the default GestureViewer instance
|
|
11
|
-
* (with ID 'default'), such as zoom changes or
|
|
11
|
+
* (with ID 'default'), such as zoom changes, rotation changes, or confirmed tap events. Events are automatically
|
|
12
12
|
* throttled to prevent excessive callback invocations during gestures.
|
|
13
13
|
*
|
|
14
14
|
* @param eventType - The type of event to listen for
|
|
@@ -25,6 +25,13 @@ import type { GestureViewerEventCallback, GestureViewerEventType } from './types
|
|
|
25
25
|
* useGestureViewerEvent('rotationChange', (data) => {
|
|
26
26
|
* console.log(`Rotation changed from ${data.previousRotation}° to ${data.rotation}°`);
|
|
27
27
|
* });
|
|
28
|
+
*
|
|
29
|
+
* // Listen to confirmed single taps on the default instance (ID: 'default')
|
|
30
|
+
* useGestureViewerEvent('tap', (data) => {
|
|
31
|
+
* if (data.kind === 'single') {
|
|
32
|
+
* console.log(`Tapped item ${data.index} at (${data.x}, ${data.y})`);
|
|
33
|
+
* }
|
|
34
|
+
* });
|
|
28
35
|
* ```
|
|
29
36
|
*/
|
|
30
37
|
export function useGestureViewerEvent<T extends GestureViewerEventType>(
|
|
@@ -53,6 +60,13 @@ export function useGestureViewerEvent<T extends GestureViewerEventType>(
|
|
|
53
60
|
* useGestureViewerEvent('modal-viewer', 'rotationChange', (data) => {
|
|
54
61
|
* updateRotationIndicator(data.rotation);
|
|
55
62
|
* });
|
|
63
|
+
*
|
|
64
|
+
* // Listen to confirmed single taps on a specific instance
|
|
65
|
+
* useGestureViewerEvent('gallery', 'tap', (data) => {
|
|
66
|
+
* if (data.kind === 'single') {
|
|
67
|
+
* console.log(`Gallery tap on item ${data.index}`);
|
|
68
|
+
* }
|
|
69
|
+
* });
|
|
56
70
|
* ```
|
|
57
71
|
*/
|
|
58
72
|
export function useGestureViewerEvent<T extends GestureViewerEventType>(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MouseEvent } from 'react';
|
|
1
2
|
import type { ScrollViewProps } from 'react-native';
|
|
2
3
|
import type { SharedValue } from 'react-native-reanimated';
|
|
3
4
|
|
|
@@ -18,6 +19,7 @@ export type UseGestureViewerPagingArgs = {
|
|
|
18
19
|
itemSpacing: number;
|
|
19
20
|
manager: GestureViewerManager | null;
|
|
20
21
|
maxZoomScale: number;
|
|
22
|
+
onSingleTap?: (x: number, y: number) => void;
|
|
21
23
|
scale: SharedValue<number>;
|
|
22
24
|
scrollTo: (index: number, animated: boolean) => void;
|
|
23
25
|
syncCurrentIndex: (nextIndex: number) => void;
|
|
@@ -27,9 +29,13 @@ export type UseGestureViewerPagingArgs = {
|
|
|
27
29
|
width: number;
|
|
28
30
|
};
|
|
29
31
|
|
|
32
|
+
export type WebClickTarget = {
|
|
33
|
+
getBoundingClientRect: () => { left: number; top: number };
|
|
34
|
+
};
|
|
35
|
+
|
|
30
36
|
export type UseGestureViewerPagingResult = {
|
|
31
37
|
onMomentumScrollEnd?: ScrollViewProps['onMomentumScrollEnd'];
|
|
32
38
|
onScroll?: ScrollViewProps['onScroll'];
|
|
33
39
|
onScrollBeginDrag?: ScrollViewProps['onScrollBeginDrag'];
|
|
34
|
-
|
|
40
|
+
onWebClick?: (event: MouseEvent<WebClickTarget>) => void;
|
|
35
41
|
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef, type MouseEvent } from 'react';
|
|
2
2
|
import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
5
|
UseGestureViewerPagingArgs,
|
|
6
6
|
UseGestureViewerPagingResult,
|
|
7
|
+
WebClickTarget,
|
|
7
8
|
} from './useGestureViewerPaging.types';
|
|
8
9
|
import { applyTapZoomAtPoint } from './utils/tapZoom';
|
|
9
10
|
import {
|
|
@@ -35,6 +36,7 @@ export function useGestureViewerPaging({
|
|
|
35
36
|
enableHorizontalSwipe,
|
|
36
37
|
enableLoop,
|
|
37
38
|
height,
|
|
39
|
+
onSingleTap,
|
|
38
40
|
isRotated,
|
|
39
41
|
isZoomed,
|
|
40
42
|
itemSpacing,
|
|
@@ -48,6 +50,8 @@ export function useGestureViewerPaging({
|
|
|
48
50
|
translateY,
|
|
49
51
|
width,
|
|
50
52
|
}: UseGestureViewerPagingArgs): UseGestureViewerPagingResult {
|
|
53
|
+
const webSingleTapTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
54
|
+
|
|
51
55
|
const webScrollRuntimeRef = useRef<WebScrollRuntime>({
|
|
52
56
|
actor: 'idle',
|
|
53
57
|
isAutoplayPausedByUser: false,
|
|
@@ -59,6 +63,13 @@ export function useGestureViewerPaging({
|
|
|
59
63
|
resumeAutoplayTimer: null,
|
|
60
64
|
});
|
|
61
65
|
|
|
66
|
+
const clearWebSingleTapTimer = useCallback(() => {
|
|
67
|
+
if (webSingleTapTimerRef.current) {
|
|
68
|
+
clearTimeout(webSingleTapTimerRef.current);
|
|
69
|
+
webSingleTapTimerRef.current = null;
|
|
70
|
+
}
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
62
73
|
const clearWebSettleTimer = useCallback(() => {
|
|
63
74
|
const runtime = webScrollRuntimeRef.current;
|
|
64
75
|
|
|
@@ -232,10 +243,11 @@ export function useGestureViewerPaging({
|
|
|
232
243
|
|
|
233
244
|
useEffect(() => {
|
|
234
245
|
return () => {
|
|
246
|
+
clearWebSingleTapTimer();
|
|
235
247
|
clearWebSettleTimer();
|
|
236
248
|
clearWebAutoplayResumeTimer();
|
|
237
249
|
};
|
|
238
|
-
}, [clearWebAutoplayResumeTimer, clearWebSettleTimer]);
|
|
250
|
+
}, [clearWebAutoplayResumeTimer, clearWebSettleTimer, clearWebSingleTapTimer]);
|
|
239
251
|
|
|
240
252
|
useEffect(() => {
|
|
241
253
|
if (
|
|
@@ -351,50 +363,62 @@ export function useGestureViewerPaging({
|
|
|
351
363
|
manager?.handleScrollBeginDrag();
|
|
352
364
|
}, [beginWebUserInteraction, clearWebSettleTimer, manager]);
|
|
353
365
|
|
|
354
|
-
const
|
|
355
|
-
(event:
|
|
356
|
-
|
|
366
|
+
const onWebClick = useCallback(
|
|
367
|
+
(event: MouseEvent<WebClickTarget>) => {
|
|
368
|
+
const detail = event.detail;
|
|
369
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
370
|
+
const resolvedX = event.clientX - rect.left;
|
|
371
|
+
const resolvedY = event.clientY - rect.top;
|
|
372
|
+
|
|
373
|
+
if (!enableDoubleTapZoom) {
|
|
374
|
+
if (!onSingleTap) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
clearWebSingleTapTimer();
|
|
379
|
+
onSingleTap(resolvedX, resolvedY);
|
|
357
380
|
return;
|
|
358
381
|
}
|
|
359
382
|
|
|
360
|
-
|
|
361
|
-
|
|
383
|
+
if (detail === 2) {
|
|
384
|
+
clearWebSingleTapTimer();
|
|
362
385
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
let locationX = nativeEvent?.locationX;
|
|
366
|
-
let locationY = nativeEvent?.locationY;
|
|
386
|
+
pauseWebAutoplayWithoutPagingInteraction();
|
|
387
|
+
scheduleWebAutoplayResume();
|
|
367
388
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
389
|
+
applyTapZoomAtPoint({
|
|
390
|
+
x: resolvedX,
|
|
391
|
+
y: resolvedY,
|
|
392
|
+
width,
|
|
393
|
+
height,
|
|
394
|
+
maxZoomScale,
|
|
395
|
+
scale,
|
|
396
|
+
translateX,
|
|
397
|
+
translateY,
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
375
401
|
|
|
376
|
-
|
|
377
|
-
|
|
402
|
+
if (detail !== 1 || !onSingleTap) {
|
|
403
|
+
return;
|
|
378
404
|
}
|
|
379
405
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
scale,
|
|
387
|
-
translateX,
|
|
388
|
-
translateY,
|
|
389
|
-
});
|
|
406
|
+
clearWebSingleTapTimer();
|
|
407
|
+
|
|
408
|
+
webSingleTapTimerRef.current = setTimeout(() => {
|
|
409
|
+
onSingleTap(resolvedX, resolvedY);
|
|
410
|
+
webSingleTapTimerRef.current = null;
|
|
411
|
+
}, 250);
|
|
390
412
|
},
|
|
391
413
|
[
|
|
414
|
+
clearWebSingleTapTimer,
|
|
392
415
|
enableDoubleTapZoom,
|
|
393
416
|
height,
|
|
394
417
|
maxZoomScale,
|
|
418
|
+
onSingleTap,
|
|
395
419
|
pauseWebAutoplayWithoutPagingInteraction,
|
|
396
|
-
scheduleWebAutoplayResume,
|
|
397
420
|
scale,
|
|
421
|
+
scheduleWebAutoplayResume,
|
|
398
422
|
translateX,
|
|
399
423
|
translateY,
|
|
400
424
|
width,
|
|
@@ -405,6 +429,6 @@ export function useGestureViewerPaging({
|
|
|
405
429
|
onMomentumScrollEnd,
|
|
406
430
|
onScroll,
|
|
407
431
|
onScrollBeginDrag,
|
|
408
|
-
|
|
432
|
+
onWebClick,
|
|
409
433
|
};
|
|
410
434
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { GestureViewerDismissDirection } from '../types';
|
|
2
|
+
|
|
3
|
+
export const shouldDismissByDirection = (
|
|
4
|
+
translationY: number,
|
|
5
|
+
threshold: number,
|
|
6
|
+
direction: GestureViewerDismissDirection,
|
|
7
|
+
): boolean => {
|
|
8
|
+
'worklet';
|
|
9
|
+
|
|
10
|
+
if (direction === 'up') {
|
|
11
|
+
return translationY < -threshold;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (direction === 'both') {
|
|
15
|
+
return Math.abs(translationY) > threshold;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return translationY > threshold;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const getDismissDistance = (
|
|
22
|
+
translationY: number,
|
|
23
|
+
direction: GestureViewerDismissDirection,
|
|
24
|
+
): number => {
|
|
25
|
+
'worklet';
|
|
26
|
+
|
|
27
|
+
if (direction === 'up') {
|
|
28
|
+
return Math.max(-translationY, 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (direction === 'both') {
|
|
32
|
+
return Math.abs(translationY);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Math.max(translationY, 0);
|
|
36
|
+
};
|