react-native-gesture-image-viewer 2.4.0 → 2.5.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 (50) hide show
  1. package/README.md +19 -5
  2. package/lib/module/GestureViewer.js +31 -13
  3. package/lib/module/GestureViewer.js.map +1 -1
  4. package/lib/module/GestureViewerManager.js +43 -14
  5. package/lib/module/GestureViewerManager.js.map +1 -1
  6. package/lib/module/index.js.map +1 -1
  7. package/lib/module/itemDimensions.js +86 -0
  8. package/lib/module/itemDimensions.js.map +1 -0
  9. package/lib/module/useGestureViewer.js +270 -40
  10. package/lib/module/useGestureViewer.js.map +1 -1
  11. package/lib/module/useGestureViewerPaging.js +1 -3
  12. package/lib/module/useGestureViewerPaging.js.map +1 -1
  13. package/lib/module/useGestureViewerPaging.web.js +26 -18
  14. package/lib/module/useGestureViewerPaging.web.js.map +1 -1
  15. package/lib/module/utils/index.js +43 -10
  16. package/lib/module/utils/index.js.map +1 -1
  17. package/lib/module/utils/tapZoom.js +51 -13
  18. package/lib/module/utils/tapZoom.js.map +1 -1
  19. package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
  20. package/lib/typescript/src/GestureViewerManager.d.ts +16 -3
  21. package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
  22. package/lib/typescript/src/index.d.ts +1 -1
  23. package/lib/typescript/src/index.d.ts.map +1 -1
  24. package/lib/typescript/src/itemDimensions.d.ts +26 -0
  25. package/lib/typescript/src/itemDimensions.d.ts.map +1 -0
  26. package/lib/typescript/src/types.d.ts +29 -0
  27. package/lib/typescript/src/types.d.ts.map +1 -1
  28. package/lib/typescript/src/useGestureViewer.d.ts +3 -2
  29. package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
  30. package/lib/typescript/src/useGestureViewerPaging.d.ts.map +1 -1
  31. package/lib/typescript/src/useGestureViewerPaging.types.d.ts +2 -0
  32. package/lib/typescript/src/useGestureViewerPaging.types.d.ts.map +1 -1
  33. package/lib/typescript/src/useGestureViewerPaging.web.d.ts +1 -1
  34. package/lib/typescript/src/useGestureViewerPaging.web.d.ts.map +1 -1
  35. package/lib/typescript/src/utils/index.d.ts +7 -3
  36. package/lib/typescript/src/utils/index.d.ts.map +1 -1
  37. package/lib/typescript/src/utils/tapZoom.d.ts +17 -1
  38. package/lib/typescript/src/utils/tapZoom.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/GestureViewer.tsx +47 -16
  41. package/src/GestureViewerManager.ts +58 -19
  42. package/src/index.tsx +4 -0
  43. package/src/itemDimensions.ts +162 -0
  44. package/src/types.ts +39 -0
  45. package/src/useGestureViewer.ts +376 -47
  46. package/src/useGestureViewerPaging.ts +1 -3
  47. package/src/useGestureViewerPaging.types.ts +2 -0
  48. package/src/useGestureViewerPaging.web.ts +41 -21
  49. package/src/utils/index.ts +86 -27
  50. package/src/utils/tapZoom.ts +66 -13
