react-native-gesture-image-viewer 3.0.0-beta.1 → 3.0.0-beta.3
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/lib/module/GestureViewer.js +9 -1
- package/lib/module/GestureViewer.js.map +1 -1
- package/lib/module/GestureViewerManager.js +32 -10
- package/lib/module/GestureViewerManager.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/itemDimensions.js +86 -0
- package/lib/module/itemDimensions.js.map +1 -0
- package/lib/module/useGestureViewer.js +127 -10
- package/lib/module/useGestureViewer.js.map +1 -1
- package/lib/module/useGestureViewerManagerBridge.js +11 -2
- package/lib/module/useGestureViewerManagerBridge.js.map +1 -1
- package/lib/module/useGestureViewerPaging.js +1 -1
- package/lib/module/useGestureViewerPaging.js.map +1 -1
- package/lib/module/useWebClickHandler.js.map +1 -1
- package/lib/module/useWebClickHandler.web.js +9 -1
- package/lib/module/useWebClickHandler.web.js.map +1 -1
- package/lib/module/utils/index.js +27 -7
- package/lib/module/utils/index.js.map +1 -1
- package/lib/module/utils/tapZoom.js +47 -14
- package/lib/module/utils/tapZoom.js.map +1 -1
- package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
- package/lib/typescript/src/GestureViewerManager.d.ts +12 -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/itemDimensions.d.ts +26 -0
- package/lib/typescript/src/itemDimensions.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +28 -0
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useGestureViewer.d.ts +3 -2
- package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
- package/lib/typescript/src/useGestureViewerManagerBridge.d.ts +3 -1
- package/lib/typescript/src/useGestureViewerManagerBridge.d.ts.map +1 -1
- package/lib/typescript/src/useWebClickHandler.d.ts +2 -0
- package/lib/typescript/src/useWebClickHandler.d.ts.map +1 -1
- package/lib/typescript/src/useWebClickHandler.web.d.ts +4 -2
- package/lib/typescript/src/useWebClickHandler.web.d.ts.map +1 -1
- package/lib/typescript/src/utils/index.d.ts +4 -2
- package/lib/typescript/src/utils/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/tapZoom.d.ts +17 -1
- package/lib/typescript/src/utils/tapZoom.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/GestureViewer.tsx +17 -2
- package/src/GestureViewerManager.ts +47 -15
- package/src/index.tsx +4 -0
- package/src/itemDimensions.ts +162 -0
- package/src/types.ts +38 -0
- package/src/useGestureViewer.ts +184 -13
- package/src/useGestureViewerManagerBridge.ts +24 -2
- package/src/useGestureViewerPaging.ts +1 -1
- package/src/useWebClickHandler.ts +2 -0
- package/src/useWebClickHandler.web.ts +12 -0
- package/src/utils/index.ts +55 -22
- package/src/utils/tapZoom.ts +55 -15
package/src/types.ts
CHANGED
|
@@ -16,12 +16,40 @@ export type GestureViewerSingleTapEvent<ItemT> = {
|
|
|
16
16
|
item: ItemT;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
export type GestureViewerItemDimensions = Readonly<{
|
|
20
|
+
/**
|
|
21
|
+
* Natural/source content width. Must be finite and greater than zero.
|
|
22
|
+
*/
|
|
23
|
+
width: number;
|
|
24
|
+
/**
|
|
25
|
+
* Natural/source content height. Must be finite and greater than zero.
|
|
26
|
+
*/
|
|
27
|
+
height: number;
|
|
28
|
+
}>;
|
|
29
|
+
|
|
30
|
+
export type GestureViewerItemDimensionsResolver<ItemT> = (
|
|
31
|
+
item: ItemT,
|
|
32
|
+
index: number,
|
|
33
|
+
) => GestureViewerItemDimensions | undefined;
|
|
34
|
+
|
|
35
|
+
export type GestureViewerItemKey = string | number;
|
|
36
|
+
|
|
37
|
+
export type GestureViewerItemKeyResolver<ItemT> = (
|
|
38
|
+
item: ItemT,
|
|
39
|
+
index: number,
|
|
40
|
+
) => GestureViewerItemKey;
|
|
41
|
+
|
|
19
42
|
export type GestureViewerRenderItemInfo = {
|
|
20
43
|
/**
|
|
21
44
|
* Whether the rendered item is currently active.
|
|
22
45
|
* @remarks The current item remains active during a page transition. When the transition finishes on another item, that item becomes active.
|
|
23
46
|
*/
|
|
24
47
|
readonly isActive: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Registers natural/source dimensions for the rendered item after they become available.
|
|
50
|
+
* @remarks Call this from an image load or event callback, or from a passive `useEffect` after commit. Do not call it during render or from a descendant layout effect.
|
|
51
|
+
*/
|
|
52
|
+
readonly setItemDimensions: (dimensions: GestureViewerItemDimensions) => void;
|
|
25
53
|
};
|
|
26
54
|
|
|
27
55
|
export type GestureViewerDismissDirection = 'down' | 'up' | 'both';
|
|
@@ -83,6 +111,16 @@ export interface GestureViewerProps<ItemT> {
|
|
|
83
111
|
* - Prefer this callback over overlaying a pressable in `renderContainer` for fullscreen tap handling.
|
|
84
112
|
*/
|
|
85
113
|
onSingleTap?: (event: GestureViewerSingleTapEvent<ItemT>) => void;
|
|
114
|
+
/**
|
|
115
|
+
* Returns natural/source dimensions for an item when they are already known.
|
|
116
|
+
* @remarks Return `undefined` while dimensions are unavailable. Invalid dimensions fall back to the viewer cell size.
|
|
117
|
+
*/
|
|
118
|
+
getItemDimensions?: GestureViewerItemDimensionsResolver<ItemT>;
|
|
119
|
+
/**
|
|
120
|
+
* Returns a stable logical key for an item when the same logical item may be recreated as a new object.
|
|
121
|
+
* @remarks Use this when item identity is not object-stable across rerenders. The key must identify the same rendered content and change when the rendered source or natural dimensions change. Do not return the array index alone. When omitted, runtime dimensions are only reused for the exact same item object.
|
|
122
|
+
*/
|
|
123
|
+
getItemKey?: GestureViewerItemKeyResolver<ItemT>;
|
|
86
124
|
/**
|
|
87
125
|
* A callback function that is called to render the container.
|
|
88
126
|
* @remarks Useful for composing additional UI (e.g., close button, toolbars) around the viewer.
|
package/src/useGestureViewer.ts
CHANGED
|
@@ -22,6 +22,12 @@ import type GestureViewerManager from './GestureViewerManager';
|
|
|
22
22
|
import { normalizeHorizontalSwipeThreshold } from './gestureViewerPaging';
|
|
23
23
|
import { registry } from './GestureViewerRegistry';
|
|
24
24
|
import { resolveGestureViewerRenderWindow } from './gestureViewerRenderWindow';
|
|
25
|
+
import {
|
|
26
|
+
type ItemDimensionsRegistry,
|
|
27
|
+
pruneItemDimensionsRegistry,
|
|
28
|
+
registerItemDimensions,
|
|
29
|
+
resolveItemDimensions,
|
|
30
|
+
} from './itemDimensions';
|
|
25
31
|
import { enableNativeTapGestures } from './platformTapGestures';
|
|
26
32
|
import {
|
|
27
33
|
type NavigationOptions,
|
|
@@ -33,12 +39,12 @@ import {
|
|
|
33
39
|
resolveNavigation,
|
|
34
40
|
shouldRunNavigationDuringTransition,
|
|
35
41
|
} from './renderWindow';
|
|
36
|
-
import type { GestureViewerProps, TriggerRect } from './types';
|
|
42
|
+
import type { GestureViewerItemDimensions, GestureViewerProps, TriggerRect } from './types';
|
|
37
43
|
import { useGestureViewerManagerBridge } from './useGestureViewerManagerBridge';
|
|
38
44
|
import { useGestureViewerPaging } from './useGestureViewerPaging';
|
|
39
45
|
import { type EmitSingleTap, useWebClickHandler } from './useWebClickHandler';
|
|
40
46
|
import { useWebSingleTapTimer } from './useWebSingleTapTimer';
|
|
41
|
-
import {
|
|
47
|
+
import { clampTranslationToBounds, resolveGeometrySyncTranslationMode } from './utils';
|
|
42
48
|
import { getDismissDistance, shouldDismissByDirection } from './utils/dismiss';
|
|
43
49
|
import { applyTapZoomAtPoint } from './utils/tapZoom';
|
|
44
50
|
import { calculateFocalPointTranslation, shouldAcceptFocalPoint } from './utils/zoom';
|
|
@@ -54,6 +60,23 @@ type UseGestureViewerProps<ItemT> = Omit<
|
|
|
54
60
|
'renderItem' | 'renderContainer' | 'containerStyle' | 'backdropStyle'
|
|
55
61
|
>;
|
|
56
62
|
|
|
63
|
+
type ContentViewport = Readonly<{
|
|
64
|
+
height: number;
|
|
65
|
+
width: number;
|
|
66
|
+
}>;
|
|
67
|
+
|
|
68
|
+
function fitItemDimensions(
|
|
69
|
+
dimensions: GestureViewerItemDimensions | undefined,
|
|
70
|
+
viewport: ContentViewport,
|
|
71
|
+
): GestureViewerItemDimensions {
|
|
72
|
+
if (!dimensions) {
|
|
73
|
+
return viewport;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fit = Math.min(viewport.width / dimensions.width, viewport.height / dimensions.height);
|
|
77
|
+
return { height: dimensions.height * fit, width: dimensions.width * fit };
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
export const useGestureViewer = <ItemT>({
|
|
58
81
|
data,
|
|
59
82
|
initialIndex = 0,
|
|
@@ -75,6 +98,8 @@ export const useGestureViewer = <ItemT>({
|
|
|
75
98
|
triggerAnimation,
|
|
76
99
|
autoPlay = false,
|
|
77
100
|
autoPlayInterval = 3000,
|
|
101
|
+
getItemDimensions,
|
|
102
|
+
getItemKey,
|
|
78
103
|
}: UseGestureViewerProps<ItemT>) => {
|
|
79
104
|
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
80
105
|
const width = customWidth || screenWidth;
|
|
@@ -117,12 +142,23 @@ export const useGestureViewer = <ItemT>({
|
|
|
117
142
|
const onAnimationCompleteRef = useRef(triggerAnimation?.onAnimationComplete);
|
|
118
143
|
const onSingleTapRef = useRef(onSingleTap);
|
|
119
144
|
const dataRef = useRef(data);
|
|
145
|
+
const getItemDimensionsRef = useRef(getItemDimensions);
|
|
146
|
+
const getItemKeyRef = useRef(getItemKey);
|
|
120
147
|
const dataLengthRef = useRef(dataLength);
|
|
121
148
|
const enableLoopRef = useRef(enableLoop);
|
|
122
149
|
const managerRef = useRef<GestureViewerManager | null>(null);
|
|
123
150
|
const isZoomedRef = useRef(isZoomed);
|
|
124
151
|
const isRotatedRef = useRef(isRotated);
|
|
125
152
|
const isPinchingRef = useRef(isPinching);
|
|
153
|
+
const itemDimensionsRef = useRef<ItemDimensionsRegistry<ItemT>>(new Map());
|
|
154
|
+
const viewportRef = useRef({ height, width });
|
|
155
|
+
const activeGeometryRef = useRef<{
|
|
156
|
+
contentHeight: number;
|
|
157
|
+
contentWidth: number;
|
|
158
|
+
height: number;
|
|
159
|
+
width: number;
|
|
160
|
+
} | null>(null);
|
|
161
|
+
const activeGeometryIndexRef = useRef<number | null>(null);
|
|
126
162
|
|
|
127
163
|
const initialTranslateY = useSharedValue(0);
|
|
128
164
|
const initialTranslateX = useSharedValue(0);
|
|
@@ -132,6 +168,8 @@ export const useGestureViewer = <ItemT>({
|
|
|
132
168
|
const translateX = useSharedValue(0);
|
|
133
169
|
const scale = useSharedValue(1);
|
|
134
170
|
const rotation = useSharedValue(0);
|
|
171
|
+
const contentWidth = useSharedValue(width);
|
|
172
|
+
const contentHeight = useSharedValue(height);
|
|
135
173
|
|
|
136
174
|
const triggerScale = useSharedValue(1);
|
|
137
175
|
const triggerTranslateX = useSharedValue(0);
|
|
@@ -189,9 +227,121 @@ export const useGestureViewer = <ItemT>({
|
|
|
189
227
|
windowSize,
|
|
190
228
|
});
|
|
191
229
|
|
|
192
|
-
const
|
|
193
|
-
() =>
|
|
194
|
-
|
|
230
|
+
const syncActiveContentDimensions = useCallback(
|
|
231
|
+
(index = pendingIndexRef.current) => {
|
|
232
|
+
const viewport = viewportRef.current;
|
|
233
|
+
const fitted = fitItemDimensions(
|
|
234
|
+
resolveItemDimensions({
|
|
235
|
+
data: dataRef.current,
|
|
236
|
+
getItemKey: getItemKeyRef.current,
|
|
237
|
+
getItemDimensions: getItemDimensionsRef.current,
|
|
238
|
+
index,
|
|
239
|
+
registry: itemDimensionsRef.current,
|
|
240
|
+
}),
|
|
241
|
+
viewport,
|
|
242
|
+
);
|
|
243
|
+
const nextGeometry = {
|
|
244
|
+
contentHeight: fitted.height,
|
|
245
|
+
contentWidth: fitted.width,
|
|
246
|
+
height: viewport.height,
|
|
247
|
+
width: viewport.width,
|
|
248
|
+
};
|
|
249
|
+
const previousGeometry = activeGeometryRef.current;
|
|
250
|
+
const previousGeometryIndex = activeGeometryIndexRef.current;
|
|
251
|
+
|
|
252
|
+
activeGeometryIndexRef.current = index;
|
|
253
|
+
|
|
254
|
+
const currentScale = scale.get();
|
|
255
|
+
const translationMode = resolveGeometrySyncTranslationMode(
|
|
256
|
+
previousGeometryIndex,
|
|
257
|
+
index,
|
|
258
|
+
currentScale,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (translationMode === 'reset') {
|
|
262
|
+
// Complete the page-owned reset before the next item's geometry can reuse the old offset.
|
|
263
|
+
translateX.set(0);
|
|
264
|
+
translateY.set(0);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (
|
|
268
|
+
previousGeometry?.contentHeight === nextGeometry.contentHeight &&
|
|
269
|
+
previousGeometry.contentWidth === nextGeometry.contentWidth &&
|
|
270
|
+
previousGeometry.height === nextGeometry.height &&
|
|
271
|
+
previousGeometry.width === nextGeometry.width
|
|
272
|
+
) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
activeGeometryRef.current = nextGeometry;
|
|
277
|
+
|
|
278
|
+
if (contentWidth.get() !== fitted.width) {
|
|
279
|
+
contentWidth.set(fitted.width);
|
|
280
|
+
}
|
|
281
|
+
if (contentHeight.get() !== fitted.height) {
|
|
282
|
+
contentHeight.set(fitted.height);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (translationMode !== 'constrain') {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const constrained = clampTranslationToBounds({
|
|
290
|
+
contentHeight: fitted.height,
|
|
291
|
+
contentWidth: fitted.width,
|
|
292
|
+
height: viewport.height,
|
|
293
|
+
width: viewport.width,
|
|
294
|
+
scale: currentScale,
|
|
295
|
+
translateX: translateX.get(),
|
|
296
|
+
translateY: translateY.get(),
|
|
297
|
+
});
|
|
298
|
+
translateX.set(withTiming(constrained.translateX));
|
|
299
|
+
translateY.set(withTiming(constrained.translateY));
|
|
300
|
+
},
|
|
301
|
+
[contentHeight, contentWidth, scale, translateX, translateY],
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const setItemDimensions = useCallback(
|
|
305
|
+
(index: number, item: ItemT, dimensions: GestureViewerItemDimensions) => {
|
|
306
|
+
if (
|
|
307
|
+
registerItemDimensions({
|
|
308
|
+
data: dataRef.current,
|
|
309
|
+
dimensions,
|
|
310
|
+
getItemKey: getItemKeyRef.current,
|
|
311
|
+
index,
|
|
312
|
+
item,
|
|
313
|
+
registry: itemDimensionsRef.current,
|
|
314
|
+
}) === 'updated' &&
|
|
315
|
+
index === pendingIndexRef.current
|
|
316
|
+
) {
|
|
317
|
+
syncActiveContentDimensions(index);
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
[syncActiveContentDimensions],
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const constrainTranslation = useCallback(
|
|
324
|
+
({
|
|
325
|
+
scale: targetScale,
|
|
326
|
+
translateX: targetX,
|
|
327
|
+
translateY: targetY,
|
|
328
|
+
}: {
|
|
329
|
+
scale: number;
|
|
330
|
+
translateX: number;
|
|
331
|
+
translateY: number;
|
|
332
|
+
}) => {
|
|
333
|
+
'worklet';
|
|
334
|
+
return clampTranslationToBounds({
|
|
335
|
+
contentHeight: contentHeight.get(),
|
|
336
|
+
contentWidth: contentWidth.get(),
|
|
337
|
+
height,
|
|
338
|
+
scale: targetScale,
|
|
339
|
+
translateX: targetX,
|
|
340
|
+
translateY: targetY,
|
|
341
|
+
width,
|
|
342
|
+
});
|
|
343
|
+
},
|
|
344
|
+
[contentHeight, contentWidth, height, width],
|
|
195
345
|
);
|
|
196
346
|
|
|
197
347
|
const resetTransformState = useCallback(() => {
|
|
@@ -221,12 +371,16 @@ export const useGestureViewer = <ItemT>({
|
|
|
221
371
|
rotation.set(0);
|
|
222
372
|
}, [initialTranslateX, initialTranslateY, rotation, scale, startScale, translateX, translateY]);
|
|
223
373
|
|
|
224
|
-
const commitCurrentIndex = useCallback(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
374
|
+
const commitCurrentIndex = useCallback(
|
|
375
|
+
(nextIndex: number) => {
|
|
376
|
+
pendingIndexRef.current = nextIndex;
|
|
377
|
+
currentIndexRef.current = nextIndex;
|
|
378
|
+
syncActiveContentDimensions(nextIndex);
|
|
379
|
+
setCurrentIndex(nextIndex);
|
|
380
|
+
managerRef.current?.notifyStateChange();
|
|
381
|
+
},
|
|
382
|
+
[syncActiveContentDimensions],
|
|
383
|
+
);
|
|
230
384
|
|
|
231
385
|
const commitVirtualIndexOnly = useCallback(
|
|
232
386
|
(nextVirtualIndex: number) => {
|
|
@@ -360,6 +514,8 @@ export const useGestureViewer = <ItemT>({
|
|
|
360
514
|
}, [navigateByDirection]);
|
|
361
515
|
|
|
362
516
|
useGestureViewerManagerBridge({
|
|
517
|
+
contentHeight,
|
|
518
|
+
contentWidth,
|
|
363
519
|
currentIndexRef,
|
|
364
520
|
dataLengthRef,
|
|
365
521
|
goToIndex: navigateToIndex,
|
|
@@ -433,9 +589,17 @@ export const useGestureViewer = <ItemT>({
|
|
|
433
589
|
});
|
|
434
590
|
}, [id]);
|
|
435
591
|
|
|
436
|
-
|
|
592
|
+
useLayoutEffect(() => {
|
|
437
593
|
dataRef.current = data;
|
|
438
|
-
|
|
594
|
+
getItemDimensionsRef.current = getItemDimensions;
|
|
595
|
+
getItemKeyRef.current = getItemKey;
|
|
596
|
+
viewportRef.current = { height, width };
|
|
597
|
+
syncActiveContentDimensions();
|
|
598
|
+
}, [data, dataLength, getItemDimensions, getItemKey, height, syncActiveContentDimensions, width]);
|
|
599
|
+
|
|
600
|
+
useEffect(() => {
|
|
601
|
+
pruneItemDimensionsRegistry(itemDimensionsRef.current, dataLength);
|
|
602
|
+
}, [dataLength]);
|
|
439
603
|
|
|
440
604
|
useEffect(() => {
|
|
441
605
|
isZoomedRef.current = isZoomed;
|
|
@@ -910,6 +1074,8 @@ export const useGestureViewer = <ItemT>({
|
|
|
910
1074
|
}
|
|
911
1075
|
|
|
912
1076
|
applyTapZoomAtPoint({
|
|
1077
|
+
contentHeight: contentHeight.get(),
|
|
1078
|
+
contentWidth: contentWidth.get(),
|
|
913
1079
|
x: event.x,
|
|
914
1080
|
y: event.y,
|
|
915
1081
|
width,
|
|
@@ -921,6 +1087,8 @@ export const useGestureViewer = <ItemT>({
|
|
|
921
1087
|
});
|
|
922
1088
|
}),
|
|
923
1089
|
[
|
|
1090
|
+
contentHeight,
|
|
1091
|
+
contentWidth,
|
|
924
1092
|
enableDoubleTapZoom,
|
|
925
1093
|
height,
|
|
926
1094
|
maxZoomScale,
|
|
@@ -1199,6 +1367,8 @@ export const useGestureViewer = <ItemT>({
|
|
|
1199
1367
|
|
|
1200
1368
|
const onWebClick = useWebClickHandler({
|
|
1201
1369
|
clearPendingWebSingleTap,
|
|
1370
|
+
contentHeight,
|
|
1371
|
+
contentWidth,
|
|
1202
1372
|
emitSingleTap,
|
|
1203
1373
|
enableDoubleTapZoom,
|
|
1204
1374
|
getCurrentTapTarget,
|
|
@@ -1255,6 +1425,7 @@ export const useGestureViewer = <ItemT>({
|
|
|
1255
1425
|
pageStride,
|
|
1256
1426
|
renderWindowSlots,
|
|
1257
1427
|
visualPage,
|
|
1428
|
+
setItemDimensions,
|
|
1258
1429
|
zoomGesture,
|
|
1259
1430
|
zoomPinchGesture,
|
|
1260
1431
|
};
|
|
@@ -7,6 +7,8 @@ import { registry } from './GestureViewerRegistry';
|
|
|
7
7
|
|
|
8
8
|
type UseGestureViewerManagerBridgeOptions = {
|
|
9
9
|
currentIndexRef: RefObject<number>;
|
|
10
|
+
contentHeight: SharedValue<number>;
|
|
11
|
+
contentWidth: SharedValue<number>;
|
|
10
12
|
dataLengthRef: RefObject<number>;
|
|
11
13
|
goToIndex: (index: number, options?: GestureViewerNavigationOptions) => void;
|
|
12
14
|
goToNext: () => void;
|
|
@@ -29,6 +31,8 @@ type UseGestureViewerManagerBridgeOptions = {
|
|
|
29
31
|
*/
|
|
30
32
|
export function useGestureViewerManagerBridge({
|
|
31
33
|
currentIndexRef,
|
|
34
|
+
contentHeight,
|
|
35
|
+
contentWidth,
|
|
32
36
|
dataLengthRef,
|
|
33
37
|
goToIndex,
|
|
34
38
|
goToNext,
|
|
@@ -61,9 +65,27 @@ export function useGestureViewerManagerBridge({
|
|
|
61
65
|
|
|
62
66
|
manager.setWidth(width);
|
|
63
67
|
manager.setHeight(height);
|
|
64
|
-
manager.setZoomSharedValues(
|
|
68
|
+
manager.setZoomSharedValues({
|
|
69
|
+
scale,
|
|
70
|
+
translateX,
|
|
71
|
+
translateY,
|
|
72
|
+
maxZoomScale,
|
|
73
|
+
contentWidth,
|
|
74
|
+
contentHeight,
|
|
75
|
+
});
|
|
65
76
|
manager.setRotation(rotation);
|
|
66
|
-
}, [
|
|
77
|
+
}, [
|
|
78
|
+
contentHeight,
|
|
79
|
+
contentWidth,
|
|
80
|
+
height,
|
|
81
|
+
manager,
|
|
82
|
+
maxZoomScale,
|
|
83
|
+
rotation,
|
|
84
|
+
scale,
|
|
85
|
+
translateX,
|
|
86
|
+
translateY,
|
|
87
|
+
width,
|
|
88
|
+
]);
|
|
67
89
|
|
|
68
90
|
useEffect(() => {
|
|
69
91
|
if (!manager) {
|
|
@@ -184,7 +184,7 @@ export function useGestureViewerPaging({
|
|
|
184
184
|
pagingAnimationActive.set(false);
|
|
185
185
|
pagingGestureActive.set(false);
|
|
186
186
|
pageTransitionLocked.set(false);
|
|
187
|
-
visualPage.set(
|
|
187
|
+
visualPage.set(centerVirtualIndex);
|
|
188
188
|
scheduleOnRN(setPageTransitioning, false);
|
|
189
189
|
};
|
|
190
190
|
|
|
@@ -26,6 +26,8 @@ export type EmitSingleTap<ItemT> = (x: number, y: number, tapTarget?: WebTapTarg
|
|
|
26
26
|
|
|
27
27
|
export type WebClickHandlerConfig<ItemT> = {
|
|
28
28
|
clearPendingWebSingleTap: () => void;
|
|
29
|
+
contentHeight: SharedValue<number>;
|
|
30
|
+
contentWidth: SharedValue<number>;
|
|
29
31
|
emitSingleTap: EmitSingleTap<ItemT>;
|
|
30
32
|
enableDoubleTapZoom: boolean;
|
|
31
33
|
getCurrentTapTarget: () => WebTapTarget<ItemT> | null;
|
|
@@ -27,6 +27,8 @@ export type EmitSingleTap<ItemT> = (x: number, y: number, tapTarget?: WebTapTarg
|
|
|
27
27
|
|
|
28
28
|
export type WebClickHandlerConfig<ItemT> = {
|
|
29
29
|
clearPendingWebSingleTap: () => void;
|
|
30
|
+
contentHeight: SharedValue<number>;
|
|
31
|
+
contentWidth: SharedValue<number>;
|
|
30
32
|
emitSingleTap: EmitSingleTap<ItemT>;
|
|
31
33
|
enableDoubleTapZoom: boolean;
|
|
32
34
|
getCurrentTapTarget: () => WebTapTarget<ItemT> | null;
|
|
@@ -42,6 +44,8 @@ export type WebClickHandlerConfig<ItemT> = {
|
|
|
42
44
|
|
|
43
45
|
export function createWebClickHandler<ItemT>({
|
|
44
46
|
clearPendingWebSingleTap,
|
|
47
|
+
contentHeight,
|
|
48
|
+
contentWidth,
|
|
45
49
|
emitSingleTap,
|
|
46
50
|
enableDoubleTapZoom,
|
|
47
51
|
getCurrentTapTarget,
|
|
@@ -78,6 +82,8 @@ export function createWebClickHandler<ItemT>({
|
|
|
78
82
|
if (event.detail === 2) {
|
|
79
83
|
clearPendingWebSingleTap();
|
|
80
84
|
applyTapZoomAtPoint({
|
|
85
|
+
contentHeight: contentHeight.get(),
|
|
86
|
+
contentWidth: contentWidth.get(),
|
|
81
87
|
x,
|
|
82
88
|
y,
|
|
83
89
|
width,
|
|
@@ -100,6 +106,8 @@ export function createWebClickHandler<ItemT>({
|
|
|
100
106
|
|
|
101
107
|
export function useWebClickHandler<ItemT>({
|
|
102
108
|
clearPendingWebSingleTap,
|
|
109
|
+
contentHeight,
|
|
110
|
+
contentWidth,
|
|
103
111
|
emitSingleTap,
|
|
104
112
|
enableDoubleTapZoom,
|
|
105
113
|
getCurrentTapTarget,
|
|
@@ -116,6 +124,8 @@ export function useWebClickHandler<ItemT>({
|
|
|
116
124
|
() =>
|
|
117
125
|
createWebClickHandler({
|
|
118
126
|
clearPendingWebSingleTap,
|
|
127
|
+
contentHeight,
|
|
128
|
+
contentWidth,
|
|
119
129
|
emitSingleTap,
|
|
120
130
|
enableDoubleTapZoom,
|
|
121
131
|
getCurrentTapTarget,
|
|
@@ -130,6 +140,8 @@ export function useWebClickHandler<ItemT>({
|
|
|
130
140
|
}),
|
|
131
141
|
[
|
|
132
142
|
clearPendingWebSingleTap,
|
|
143
|
+
contentHeight,
|
|
144
|
+
contentWidth,
|
|
133
145
|
emitSingleTap,
|
|
134
146
|
enableDoubleTapZoom,
|
|
135
147
|
getCurrentTapTarget,
|
package/src/utils/index.ts
CHANGED
|
@@ -1,28 +1,61 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
(
|
|
4
|
-
|
|
5
|
-
translateX,
|
|
6
|
-
translateY,
|
|
7
|
-
}: {
|
|
8
|
-
translateX: number;
|
|
9
|
-
translateY: number;
|
|
10
|
-
scale: number;
|
|
11
|
-
}) => {
|
|
12
|
-
'worklet';
|
|
1
|
+
const isValidDimension = (value: number): boolean => {
|
|
2
|
+
'worklet';
|
|
3
|
+
return Number.isFinite(value) && value > 0;
|
|
4
|
+
};
|
|
13
5
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
export const resolveGeometrySyncTranslationMode = (
|
|
7
|
+
previousIndex: number | null,
|
|
8
|
+
nextIndex: number,
|
|
9
|
+
scale: number,
|
|
10
|
+
): 'constrain' | 'none' | 'reset' => {
|
|
11
|
+
if (previousIndex !== null && previousIndex !== nextIndex) {
|
|
12
|
+
return 'reset';
|
|
13
|
+
}
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
return scale > 1 ? 'constrain' : 'none';
|
|
16
|
+
};
|
|
23
17
|
|
|
18
|
+
const clampTranslation = (value: number, max: number): number => {
|
|
19
|
+
'worklet';
|
|
20
|
+
const result = Math.max(-max, Math.min(max, value));
|
|
21
|
+
return result === 0 ? 0 : result;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const clampTranslationToBounds = ({
|
|
25
|
+
width,
|
|
26
|
+
height,
|
|
27
|
+
contentWidth,
|
|
28
|
+
contentHeight,
|
|
29
|
+
scale,
|
|
30
|
+
translateX,
|
|
31
|
+
translateY,
|
|
32
|
+
}: {
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
contentWidth?: number;
|
|
36
|
+
contentHeight?: number;
|
|
37
|
+
translateX: number;
|
|
38
|
+
translateY: number;
|
|
39
|
+
scale: number;
|
|
40
|
+
}) => {
|
|
41
|
+
'worklet';
|
|
42
|
+
|
|
43
|
+
if (scale <= 1) {
|
|
24
44
|
return {
|
|
25
|
-
translateX
|
|
26
|
-
translateY
|
|
45
|
+
translateX,
|
|
46
|
+
translateY,
|
|
27
47
|
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const baseWidth =
|
|
51
|
+
contentWidth !== undefined && isValidDimension(contentWidth) ? contentWidth : width;
|
|
52
|
+
const baseHeight =
|
|
53
|
+
contentHeight !== undefined && isValidDimension(contentHeight) ? contentHeight : height;
|
|
54
|
+
const maxTranslateX = Math.max(0, (baseWidth * scale - width) / 2);
|
|
55
|
+
const maxTranslateY = Math.max(0, (baseHeight * scale - height) / 2);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
translateX: clampTranslation(translateX, maxTranslateX),
|
|
59
|
+
translateY: clampTranslation(translateY, maxTranslateY),
|
|
28
60
|
};
|
|
61
|
+
};
|
package/src/utils/tapZoom.ts
CHANGED
|
@@ -1,11 +1,51 @@
|
|
|
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
|
+
const nextScale = scale > 1 ? 1 : maxZoomScale;
|
|
27
|
+
if (nextScale <= 1) return { scale: nextScale, translateX: 0, translateY: 0 };
|
|
28
|
+
return {
|
|
29
|
+
scale: nextScale,
|
|
30
|
+
...clampTranslationToBounds({
|
|
31
|
+
contentHeight,
|
|
32
|
+
contentWidth,
|
|
33
|
+
height,
|
|
34
|
+
scale: nextScale,
|
|
35
|
+
translateX: -(x - width / 2) * (nextScale - 1),
|
|
36
|
+
translateY: -(y - height / 2) * (nextScale - 1),
|
|
37
|
+
width,
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
4
42
|
export const applyTapZoomAtPoint = ({
|
|
5
43
|
x,
|
|
6
44
|
y,
|
|
7
45
|
width,
|
|
8
46
|
height,
|
|
47
|
+
contentWidth,
|
|
48
|
+
contentHeight,
|
|
9
49
|
maxZoomScale,
|
|
10
50
|
scale,
|
|
11
51
|
translateX,
|
|
@@ -15,6 +55,8 @@ export const applyTapZoomAtPoint = ({
|
|
|
15
55
|
y: number;
|
|
16
56
|
width: number;
|
|
17
57
|
height: number;
|
|
58
|
+
contentWidth?: number;
|
|
59
|
+
contentHeight?: number;
|
|
18
60
|
maxZoomScale: number;
|
|
19
61
|
scale: SharedValue<number>;
|
|
20
62
|
translateX: SharedValue<number>;
|
|
@@ -22,24 +64,22 @@ export const applyTapZoomAtPoint = ({
|
|
|
22
64
|
}) => {
|
|
23
65
|
'worklet';
|
|
24
66
|
|
|
25
|
-
const
|
|
67
|
+
const target = getTapZoomTarget({
|
|
68
|
+
contentHeight,
|
|
69
|
+
contentWidth,
|
|
70
|
+
height,
|
|
71
|
+
maxZoomScale,
|
|
72
|
+
scale: scale.get(),
|
|
73
|
+
width,
|
|
74
|
+
x,
|
|
75
|
+
y,
|
|
76
|
+
});
|
|
26
77
|
const timingConfig = {
|
|
27
78
|
duration: 300,
|
|
28
79
|
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
|
|
29
80
|
};
|
|
30
81
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// NOTE 확대로 밀려난 거리만큼 반대로 이동해서 탭 지점을 제자리에 유지
|
|
36
|
-
translateX.set(withTiming(-centerX * (nextScale - 1), timingConfig));
|
|
37
|
-
translateY.set(withTiming(-centerY * (nextScale - 1), timingConfig));
|
|
38
|
-
scale.set(withTiming(nextScale, timingConfig));
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
translateX.set(withTiming(0, timingConfig));
|
|
43
|
-
translateY.set(withTiming(0, timingConfig));
|
|
44
|
-
scale.set(withTiming(nextScale, timingConfig));
|
|
82
|
+
translateX.set(withTiming(target.translateX, timingConfig));
|
|
83
|
+
translateY.set(withTiming(target.translateY, timingConfig));
|
|
84
|
+
scale.set(withTiming(target.scale, timingConfig));
|
|
45
85
|
};
|