react-native-gesture-image-viewer 2.1.3 → 2.2.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.
Files changed (55) hide show
  1. package/lib/module/GestureTrigger.js +22 -5
  2. package/lib/module/GestureTrigger.js.map +1 -1
  3. package/lib/module/GestureViewer.js +50 -40
  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 +52 -8
  8. package/lib/module/GestureViewerRegistry.js.map +1 -1
  9. package/lib/module/useGestureViewer.js +149 -113
  10. package/lib/module/useGestureViewer.js.map +1 -1
  11. package/lib/module/useGestureViewerPaging.js +82 -0
  12. package/lib/module/useGestureViewerPaging.js.map +1 -0
  13. package/lib/module/useGestureViewerPaging.types.js +4 -0
  14. package/lib/module/useGestureViewerPaging.types.js.map +1 -0
  15. package/lib/module/useGestureViewerPaging.web.js +254 -0
  16. package/lib/module/useGestureViewerPaging.web.js.map +1 -0
  17. package/lib/module/utils/index.js +9 -0
  18. package/lib/module/utils/index.js.map +1 -1
  19. package/lib/module/utils/tapZoom.js +34 -0
  20. package/lib/module/utils/tapZoom.js.map +1 -0
  21. package/lib/module/utils/webScroll.js +70 -0
  22. package/lib/module/utils/webScroll.js.map +1 -0
  23. package/lib/typescript/src/GestureTrigger.d.ts +9 -3
  24. package/lib/typescript/src/GestureTrigger.d.ts.map +1 -1
  25. package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
  26. package/lib/typescript/src/GestureViewerManager.d.ts +5 -0
  27. package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
  28. package/lib/typescript/src/GestureViewerRegistry.d.ts +11 -4
  29. package/lib/typescript/src/GestureViewerRegistry.d.ts.map +1 -1
  30. package/lib/typescript/src/useGestureViewer.d.ts +4 -3
  31. package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
  32. package/lib/typescript/src/useGestureViewerPaging.d.ts +3 -0
  33. package/lib/typescript/src/useGestureViewerPaging.d.ts.map +1 -0
  34. package/lib/typescript/src/useGestureViewerPaging.types.d.ts +33 -0
  35. package/lib/typescript/src/useGestureViewerPaging.types.d.ts.map +1 -0
  36. package/lib/typescript/src/useGestureViewerPaging.web.d.ts +3 -0
  37. package/lib/typescript/src/useGestureViewerPaging.web.d.ts.map +1 -0
  38. package/lib/typescript/src/utils/index.d.ts +7 -0
  39. package/lib/typescript/src/utils/index.d.ts.map +1 -1
  40. package/lib/typescript/src/utils/tapZoom.d.ts +12 -0
  41. package/lib/typescript/src/utils/tapZoom.d.ts.map +1 -0
  42. package/lib/typescript/src/utils/webScroll.d.ts +18 -0
  43. package/lib/typescript/src/utils/webScroll.d.ts.map +1 -0
  44. package/package.json +1 -1
  45. package/src/GestureTrigger.tsx +31 -6
  46. package/src/GestureViewer.tsx +39 -15
  47. package/src/GestureViewerManager.ts +29 -6
  48. package/src/GestureViewerRegistry.ts +67 -8
  49. package/src/useGestureViewer.ts +174 -165
  50. package/src/useGestureViewerPaging.ts +128 -0
  51. package/src/useGestureViewerPaging.types.ts +35 -0
  52. package/src/useGestureViewerPaging.web.ts +410 -0
  53. package/src/utils/index.ts +17 -1
  54. package/src/utils/tapZoom.ts +44 -0
  55. package/src/utils/webScroll.ts +96 -0