@@ -30,6 +30,8 @@ export function useGestureViewerPaging({
30
30
  adjustedInitialIndex,
31
31
  autoPlay,
32
32
  autoPlayInterval,
33
+ contentHeight,
34
+ contentWidth,
33
35
  currentIndex,
34
36
  dataLength,
35
37
  enableDoubleTapZoom,
@@ -51,20 +53,25 @@ export function useGestureViewerPaging({
51
53
  width,
52
54
  }: UseGestureViewerPagingArgs): UseGestureViewerPagingResult {
53
55
  const [activeListIndex, setActiveListIndex] = useState(adjustedInitialIndex);
54
- const activeResetItemSpacing = adjustedInitialIndex > 0 ? itemSpacing : 0;
55
- const activeResetWidth = adjustedInitialIndex > 0 ? width : 0;
56
+ const pageStride = width + itemSpacing;
56
57
  const webSingleTapTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
57
58
 
58
59
  const webScrollRuntimeRef = useRef<WebScrollRuntime>({
59
60
  actor: 'idle',
60
61
  isAutoplayPausedByUser: false,
61
62
  lastSettledPhysicalIndex: adjustedInitialIndex,
62
- latestOffsetX: adjustedInitialIndex * (width + itemSpacing),
63
+ latestOffsetX: adjustedInitialIndex * pageStride,
63
64
  latestRawPhysicalIndex: adjustedInitialIndex,
64
65
  lastProgrammaticScrollVersion: 0,
65
66
  settleTimer: null,
66
67
  resumeAutoplayTimer: null,
67
68
  });
69
+ const previousPagingLayoutRef = useRef<{
70
+ adjustedInitialIndex: number;
71
+ dataLength: number;
72
+ manager: typeof manager;
73
+ pageStride: number;
74
+ } | null>(null);
68
75
 
69
76
  const clearWebSingleTapTimer = useCallback(() => {
70
77
  if (webSingleTapTimerRef.current) {
@@ -227,38 +234,47 @@ export function useGestureViewerPaging({
227
234
 
228
235
  useEffect(() => {
229
236
  const runtime = webScrollRuntimeRef.current;
237
+ const previousLayout = previousPagingLayoutRef.current;
238
+ const shouldResetToInitialIndex =
239
+ previousLayout === null ||
240
+ previousLayout.adjustedInitialIndex !== adjustedInitialIndex ||
241
+ previousLayout.dataLength !== dataLength ||
242
+ previousLayout.manager !== manager;
243
+ const didPageStrideChange = previousLayout !== null && previousLayout.pageStride !== pageStride;
244
+
245
+ previousPagingLayoutRef.current = {
246
+ adjustedInitialIndex,
247
+ dataLength,
248
+ manager,
249
+ pageStride,
250
+ };
251
+
252
+ if (!shouldResetToInitialIndex && !didPageStrideChange) {
253
+ return;
254
+ }
230
255
 
231
256
  runtime.actor = 'idle';
232
257
  runtime.isAutoplayPausedByUser = false;
233
- runtime.lastSettledPhysicalIndex = adjustedInitialIndex;
234
- runtime.latestOffsetX = adjustedInitialIndex * (activeResetWidth + activeResetItemSpacing);
235
- runtime.latestRawPhysicalIndex = adjustedInitialIndex;
258
+
259
+ if (shouldResetToInitialIndex) {
260
+ runtime.lastSettledPhysicalIndex = adjustedInitialIndex;
261
+ setActiveListIndex(adjustedInitialIndex);
262
+ }
263
+
264
+ runtime.latestOffsetX = runtime.lastSettledPhysicalIndex * pageStride;
265
+ runtime.latestRawPhysicalIndex = runtime.lastSettledPhysicalIndex;
236
266
  runtime.lastProgrammaticScrollVersion = manager?.getProgrammaticScrollVersion() ?? 0;
237
- setActiveListIndex(adjustedInitialIndex);
238
267
  clearWebSettleTimer();
239
268
  clearWebAutoplayResumeTimer();
240
269
  }, [
241
- activeResetItemSpacing,
242
- activeResetWidth,
243
270
  adjustedInitialIndex,
244
271
  clearWebAutoplayResumeTimer,
245
272
  clearWebSettleTimer,
246
273
  dataLength,
247
274
  manager,
275
+ pageStride,
248
276
  ]);
249
277
 
250
- useEffect(() => {
251
- const runtime = webScrollRuntimeRef.current;
252
-
253
- runtime.actor = 'idle';
254
- runtime.isAutoplayPausedByUser = false;
255
- runtime.latestOffsetX = runtime.lastSettledPhysicalIndex * (width + itemSpacing);
256
- runtime.latestRawPhysicalIndex = runtime.lastSettledPhysicalIndex;
257
- runtime.lastProgrammaticScrollVersion = manager?.getProgrammaticScrollVersion() ?? 0;
258
- clearWebSettleTimer();
259
- clearWebAutoplayResumeTimer();
260
- }, [clearWebAutoplayResumeTimer, clearWebSettleTimer, itemSpacing, manager, width]);
261
-
262
278
  useEffect(() => {
263
279
  return () => {
264
280
  clearWebSingleTapTimer();
@@ -405,6 +421,8 @@ export function useGestureViewerPaging({
405
421
  scheduleWebAutoplayResume();
406
422
 
407
423
  applyTapZoomAtPoint({
424
+ contentHeight: contentHeight.get(),
425
+ contentWidth: contentWidth.get(),
408
426
  x: resolvedX,
409
427
  y: resolvedY,
410
428
  width,
@@ -430,6 +448,8 @@ export function useGestureViewerPaging({
430
448
  },
431
449
  [
432
450
  clearWebSingleTapTimer,
451
+ contentHeight,
452
+ contentWidth,
433
453
  enableDoubleTapZoom,
434
454
  height,
435
455
  maxZoomScale,
@@ -47,39 +47,92 @@ export const shouldUseNativeScrollGesture = (
47
47
  return platformOS === 'ios' && component !== GestureScrollView && component !== GestureFlatList;
48
48
  };
49
49
 
50
- export const createBoundsConstraint =
51
- ({ width, height }: { width: number; height: number }) =>
52
- ({
53
- scale,
54
- translateX,
55
- translateY,
56
- }: {
57
- translateX: number;
58
- translateY: number;
59
- scale: number;
60
- }) => {
61
- 'worklet';
62
-
63
- if (scale <= 1) {
64
- return {
65
- translateX,
66
- translateY,
67
- };
68
- }
50
+ export const clampIndex = (index: number | undefined, dataLength: number): number => {
51
+ if (dataLength <= 0) {
52
+ return 0;
53
+ }
54
+
55
+ const candidateIndex = index ?? 0;
56
+
57
+ if (!Number.isFinite(candidateIndex)) {
58
+ return 0;
59
+ }
60
+
61
+ return Math.min(Math.max(Math.trunc(candidateIndex), 0), dataLength - 1);
62
+ };
63
+
64
+ export const resolveGeometrySyncTranslationMode = (
65
+ previousIndex: number | null,
66
+ nextIndex: number,
67
+ scale: number,
68
+ ): 'constrain' | 'none' | 'reset' => {
69
+ if (previousIndex !== null && previousIndex !== nextIndex) {
70
+ return 'reset';
71
+ }
72
+
73
+ return scale > 1 ? 'constrain' : 'none';
74
+ };
75
+
76
+ const isValidDimension = (value: number): boolean => {
77
+ 'worklet';
78
+
79
+ return Number.isFinite(value) && value > 0;
80
+ };
69
81
 
70
- const maxTranslateX = (width * scale - width) / 2;
71
- const maxTranslateY = (height * scale - height) / 2;
82
+ const clampTranslation = (value: number, max: number): number => {
83
+ 'worklet';
72
84
 
85
+ if (max <= 0) {
86
+ return 0;
87
+ }
88
+
89
+ const clamped = Math.max(-max, Math.min(max, value));
90
+
91
+ return clamped === 0 ? 0 : clamped;
92
+ };
93
+
94
+ export const clampTranslationToBounds = ({
95
+ width,
96
+ height,
97
+ contentWidth,
98
+ contentHeight,
99
+ scale,
100
+ translateX,
101
+ translateY,
102
+ }: {
103
+ width: number;
104
+ height: number;
105
+ contentWidth?: number;
106
+ contentHeight?: number;
107
+ translateX: number;
108
+ translateY: number;
109
+ scale: number;
110
+ }) => {
111
+ 'worklet';
112
+
113
+ if (scale <= 1) {
73
114
  return {
74
- translateX: Math.max(-maxTranslateX, Math.min(maxTranslateX, translateX)),
75
- translateY: Math.max(-maxTranslateY, Math.min(maxTranslateY, translateY)),
115
+ translateX,
116
+ translateY,
76
117
  };
77
- };
118
+ }
78
119
 
79
- export const createLoopData = <T>(dataRef: React.RefObject<T[]>, enableLoop: boolean): T[] => {
80
- const data = dataRef.current;
120
+ const baseContentWidth =
121
+ contentWidth !== undefined && isValidDimension(contentWidth) ? contentWidth : width;
122
+ const baseContentHeight =
123
+ contentHeight !== undefined && isValidDimension(contentHeight) ? contentHeight : height;
81
124
 
82
- if (!enableLoop || !data?.length || data.length <= 1) {
125
+ const maxTranslateX = Math.max(0, (baseContentWidth * scale - width) / 2);
126
+ const maxTranslateY = Math.max(0, (baseContentHeight * scale - height) / 2);
127
+
128
+ return {
129
+ translateX: clampTranslation(translateX, maxTranslateX),
130
+ translateY: clampTranslation(translateY, maxTranslateY),
131
+ };
132
+ };
133
+
134
+ export const createLoopData = <T>(data: T[], enableLoop: boolean): T[] => {
135
+ if (!enableLoop || data.length <= 1) {
83
136
  return data;
84
137
  }
85
138
 
@@ -93,6 +146,12 @@ export const createLoopData = <T>(dataRef: React.RefObject<T[]>, enableLoop: boo
93
146
  return [lastItem, ...data, firstItem];
94
147
  };
95
148
 
149
+ export const getLoopPhysicalIndex = (
150
+ logicalIndex: number,
151
+ dataLength: number,
152
+ enableLoop: boolean,
153
+ ): number => (enableLoop && dataLength > 1 ? logicalIndex + 1 : logicalIndex);
154
+
96
155
  export const getLoopAdjustedIndex = (
97
156
  scrollIndex: number,
98
157
  originalDataLength: number,
@@ -1,11 +1,62 @@
1
1
  import type { SharedValue } from 'react-native-reanimated';
2
2
  import { Easing, withTiming } from 'react-native-reanimated';
3
3
 
4
+ import { clampTranslationToBounds } from '.';
5
+
6
+ export const getTapZoomTarget = ({
7
+ x,
8
+ y,
9
+ width,
10
+ height,
11
+ contentWidth,
12
+ contentHeight,
13
+ maxZoomScale,
14
+ scale,
15
+ }: {
16
+ x: number;
17
+ y: number;
18
+ width: number;
19
+ height: number;
20
+ contentWidth?: number;
21
+ contentHeight?: number;
22
+ maxZoomScale: number;
23
+ scale: number;
24
+ }) => {
25
+ 'worklet';
26
+
27
+ const nextScale = scale > 1 ? 1 : maxZoomScale;
28
+
29
+ if (nextScale <= 1) {
30
+ return {
31
+ scale: nextScale,
32
+ translateX: 0,
33
+ translateY: 0,
34
+ };
35
+ }
36
+
37
+ const centerX = x - width / 2;
38
+ const centerY = y - height / 2;
39
+ return {
40
+ scale: nextScale,
41
+ ...clampTranslationToBounds({
42
+ contentHeight,
43
+ contentWidth,
44
+ height,
45
+ scale: nextScale,
46
+ translateX: -centerX * (nextScale - 1),
47
+ translateY: -centerY * (nextScale - 1),
48
+ width,
49
+ }),
50
+ };
51
+ };
52
+
4
53
  export const applyTapZoomAtPoint = ({
5
54
  x,
6
55
  y,
7
56
  width,
8
57
  height,
58
+ contentWidth,
59
+ contentHeight,
9
60
  maxZoomScale,
10
61
  scale,
11
62
  translateX,
@@ -15,6 +66,8 @@ export const applyTapZoomAtPoint = ({
15
66
  y: number;
16
67
  width: number;
17
68
  height: number;
69
+ contentWidth?: number;
70
+ contentHeight?: number;
18
71
  maxZoomScale: number;
19
72
  scale: SharedValue<number>;
20
73
  translateX: SharedValue<number>;
@@ -22,23 +75,23 @@ export const applyTapZoomAtPoint = ({
22
75
  }) => {
23
76
  'worklet';
24
77
 
25
- const nextScale = scale.get() > 1 ? 1 : maxZoomScale;
78
+ const target = getTapZoomTarget({
79
+ contentHeight,
80
+ contentWidth,
81
+ height,
82
+ maxZoomScale,
83
+ scale: scale.get(),
84
+ width,
85
+ x,
86
+ y,
87
+ });
26
88
  const timingConfig = {
27
89
  duration: 300,
28
90
  easing: Easing.bezier(0.25, 0.1, 0.25, 1),
29
91
  };
30
92
 
31
- if (nextScale > 1) {
32
- const centerX = x - width / 2;
33
- const centerY = y - height / 2;
34
-
35
- // NOTE 확대로 밀려난 거리만큼 반대로 이동해서 탭 지점을 제자리에 유지
36
- translateX.set(withTiming(-centerX * (nextScale - 1), timingConfig));
37
- translateY.set(withTiming(-centerY * (nextScale - 1), timingConfig));
38
- } else {
39
- translateX.set(withTiming(0, timingConfig));
40
- translateY.set(withTiming(0, timingConfig));
41
- }
93
+ translateX.set(withTiming(target.translateX, timingConfig));
94
+ translateY.set(withTiming(target.translateY, timingConfig));
42
95
 
43
- scale.set(withTiming(nextScale, timingConfig));
96
+ scale.set(withTiming(target.scale, timingConfig));
44
97
  };