@@ -0,0 +1,33 @@
1
+ import type { ScrollViewProps } from 'react-native';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+ import type GestureViewerManager from './GestureViewerManager';
4
+ export type UseGestureViewerPagingArgs = {
5
+ adjustedInitialIndex: number;
6
+ autoPlay: boolean;
7
+ autoPlayInterval: number;
8
+ currentIndex: number;
9
+ dataLength: number;
10
+ enableDoubleTapZoom: boolean;
11
+ enableHorizontalSwipe: boolean;
12
+ enableLoop: boolean;
13
+ height: number;
14
+ isRotated: boolean;
15
+ isZoomed: boolean;
16
+ itemSpacing: number;
17
+ manager: GestureViewerManager | null;
18
+ maxZoomScale: number;
19
+ scale: SharedValue<number>;
20
+ scrollTo: (index: number, animated: boolean) => void;
21
+ syncCurrentIndex: (nextIndex: number) => void;
22
+ syncPendingIndex: (nextIndex: number) => void;
23
+ translateX: SharedValue<number>;
24
+ translateY: SharedValue<number>;
25
+ width: number;
26
+ };
27
+ export type UseGestureViewerPagingResult = {
28
+ onMomentumScrollEnd?: ScrollViewProps['onMomentumScrollEnd'];
29
+ onScroll?: ScrollViewProps['onScroll'];
30
+ onScrollBeginDrag?: ScrollViewProps['onScrollBeginDrag'];
31
+ onWebDoubleClick?: (event: any) => void;
32
+ };
33
+ //# sourceMappingURL=useGestureViewerPaging.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGestureViewerPaging.types.d.ts","sourceRoot":"","sources":["../../../src/useGestureViewerPaging.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,KAAK,oBAAoB,MAAM,wBAAwB,CAAC;AAE/D,MAAM,MAAM,0BAA0B,GAAG;IACvC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IACrD,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,mBAAmB,CAAC,EAAE,eAAe,CAAC,qBAAqB,CAAC,CAAC;IAC7D,QAAQ,CAAC,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;IACvC,iBAAiB,CAAC,EAAE,eAAe,CAAC,mBAAmB,CAAC,CAAC;IACzD,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;CACzC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { UseGestureViewerPagingArgs, UseGestureViewerPagingResult } from './useGestureViewerPaging.types';
2
+ export declare function useGestureViewerPaging({ adjustedInitialIndex, autoPlay, autoPlayInterval, currentIndex, dataLength, enableDoubleTapZoom, enableHorizontalSwipe, enableLoop, height, isRotated, isZoomed, itemSpacing, manager, maxZoomScale, scale, scrollTo, syncCurrentIndex, syncPendingIndex, translateX, translateY, width, }: UseGestureViewerPagingArgs): UseGestureViewerPagingResult;
3
+ //# sourceMappingURL=useGestureViewerPaging.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGestureViewerPaging.web.d.ts","sourceRoot":"","sources":["../../../src/useGestureViewerPaging.web.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,0BAA0B,EAC1B,4BAA4B,EAC7B,MAAM,gCAAgC,CAAC;AAqBxC,wBAAgB,sBAAsB,CAAC,EACrC,oBAAoB,EACpB,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,mBAAmB,EACnB,qBAAqB,EACrB,UAAU,EACV,MAAM,EACN,SAAS,EACT,QAAQ,EACR,WAAW,EACX,OAAO,EACP,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,KAAK,GACN,EAAE,0BAA0B,GAAG,4BAA4B,CAwW3D"}
@@ -1,7 +1,14 @@
1
+ import { type PlatformOSType } from 'react-native';
1
2
  import type { FlatListComponent, ScrollViewComponent } from '../types';
2
3
  export declare const isScrollViewLike: (component: React.ComponentType<any>) => component is ScrollViewComponent;
3
4
  export declare const isFlatListLike: (component: React.ComponentType<any>) => component is FlatListComponent<any>;
4
5
  export declare const isFlashListLike: (component: React.ComponentType<any>) => boolean;
6
+ /**
7
+ * iOS needs the extra Native gesture wrapper so the dismiss pan can resolve before the scrollable claims touches.
8
+ * We intentionally skip Android because the same require-to-fail relation regressed horizontal paging there,
9
+ * and we also avoid wrapping RNGH-provided scrollables to prevent double-applying Gesture.Native() to components that already participate in RNGH.
10
+ */
11
+ export declare const shouldUseNativeScrollGesture: (platformOS: PlatformOSType, component: React.ComponentType<any>) => boolean;
5
12
  export declare const createBoundsConstraint: ({ width, height }: {
6
13
  width: number;
7
14
  height: number;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/utils/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAIvE,eAAO,MAAM,gBAAgB,GAC3B,WAAW,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,KAClC,SAAS,IAAI,mBAC+C,CAAC;AAEhE,eAAO,MAAM,cAAc,GACzB,WAAW,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,KAClC,SAAS,IAAI,iBAAiB,CAAC,GAAG,CAMpC,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,WAAW,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,KAAG,OAMrE,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAChC,mBAAmB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,MACpD,oCAIE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;;;CAiBA,CAAC;AAEJ,eAAO,MAAM,cAAc,GAAI,CAAC,EAAE,SAAS,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,OAAO,KAAG,CAAC,EAevF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,aAAa,MAAM,EACnB,oBAAoB,MAAM,EAC1B,YAAY,OAAO,KAClB;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAoB/D,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,SAAS,GAAG,EAAE,OAAO,MAAM;sBAC1C,MAAM,YAAY,OAAO;CAO3C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,cAAc,CAAC;AAMtB,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAIvE,eAAO,MAAM,gBAAgB,GAC3B,WAAW,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,KAClC,SAAS,IAAI,mBAC+C,CAAC;AAEhE,eAAO,MAAM,cAAc,GACzB,WAAW,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,KAClC,SAAS,IAAI,iBAAiB,CAAC,GAAG,CAMpC,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,WAAW,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,KAAG,OAMrE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,GACvC,YAAY,cAAc,EAC1B,WAAW,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,KAClC,OAEF,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAChC,mBAAmB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,MACpD,oCAIE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;;;CAiBA,CAAC;AAEJ,eAAO,MAAM,cAAc,GAAI,CAAC,EAAE,SAAS,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,OAAO,KAAG,CAAC,EAevF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,aAAa,MAAM,EACnB,oBAAoB,MAAM,EAC1B,YAAY,OAAO,KAClB;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAoB/D,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,SAAS,GAAG,EAAE,OAAO,MAAM;sBAC1C,MAAM,YAAY,OAAO;CAO3C,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { SharedValue } from 'react-native-reanimated';
2
+ export declare const applyTapZoomAtPoint: ({ x, y, width, height, maxZoomScale, scale, translateX, translateY, }: {
3
+ x: number;
4
+ y: number;
5
+ width: number;
6
+ height: number;
7
+ maxZoomScale: number;
8
+ scale: SharedValue<number>;
9
+ translateX: SharedValue<number>;
10
+ translateY: SharedValue<number>;
11
+ }) => void;
12
+ //# sourceMappingURL=tapZoom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tapZoom.d.ts","sourceRoot":"","sources":["../../../../src/utils/tapZoom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAG3D,eAAO,MAAM,mBAAmB,GAAI,uEASjC;IACD,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACjC,SAsBA,CAAC"}
@@ -0,0 +1,18 @@
1
+ export declare const getWebScrollPhysicalIndex: (offsetX: number, pageWidth: number) => number;
2
+ export declare const resolveWebScrollFinalState: ({ dataLength, enableLoop, lastSettledPhysicalIndex, offsetX, pageWidth, }: {
3
+ offsetX: number;
4
+ pageWidth: number;
5
+ lastSettledPhysicalIndex: number;
6
+ dataLength: number;
7
+ enableLoop: boolean;
8
+ }) => {
9
+ logicalIndex: number;
10
+ rawPhysicalIndex: number;
11
+ settledPhysicalIndex: number;
12
+ } | null;
13
+ export declare const getWebAutoPlayTargetPhysicalIndex: ({ currentIndex, dataLength, enableLoop, }: {
14
+ currentIndex: number;
15
+ dataLength: number;
16
+ enableLoop: boolean;
17
+ }) => number | null;
18
+ //# sourceMappingURL=webScroll.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webScroll.d.ts","sourceRoot":"","sources":["../../../../src/utils/webScroll.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,yBAAyB,GAAI,SAAS,MAAM,EAAE,WAAW,MAAM,KAAG,MAM9E,CAAC;AAEF,eAAO,MAAM,0BAA0B,GAAI,2EAMxC;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,wBAAwB,EAAE,MAAM,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;CACrB,KAAG;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,oBAAoB,EAAE,MAAM,CAAA;CAAE,GAAG,IAiDtF,CAAC;AAEF,eAAO,MAAM,iCAAiC,GAAI,2CAI/C;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;CACrB,KAAG,MAAM,GAAG,IAgBZ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-gesture-image-viewer",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "description": "🖼️ React Native Image Viewer - Reanimated-powered image gestures with full control",
5
5
  "keywords": [
6
6
  "android",
@@ -1,5 +1,5 @@
1
1
  import type { ReactElement } from 'react';
2
- import { Children, cloneElement, isValidElement, useMemo, useRef } from 'react';
2
+ import { Children, cloneElement, isValidElement, useCallback, useMemo, useRef } from 'react';
3
3
  import { View } from 'react-native';
4
4
 
5
5
  import { registry } from './GestureViewerRegistry';
@@ -16,11 +16,13 @@ type WithOnPress = { onPress?: (...args: unknown[]) => void };
16
16
  * @typeParam T - The child's props type. Must include an optional `onPress` handler.
17
17
  *
18
18
  * @property id - Optional identifier to associate this trigger with a `GestureViewer`.
19
+ * @property index - Optional item index used to align dismiss animations with the current viewer index.
19
20
  * @property children - A single React element whose props include `onPress` (e.g., `Pressable`, `Touchable*`).
20
21
  * @property onPress - Optional handler invoked after the child's own `onPress`.
21
22
  */
22
23
  export type GestureTriggerProps<T extends WithOnPress> = {
23
24
  id?: string;
25
+ index?: number;
24
26
  children: ReactElement<T>;
25
27
  onPress?: (...args: unknown[]) => void;
26
28
  };
@@ -30,15 +32,19 @@ export type GestureTriggerProps<T extends WithOnPress> = {
30
32
  *
31
33
  * @remarks
32
34
  * Behavior on press:
33
- * - Registers the child's native node to the internal registry under the given `id`.
35
+ * - Registers the child's native node as the active trigger under the given `id`.
34
36
  * - Invokes the child's own `onPress` first (if provided).
35
37
  * - Invokes the `onPress` passed to `GestureTrigger` next (if provided).
36
38
  *
39
+ * When `index` is provided, the trigger is also registered by item index while mounted,
40
+ * allowing dismiss animations to target the currently visible item instead of only the opening trigger.
41
+ *
37
42
  * Type parameters:
38
43
  * - `T` — The child's props type. Must include an optional `onPress` handler, ensuring the child is pressable.
39
44
  *
40
45
  * Props:
41
46
  * - `id` — Optional identifier to associate this trigger with a `GestureViewer`. Defaults to `"default"`.
47
+ * - `index` — Optional item index used to support dismiss animations back to the current item trigger.
42
48
  * - `children` — A single React element whose props include `onPress` (e.g., `Pressable`, `Touchable*`, custom button).
43
49
  * - `onPress` — Optional handler invoked after the child's `onPress`. Receives the same arguments as the child's handler.
44
50
  *
@@ -48,7 +54,7 @@ export type GestureTriggerProps<T extends WithOnPress> = {
48
54
  *
49
55
  * Example:
50
56
  * ```tsx
51
- * <GestureTrigger id="gallery" onPress={() => openModal(index)}>
57
+ * <GestureTrigger id="gallery" index={index} onPress={() => openModal(index)}>
52
58
  * <Pressable style={styles.thumb}>
53
59
  * <Image source={{ uri }} style={styles.thumbImage} />
54
60
  * </Pressable>
@@ -57,10 +63,29 @@ export type GestureTriggerProps<T extends WithOnPress> = {
57
63
  */
58
64
  export function GestureTrigger<T extends WithOnPress>({
59
65
  id = 'default',
66
+ index,
60
67
  children,
61
68
  onPress,
62
69
  }: GestureTriggerProps<T>) {
63
- const ref = useRef<View>(null);
70
+ const ref = useRef<View | null>(null);
71
+
72
+ const handleRef = useCallback(
73
+ (node: View | null) => {
74
+ ref.current = node;
75
+
76
+ if (index === undefined) {
77
+ return;
78
+ }
79
+
80
+ if (node) {
81
+ registry.setIndexedTriggerNode(id, index, node);
82
+ return;
83
+ }
84
+
85
+ registry.clearIndexedTriggerNode(id, index);
86
+ },
87
+ [id, index],
88
+ );
64
89
 
65
90
  const wrapped = useMemo(() => {
66
91
  const child = Children.only(children);
@@ -72,7 +97,7 @@ export function GestureTrigger<T extends WithOnPress>({
72
97
  const originalOnPress = child.props?.onPress;
73
98
 
74
99
  const handlePress = (...args: unknown[]) => {
75
- registry.setTriggerNode(id, ref.current);
100
+ registry.setActiveTriggerNode(id, ref.current);
76
101
  originalOnPress?.(...args);
77
102
  onPress?.(...args);
78
103
 
@@ -90,7 +115,7 @@ export function GestureTrigger<T extends WithOnPress>({
90
115
  }, [id, onPress, children]);
91
116
 
92
117
  return (
93
- <View ref={ref} collapsable={false}>
118
+ <View ref={handleRef} collapsable={false}>
94
119
  {wrapped}
95
120
  </View>
96
121
  );
@@ -12,7 +12,13 @@ import Animated from 'react-native-reanimated';
12
12
  import { registry } from './GestureViewerRegistry';
13
13
  import type { GestureViewerProps } from './types';
14
14
  import { useGestureViewer } from './useGestureViewer';
15
- import { createLoopData, isFlashListLike, isFlatListLike, isScrollViewLike } from './utils';
15
+ import {
16
+ createLoopData,
17
+ isFlashListLike,
18
+ isFlatListLike,
19
+ isScrollViewLike,
20
+ shouldUseNativeScrollGesture,
21
+ } from './utils';
16
22
  import WebPagingFixStyle from './WebPagingFixStyle';
17
23
 
18
24
  export function GestureViewer<ItemT, LC>({
@@ -53,7 +59,9 @@ export function GestureViewer<ItemT, LC>({
53
59
  dismissGesture,
54
60
  zoomGesture,
55
61
  nativeScrollGesture,
62
+ onWebDoubleClick,
56
63
  onMomentumScrollEnd,
64
+ onScroll,
57
65
  onScrollBeginDrag,
58
66
  animatedStyle,
59
67
  backdropStyle,
@@ -130,7 +138,8 @@ export function GestureViewer<ItemT, LC>({
130
138
  horizontal: true,
131
139
  scrollEnabled: !isZoomed && !isRotated && !isPinching,
132
140
  showsHorizontalScrollIndicator: false,
133
- onMomentumScrollEnd: onMomentumScrollEnd,
141
+ onMomentumScrollEnd,
142
+ onScroll,
134
143
  onScrollBeginDrag,
135
144
  ...(enableSnapMode
136
145
  ? {
@@ -151,6 +160,7 @@ export function GestureViewer<ItemT, LC>({
151
160
  isRotated,
152
161
  isPinching,
153
162
  onMomentumScrollEnd,
163
+ onScroll,
154
164
  onScrollBeginDrag,
155
165
  enableSnapMode,
156
166
  ],
@@ -158,6 +168,22 @@ export function GestureViewer<ItemT, LC>({
158
168
 
159
169
  const control = useMemo(() => ({ dismiss: handleDismiss }), [handleDismiss]);
160
170
 
171
+ const shouldWrapScrollableWithNativeGesture = shouldUseNativeScrollGesture(
172
+ Platform.OS,
173
+ Component,
174
+ );
175
+
176
+ const maybeWrapWithNativeScrollGesture = useCallback(
177
+ (children: React.ReactNode) => {
178
+ if (!shouldWrapScrollableWithNativeGesture) {
179
+ return children;
180
+ }
181
+
182
+ return <GestureDetector gesture={nativeScrollGesture}>{children}</GestureDetector>;
183
+ },
184
+ [nativeScrollGesture, shouldWrapScrollableWithNativeGesture],
185
+ );
186
+
161
187
  const listComponent = (
162
188
  <GestureHandlerRootView>
163
189
  <GestureDetector gesture={gesture}>
@@ -165,18 +191,18 @@ export function GestureViewer<ItemT, LC>({
165
191
  <Animated.View style={[styles.background, backdropStyleProps, backdropStyle]} />
166
192
  <Animated.View
167
193
  style={[styles.content, animatedStyle]}
194
+ {...(Platform.OS === 'web' && { onClick: onWebDoubleClick })}
168
195
  {...(Platform.OS === 'web' &&
169
196
  isFlashListLike(Component) && { dataSet: { 'flash-list-paging-enabled-fix': true } })}
170
197
  >
171
- {isScrollView ? (
172
- <GestureDetector gesture={nativeScrollGesture}>
173
- <Component ref={listRef} {...commonProps} {...listProps}>
174
- {loopData.map((item, index) => renderItem({ item, index }))}
175
- </Component>
176
- </GestureDetector>
177
- ) : (
178
- isFlatListLike(Component) && (
179
- <GestureDetector gesture={nativeScrollGesture}>
198
+ {isScrollView
199
+ ? maybeWrapWithNativeScrollGesture(
200
+ <Component ref={listRef} {...commonProps} {...listProps}>
201
+ {loopData.map((item, index) => renderItem({ item, index }))}
202
+ </Component>,
203
+ )
204
+ : isFlatListLike(Component) &&
205
+ maybeWrapWithNativeScrollGesture(
180
206
  <Component
181
207
  ref={listRef}
182
208
  {...commonProps}
@@ -196,10 +222,8 @@ export function GestureViewer<ItemT, LC>({
196
222
  dataSet: { 'flat-list-paging-enabled-fix': true },
197
223
  })}
198
224
  {...listProps}
199
- />
200
- </GestureDetector>
201
- )
202
- )}
225
+ />,
226
+ )}
203
227
  </Animated.View>
204
228
  <WebPagingFixStyle Component={Component} />
205
229
  </View>
@@ -22,8 +22,10 @@ class GestureViewerManager {
22
22
  private rotation: SharedValue<number> | null = null;
23
23
  private translateX: SharedValue<number> | null = null;
24
24
  private translateY: SharedValue<number> | null = null;
25
+ private resetTransformCallback: (() => void) | null = null;
25
26
 
26
27
  private loopCallback: (() => void) | null = null;
28
+ private programmaticScrollVersion = 0;
27
29
 
28
30
  private listeners = new Set<(state: GestureViewerState) => void>();
29
31
  private eventListeners = new Map<GestureViewerEventType, Set<(data: any) => void>>();
@@ -91,6 +93,10 @@ class GestureViewerManager {
91
93
  };
92
94
  }
93
95
 
96
+ getProgrammaticScrollVersion() {
97
+ return this.programmaticScrollVersion;
98
+ }
99
+
94
100
  setEnableLoop(enabled: boolean) {
95
101
  this.enableLoop = enabled;
96
102
  }
@@ -133,6 +139,10 @@ class GestureViewerManager {
133
139
  this.maxZoomScale = maxZoomScale;
134
140
  }
135
141
 
142
+ setResetTransformCallback(callback: (() => void) | null) {
143
+ this.resetTransformCallback = callback;
144
+ }
145
+
136
146
  notifyStateChange() {
137
147
  this.notifyListeners();
138
148
  }
@@ -255,7 +265,7 @@ class GestureViewerManager {
255
265
  return;
256
266
  }
257
267
 
258
- this.loopCallback = null;
268
+ this.cancelPendingLoopTransition();
259
269
 
260
270
  const { scrollTo } = createScrollAction(this.listRef, this.width);
261
271
 
@@ -264,9 +274,11 @@ class GestureViewerManager {
264
274
  this.loopCallback = () => {
265
275
  scrollTo(this.dataLength, false);
266
276
  this.updateCurrentIndex(this.dataLength - 1);
267
- this.loopCallback = null;
277
+ this.cancelPendingLoopTransition();
268
278
  };
269
279
 
280
+ this.resetTransformCallback?.();
281
+ this.programmaticScrollVersion += 1;
270
282
  scrollTo(0, true);
271
283
  return;
272
284
  }
@@ -275,13 +287,17 @@ class GestureViewerManager {
275
287
  this.loopCallback = () => {
276
288
  scrollTo(1, false);
277
289
  this.updateCurrentIndex(0);
278
- this.loopCallback = null;
290
+ this.cancelPendingLoopTransition();
279
291
  };
280
292
 
293
+ this.resetTransformCallback?.();
294
+ this.programmaticScrollVersion += 1;
281
295
  scrollTo(this.dataLength + 1, true);
282
296
  return;
283
297
  }
284
298
 
299
+ this.resetTransformCallback?.();
300
+ this.programmaticScrollVersion += 1;
285
301
  scrollTo(index + 1, true);
286
302
  this.updateCurrentIndex(index);
287
303
 
@@ -292,6 +308,8 @@ class GestureViewerManager {
292
308
  return;
293
309
  }
294
310
 
311
+ this.resetTransformCallback?.();
312
+ this.programmaticScrollVersion += 1;
295
313
  scrollTo(index, true);
296
314
  this.updateCurrentIndex(index);
297
315
  };
@@ -306,14 +324,18 @@ class GestureViewerManager {
306
324
  return true;
307
325
  }
308
326
 
309
- this.loopCallback = null;
327
+ this.cancelPendingLoopTransition();
310
328
  return true;
311
329
  };
312
330
 
313
- handleScrollBeginDrag = () => {
331
+ cancelPendingLoopTransition = () => {
314
332
  this.loopCallback = null;
315
333
  };
316
334
 
335
+ handleScrollBeginDrag = () => {
336
+ this.cancelPendingLoopTransition();
337
+ };
338
+
317
339
  goToPrevious = () => {
318
340
  this.goToIndex(this.currentIndex - 1);
319
341
  };
@@ -323,7 +345,7 @@ class GestureViewerManager {
323
345
  };
324
346
 
325
347
  cleanUp() {
326
- this.loopCallback = null;
348
+ this.cancelPendingLoopTransition();
327
349
  this.listeners.clear();
328
350
  this.listRef = null;
329
351
  this.enableHorizontalSwipe = true;
@@ -334,6 +356,7 @@ class GestureViewerManager {
334
356
  this.translateX = null;
335
357
  this.translateY = null;
336
358
  this.rotation = null;
359
+ this.resetTransformCallback = null;
337
360
  this.eventListeners.clear();
338
361
  }
339
362
 
@@ -5,7 +5,9 @@ import GestureViewerManager from './GestureViewerManager';
5
5
  class GestureViewerRegistry {
6
6
  private managers = new Map<string, GestureViewerManager>();
7
7
  private subscribers = new Map<string, Set<(manager: GestureViewerManager | null) => void>>();
8
- private triggers = new Map<string, View | null>();
8
+ private activeTriggerSubscribers = new Map<string, Set<(node: View | null) => void>>();
9
+ private activeTriggers = new Map<string, View | null>();
10
+ private indexedTriggers = new Map<string, Map<number, View | null>>();
9
11
 
10
12
  subscribeToManager(id: string, callback: (manager: GestureViewerManager | null) => void) {
11
13
  if (!this.subscribers.has(id)) {
@@ -29,6 +31,26 @@ class GestureViewerRegistry {
29
31
  };
30
32
  }
31
33
 
34
+ subscribeToActiveTrigger(id: string, callback: (node: View | null) => void) {
35
+ if (!this.activeTriggerSubscribers.has(id)) {
36
+ this.activeTriggerSubscribers.set(id, new Set());
37
+ }
38
+
39
+ this.activeTriggerSubscribers.get(id)?.add(callback);
40
+
41
+ callback(this.getActiveTriggerNode(id));
42
+
43
+ return () => {
44
+ const subscribers = this.activeTriggerSubscribers.get(id);
45
+
46
+ subscribers?.delete(callback);
47
+
48
+ if (subscribers && subscribers.size === 0) {
49
+ this.activeTriggerSubscribers.delete(id);
50
+ }
51
+ };
52
+ }
53
+
32
54
  createManager(id: string): GestureViewerManager | null {
33
55
  if (this.managers.has(id)) {
34
56
  return this.managers.get(id) || null;
@@ -55,7 +77,8 @@ class GestureViewerRegistry {
55
77
 
56
78
  this.notifySubscribers(id, null);
57
79
 
58
- this.triggers.delete(id);
80
+ this.activeTriggers.delete(id);
81
+ this.notifyActiveTriggerSubscribers(id, null);
59
82
  }
60
83
  }
61
84
 
@@ -67,16 +90,52 @@ class GestureViewerRegistry {
67
90
  }
68
91
  }
69
92
 
70
- setTriggerNode(id: string, node: View | null) {
71
- this.triggers.set(id, node);
93
+ notifyActiveTriggerSubscribers(id: string, node: View | null) {
94
+ const listeners = this.activeTriggerSubscribers.get(id);
95
+
96
+ if (listeners) {
97
+ [...listeners].forEach((callback) => callback(node));
98
+ }
72
99
  }
73
100
 
74
- getTriggerNode(id: string): View | null {
75
- return this.triggers.get(id) ?? null;
101
+ setActiveTriggerNode(id: string, node: View | null) {
102
+ this.activeTriggers.set(id, node);
103
+ this.notifyActiveTriggerSubscribers(id, node);
76
104
  }
77
105
 
78
- clearTriggerNode(id: string) {
79
- this.triggers.delete(id);
106
+ getActiveTriggerNode(id: string): View | null {
107
+ return this.activeTriggers.get(id) ?? null;
108
+ }
109
+
110
+ clearActiveTriggerNode(id: string) {
111
+ this.activeTriggers.delete(id);
112
+ this.notifyActiveTriggerSubscribers(id, null);
113
+ }
114
+
115
+ setIndexedTriggerNode(id: string, index: number, node: View | null) {
116
+ if (!this.indexedTriggers.has(id)) {
117
+ this.indexedTriggers.set(id, new Map());
118
+ }
119
+
120
+ this.indexedTriggers.get(id)?.set(index, node);
121
+ }
122
+
123
+ getIndexedTriggerNode(id: string, index: number): View | null {
124
+ return this.indexedTriggers.get(id)?.get(index) ?? null;
125
+ }
126
+
127
+ clearIndexedTriggerNode(id: string, index: number) {
128
+ const triggersByIndex = this.indexedTriggers.get(id);
129
+
130
+ if (!triggersByIndex) {
131
+ return;
132
+ }
133
+
134
+ triggersByIndex.delete(index);
135
+
136
+ if (triggersByIndex.size === 0) {
137
+ this.indexedTriggers.delete(id);
138
+ }
80
139
  }
81
140
  }
82
141