react-native-gesture-image-viewer 2.3.3 → 2.5.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 +19 -5
- package/lib/module/GestureViewer.js +39 -15
- package/lib/module/GestureViewer.js.map +1 -1
- package/lib/module/GestureViewerManager.js +43 -14
- 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 +246 -28
- package/lib/module/useGestureViewer.js.map +1 -1
- package/lib/module/useGestureViewerPaging.js +17 -5
- package/lib/module/useGestureViewerPaging.js.map +1 -1
- package/lib/module/useGestureViewerPaging.web.js +31 -7
- package/lib/module/useGestureViewerPaging.web.js.map +1 -1
- package/lib/module/utils/index.js +43 -10
- package/lib/module/utils/index.js.map +1 -1
- package/lib/module/utils/tapZoom.js +51 -13
- 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 +16 -3
- 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 +40 -1
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useGestureViewer.d.ts +4 -2
- package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
- package/lib/typescript/src/useGestureViewerPaging.d.ts +1 -1
- package/lib/typescript/src/useGestureViewerPaging.d.ts.map +1 -1
- package/lib/typescript/src/useGestureViewerPaging.types.d.ts +3 -0
- 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/index.d.ts +7 -3
- 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 +64 -18
- package/src/GestureViewerManager.ts +58 -19
- package/src/index.tsx +5 -0
- package/src/itemDimensions.ts +162 -0
- package/src/types.ts +51 -1
- package/src/useGestureViewer.ts +381 -52
- package/src/useGestureViewerPaging.ts +21 -7
- package/src/useGestureViewerPaging.types.ts +3 -0
- package/src/useGestureViewerPaging.web.ts +46 -7
- package/src/utils/index.ts +86 -27
- package/src/utils/tapZoom.ts +66 -13
package/src/types.ts
CHANGED
|
@@ -82,6 +82,42 @@ export type GestureViewerSingleTapEvent<ItemT> = {
|
|
|
82
82
|
item: ItemT;
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
export type GestureViewerItemDimensions = Readonly<{
|
|
86
|
+
/**
|
|
87
|
+
* Natural/source content width. Must be finite and greater than zero.
|
|
88
|
+
*/
|
|
89
|
+
width: number;
|
|
90
|
+
/**
|
|
91
|
+
* Natural/source content height. Must be finite and greater than zero.
|
|
92
|
+
*/
|
|
93
|
+
height: number;
|
|
94
|
+
}>;
|
|
95
|
+
|
|
96
|
+
export type GestureViewerItemDimensionsResolver<ItemT> = (
|
|
97
|
+
item: ItemT,
|
|
98
|
+
index: number,
|
|
99
|
+
) => GestureViewerItemDimensions | undefined;
|
|
100
|
+
|
|
101
|
+
export type GestureViewerItemKey = string | number;
|
|
102
|
+
|
|
103
|
+
export type GestureViewerItemKeyResolver<ItemT> = (
|
|
104
|
+
item: ItemT,
|
|
105
|
+
index: number,
|
|
106
|
+
) => GestureViewerItemKey;
|
|
107
|
+
|
|
108
|
+
export type GestureViewerRenderItemInfo = {
|
|
109
|
+
/**
|
|
110
|
+
* Whether the rendered item is currently active.
|
|
111
|
+
* @remarks The current item remains active during a page transition. When the transition finishes on another item, that item becomes active.
|
|
112
|
+
*/
|
|
113
|
+
readonly isActive: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Registers natural/source dimensions for the rendered item after they become available.
|
|
116
|
+
* @remarks Call this from an image load/event callback or a passive effect after commit. Do not call it directly while rendering or from descendant layout effects.
|
|
117
|
+
*/
|
|
118
|
+
readonly setItemDimensions: (dimensions: GestureViewerItemDimensions) => void;
|
|
119
|
+
};
|
|
120
|
+
|
|
85
121
|
export type GestureViewerDismissDirection = 'down' | 'up' | 'both';
|
|
86
122
|
|
|
87
123
|
export interface TriggerAnimationConfig extends WithTimingConfig {
|
|
@@ -114,6 +150,7 @@ export interface GestureViewerProps<ItemT, LC> {
|
|
|
114
150
|
data: ItemT[];
|
|
115
151
|
/**
|
|
116
152
|
* The index of the item to display in the `GestureViewer` when the component is mounted.
|
|
153
|
+
* @remarks The value is normalized to the current data: non-finite or negative values use `0`, values above the available range use the last index, and empty data uses `0`. Updating this prop repositions a mounted viewer.
|
|
117
154
|
* @defaultValue 0
|
|
118
155
|
*/
|
|
119
156
|
initialIndex?: number;
|
|
@@ -128,8 +165,11 @@ export interface GestureViewerProps<ItemT, LC> {
|
|
|
128
165
|
onDismissStart?: () => void;
|
|
129
166
|
/**
|
|
130
167
|
* A callback function that is called to render the item.
|
|
168
|
+
* @param item - The item to render.
|
|
169
|
+
* @param index - The list index of the rendered item.
|
|
170
|
+
* @param info - Render state for this list cell.
|
|
131
171
|
*/
|
|
132
|
-
renderItem: (item: ItemT, index: number) => React.ReactElement;
|
|
172
|
+
renderItem: (item: ItemT, index: number, info: GestureViewerRenderItemInfo) => React.ReactElement;
|
|
133
173
|
/**
|
|
134
174
|
* A callback function that is called when a single tap is confirmed on the viewer content.
|
|
135
175
|
* @remarks
|
|
@@ -138,6 +178,16 @@ export interface GestureViewerProps<ItemT, LC> {
|
|
|
138
178
|
* - Prefer this callback over overlaying a pressable in `renderContainer` for fullscreen tap handling.
|
|
139
179
|
*/
|
|
140
180
|
onSingleTap?: (event: GestureViewerSingleTapEvent<ItemT>) => void;
|
|
181
|
+
/**
|
|
182
|
+
* Returns natural/source dimensions for an item when they are already known.
|
|
183
|
+
* @remarks Return `undefined` while dimensions are unavailable. Invalid dimensions fall back to the viewer cell size.
|
|
184
|
+
*/
|
|
185
|
+
getItemDimensions?: GestureViewerItemDimensionsResolver<ItemT>;
|
|
186
|
+
/**
|
|
187
|
+
* Returns a stable key used to retain loaded dimensions when equivalent item objects are recreated at the same index.
|
|
188
|
+
* @remarks Keys must be unique within `data` and change when the rendered content's natural dimensions can change. Do not use the index by itself. This is only needed when object items are recreated and dimensions are reported through `setItemDimensions`.
|
|
189
|
+
*/
|
|
190
|
+
getItemKey?: GestureViewerItemKeyResolver<ItemT>;
|
|
141
191
|
/**
|
|
142
192
|
* A callback function that is called to render the container.
|
|
143
193
|
* @remarks Useful for composing additional UI (e.g., close button, toolbars) around the viewer.
|
package/src/useGestureViewer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { Platform, type View, useWindowDimensions } from 'react-native';
|
|
3
3
|
import { Gesture, type GestureType } from 'react-native-gesture-handler';
|
|
4
4
|
import {
|
|
@@ -14,10 +14,23 @@ import { scheduleOnRN } from 'react-native-worklets';
|
|
|
14
14
|
|
|
15
15
|
import type GestureViewerManager from './GestureViewerManager';
|
|
16
16
|
import { registry } from './GestureViewerRegistry';
|
|
17
|
+
import {
|
|
18
|
+
type ItemDimensionsRegistry,
|
|
19
|
+
pruneItemDimensionsRegistry,
|
|
20
|
+
registerItemDimensions,
|
|
21
|
+
resolveItemDimensions,
|
|
22
|
+
} from './itemDimensions';
|
|
17
23
|
import { scheduleInitialScroll } from './scheduleInitialScroll';
|
|
18
|
-
import type { GestureViewerProps, TriggerRect } from './types';
|
|
24
|
+
import type { GestureViewerItemDimensions, GestureViewerProps, TriggerRect } from './types';
|
|
19
25
|
import { useGestureViewerPaging } from './useGestureViewerPaging';
|
|
20
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
clampIndex,
|
|
28
|
+
clampTranslationToBounds,
|
|
29
|
+
createScrollAction,
|
|
30
|
+
getLoopAdjustedIndex,
|
|
31
|
+
getLoopPhysicalIndex,
|
|
32
|
+
resolveGeometrySyncTranslationMode,
|
|
33
|
+
} from './utils';
|
|
21
34
|
import { getDismissDistance, shouldDismissByDirection } from './utils/dismiss';
|
|
22
35
|
import { applyTapZoomAtPoint } from './utils/tapZoom';
|
|
23
36
|
import { calculateFocalPointTranslation, shouldAcceptFocalPoint } from './utils/zoom';
|
|
@@ -33,6 +46,43 @@ type UseGestureViewerProps<ItemT, LC> = Omit<
|
|
|
33
46
|
| 'enableSnapMode'
|
|
34
47
|
>;
|
|
35
48
|
|
|
49
|
+
type ViewerPositionSnapshot = Readonly<{
|
|
50
|
+
dataLength: number;
|
|
51
|
+
initialIndex: number;
|
|
52
|
+
pageStride: number;
|
|
53
|
+
usesLoopSentinels: boolean;
|
|
54
|
+
}>;
|
|
55
|
+
|
|
56
|
+
function getViewerPositionChanges(
|
|
57
|
+
previous: ViewerPositionSnapshot | null,
|
|
58
|
+
current: ViewerPositionSnapshot,
|
|
59
|
+
) {
|
|
60
|
+
return {
|
|
61
|
+
isInitial: previous === null,
|
|
62
|
+
didDataLengthChange: previous !== null && previous.dataLength !== current.dataLength,
|
|
63
|
+
didInitialIndexChange: previous !== null && previous.initialIndex !== current.initialIndex,
|
|
64
|
+
didLoopLayoutChange:
|
|
65
|
+
previous !== null && previous.usesLoopSentinels !== current.usesLoopSentinels,
|
|
66
|
+
didPageStrideChange: previous !== null && previous.pageStride !== current.pageStride,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fitItemDimensions(
|
|
71
|
+
dimensions: GestureViewerItemDimensions | undefined,
|
|
72
|
+
viewport: GestureViewerItemDimensions,
|
|
73
|
+
): GestureViewerItemDimensions {
|
|
74
|
+
if (!dimensions) {
|
|
75
|
+
return viewport;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fitScale = Math.min(viewport.width / dimensions.width, viewport.height / dimensions.height);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
width: dimensions.width * fitScale,
|
|
82
|
+
height: dimensions.height * fitScale,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
36
86
|
export const useGestureViewer = <ItemT, LC>({
|
|
37
87
|
data,
|
|
38
88
|
initialIndex = 0,
|
|
@@ -53,6 +103,8 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
53
103
|
triggerAnimation,
|
|
54
104
|
autoPlay = false,
|
|
55
105
|
autoPlayInterval = 3000,
|
|
106
|
+
getItemDimensions,
|
|
107
|
+
getItemKey,
|
|
56
108
|
}: UseGestureViewerProps<ItemT, LC>) => {
|
|
57
109
|
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
58
110
|
const width = customWidth || screenWidth;
|
|
@@ -74,7 +126,11 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
74
126
|
const onAnimationCompleteRef = useRef(triggerAnimation?.onAnimationComplete);
|
|
75
127
|
const onSingleTapRef = useRef(onSingleTap);
|
|
76
128
|
const dataRef = useRef(data);
|
|
129
|
+
const getItemDimensionsRef = useRef(getItemDimensions);
|
|
130
|
+
const getItemKeyRef = useRef(getItemKey);
|
|
77
131
|
const managerRef = useRef(manager);
|
|
132
|
+
const configuredManagerRef = useRef<GestureViewerManager | null>(null);
|
|
133
|
+
const itemDimensionsRef = useRef<ItemDimensionsRegistry<ItemT>>(new Map());
|
|
78
134
|
|
|
79
135
|
const isValidTriggerRect = useCallback((rect: TriggerRect | null): rect is TriggerRect => {
|
|
80
136
|
return !!rect && rect.width > 0 && rect.height > 0;
|
|
@@ -89,6 +145,8 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
89
145
|
const scale = useSharedValue(1);
|
|
90
146
|
const backdropOpacity = useSharedValue(1);
|
|
91
147
|
const rotation = useSharedValue(0);
|
|
148
|
+
const contentWidth = useSharedValue(width);
|
|
149
|
+
const contentHeight = useSharedValue(height);
|
|
92
150
|
|
|
93
151
|
const triggerScale = useSharedValue(1);
|
|
94
152
|
const triggerTranslateX = useSharedValue(0);
|
|
@@ -102,6 +160,122 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
102
160
|
const hasActiveFocal = useSharedValue(false);
|
|
103
161
|
|
|
104
162
|
const dataLength = data?.length || 0;
|
|
163
|
+
const usesLoopSentinels = enableLoop && dataLength > 1;
|
|
164
|
+
const pageStride = width + itemSpacing;
|
|
165
|
+
const adjustedInitialIndex = getLoopPhysicalIndex(initialIndex, dataLength, enableLoop);
|
|
166
|
+
// Manager setup can rerender before a deferred list scroll runs, so each consumer tracks
|
|
167
|
+
// the last committed position inputs independently.
|
|
168
|
+
const managerPositionSnapshotRef = useRef<ViewerPositionSnapshot | null>(null);
|
|
169
|
+
const listPositionSnapshotRef = useRef<ViewerPositionSnapshot | null>(null);
|
|
170
|
+
const viewportRef = useRef({ height, width });
|
|
171
|
+
const loopConfigRef = useRef({ dataLength, enableLoop });
|
|
172
|
+
const activeGeometryRef = useRef<{
|
|
173
|
+
contentHeight: number;
|
|
174
|
+
contentWidth: number;
|
|
175
|
+
height: number;
|
|
176
|
+
width: number;
|
|
177
|
+
} | null>(null);
|
|
178
|
+
const activeGeometryIndexRef = useRef<number | null>(null);
|
|
179
|
+
|
|
180
|
+
const syncActiveContentDimensions = useCallback(
|
|
181
|
+
(logicalIndex = pendingIndexRef.current) => {
|
|
182
|
+
const viewport = viewportRef.current;
|
|
183
|
+
const fitted = fitItemDimensions(
|
|
184
|
+
resolveItemDimensions({
|
|
185
|
+
data: dataRef.current,
|
|
186
|
+
getItemDimensions: getItemDimensionsRef.current,
|
|
187
|
+
getItemKey: getItemKeyRef.current,
|
|
188
|
+
index: logicalIndex,
|
|
189
|
+
registry: itemDimensionsRef.current,
|
|
190
|
+
}),
|
|
191
|
+
viewport,
|
|
192
|
+
);
|
|
193
|
+
const nextGeometry = {
|
|
194
|
+
contentHeight: fitted.height,
|
|
195
|
+
contentWidth: fitted.width,
|
|
196
|
+
height: viewport.height,
|
|
197
|
+
width: viewport.width,
|
|
198
|
+
};
|
|
199
|
+
const previousGeometry = activeGeometryRef.current;
|
|
200
|
+
const previousGeometryIndex = activeGeometryIndexRef.current;
|
|
201
|
+
|
|
202
|
+
activeGeometryIndexRef.current = logicalIndex;
|
|
203
|
+
|
|
204
|
+
const currentScale = scale.get();
|
|
205
|
+
const translationMode = resolveGeometrySyncTranslationMode(
|
|
206
|
+
previousGeometryIndex,
|
|
207
|
+
logicalIndex,
|
|
208
|
+
currentScale,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (translationMode === 'reset') {
|
|
212
|
+
// Complete the page-owned reset before the next item's geometry can reuse the old offset.
|
|
213
|
+
translateX.set(0);
|
|
214
|
+
translateY.set(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
previousGeometry?.contentHeight === nextGeometry.contentHeight &&
|
|
219
|
+
previousGeometry.contentWidth === nextGeometry.contentWidth &&
|
|
220
|
+
previousGeometry.height === nextGeometry.height &&
|
|
221
|
+
previousGeometry.width === nextGeometry.width
|
|
222
|
+
) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
activeGeometryRef.current = nextGeometry;
|
|
227
|
+
|
|
228
|
+
if (contentWidth.get() !== fitted.width) {
|
|
229
|
+
contentWidth.set(fitted.width);
|
|
230
|
+
}
|
|
231
|
+
if (contentHeight.get() !== fitted.height) {
|
|
232
|
+
contentHeight.set(fitted.height);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (translationMode !== 'constrain') {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const { translateX: constrainedTranslateX, translateY: constrainedTranslateY } =
|
|
240
|
+
clampTranslationToBounds({
|
|
241
|
+
contentHeight: fitted.height,
|
|
242
|
+
contentWidth: fitted.width,
|
|
243
|
+
height: viewport.height,
|
|
244
|
+
scale: currentScale,
|
|
245
|
+
translateX: translateX.get(),
|
|
246
|
+
translateY: translateY.get(),
|
|
247
|
+
width: viewport.width,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
translateX.set(withTiming(constrainedTranslateX));
|
|
251
|
+
translateY.set(withTiming(constrainedTranslateY));
|
|
252
|
+
},
|
|
253
|
+
[contentHeight, contentWidth, scale, translateX, translateY],
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const setItemDimensions = useCallback(
|
|
257
|
+
(listIndex: number, item: ItemT, dimensions: GestureViewerItemDimensions) => {
|
|
258
|
+
const { dataLength: currentDataLength, enableLoop: currentEnableLoop } =
|
|
259
|
+
loopConfigRef.current;
|
|
260
|
+
const logicalIndex =
|
|
261
|
+
currentDataLength <= 0
|
|
262
|
+
? listIndex
|
|
263
|
+
: getLoopAdjustedIndex(listIndex, currentDataLength, currentEnableLoop).realIndex;
|
|
264
|
+
const didUpdateDimensions = registerItemDimensions({
|
|
265
|
+
data: dataRef.current,
|
|
266
|
+
dimensions,
|
|
267
|
+
getItemKey: getItemKeyRef.current,
|
|
268
|
+
index: logicalIndex,
|
|
269
|
+
item,
|
|
270
|
+
registry: itemDimensionsRef.current,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (didUpdateDimensions && logicalIndex === pendingIndexRef.current) {
|
|
274
|
+
syncActiveContentDimensions(logicalIndex);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
[syncActiveContentDimensions],
|
|
278
|
+
);
|
|
105
279
|
|
|
106
280
|
const animationConfig = useMemo(
|
|
107
281
|
() => ({
|
|
@@ -129,26 +303,38 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
129
303
|
],
|
|
130
304
|
);
|
|
131
305
|
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
306
|
+
const constrainTranslation = useCallback(
|
|
307
|
+
({
|
|
308
|
+
scale: targetScale,
|
|
309
|
+
translateX: targetTranslateX,
|
|
310
|
+
translateY: targetTranslateY,
|
|
311
|
+
}: {
|
|
312
|
+
translateX: number;
|
|
313
|
+
translateY: number;
|
|
314
|
+
scale: number;
|
|
315
|
+
}) => {
|
|
316
|
+
'worklet';
|
|
317
|
+
|
|
318
|
+
return clampTranslationToBounds({
|
|
319
|
+
contentHeight: contentHeight.get(),
|
|
320
|
+
contentWidth: contentWidth.get(),
|
|
321
|
+
height,
|
|
322
|
+
scale: targetScale,
|
|
323
|
+
translateX: targetTranslateX,
|
|
324
|
+
translateY: targetTranslateY,
|
|
325
|
+
width,
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
[contentHeight, contentWidth, height, width],
|
|
143
329
|
);
|
|
144
330
|
|
|
145
331
|
const scrollTo = useCallback(
|
|
146
332
|
(index: number, animated: boolean) => {
|
|
147
|
-
const scrollAction = createScrollAction(listRef.current,
|
|
333
|
+
const scrollAction = createScrollAction(listRef.current, pageStride);
|
|
148
334
|
|
|
149
335
|
return scrollAction.scrollTo(index, animated);
|
|
150
336
|
},
|
|
151
|
-
[
|
|
337
|
+
[pageStride],
|
|
152
338
|
);
|
|
153
339
|
|
|
154
340
|
const resetTransformState = useCallback(() => {
|
|
@@ -167,9 +353,14 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
167
353
|
return;
|
|
168
354
|
}
|
|
169
355
|
|
|
356
|
+
if (nextIndex === pendingIndexRef.current) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
170
360
|
pendingIndexRef.current = nextIndex;
|
|
361
|
+
syncActiveContentDimensions(nextIndex);
|
|
171
362
|
},
|
|
172
|
-
[dataLength],
|
|
363
|
+
[dataLength, syncActiveContentDimensions],
|
|
173
364
|
);
|
|
174
365
|
|
|
175
366
|
const syncCurrentIndex = useCallback(
|
|
@@ -179,6 +370,7 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
179
370
|
}
|
|
180
371
|
|
|
181
372
|
pendingIndexRef.current = nextIndex;
|
|
373
|
+
syncActiveContentDimensions(nextIndex);
|
|
182
374
|
|
|
183
375
|
const managerCurrentIndex = manager.getState().currentIndex;
|
|
184
376
|
|
|
@@ -190,7 +382,7 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
190
382
|
manager.notifyStateChange();
|
|
191
383
|
resetTransformState();
|
|
192
384
|
},
|
|
193
|
-
[dataLength, manager, resetTransformState],
|
|
385
|
+
[dataLength, manager, resetTransformState, syncActiveContentDimensions],
|
|
194
386
|
);
|
|
195
387
|
|
|
196
388
|
const emitZoomChange = useCallback((currentScale: number, prevScale: number | null) => {
|
|
@@ -249,37 +441,78 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
249
441
|
}, [id]);
|
|
250
442
|
|
|
251
443
|
useEffect(() => {
|
|
252
|
-
|
|
444
|
+
const currentPositionSnapshot = {
|
|
445
|
+
dataLength,
|
|
446
|
+
initialIndex,
|
|
447
|
+
pageStride,
|
|
448
|
+
usesLoopSentinels,
|
|
449
|
+
};
|
|
450
|
+
const { didDataLengthChange, didInitialIndexChange, didLoopLayoutChange } =
|
|
451
|
+
getViewerPositionChanges(managerPositionSnapshotRef.current, currentPositionSnapshot);
|
|
452
|
+
|
|
453
|
+
managerPositionSnapshotRef.current = currentPositionSnapshot;
|
|
253
454
|
|
|
254
455
|
if (!manager) {
|
|
456
|
+
configuredManagerRef.current = null;
|
|
255
457
|
return;
|
|
256
458
|
}
|
|
257
459
|
|
|
460
|
+
const isNewManager = configuredManagerRef.current !== manager;
|
|
461
|
+
const shouldApplyInitialIndex =
|
|
462
|
+
isNewManager || didInitialIndexChange || didDataLengthChange || didLoopLayoutChange;
|
|
463
|
+
const shouldSyncInitialIndex =
|
|
464
|
+
didInitialIndexChange ||
|
|
465
|
+
didDataLengthChange ||
|
|
466
|
+
didLoopLayoutChange ||
|
|
467
|
+
activeGeometryIndexRef.current !== initialIndex;
|
|
468
|
+
|
|
258
469
|
manager.setDataLength(dataLength);
|
|
259
470
|
manager.setEnableHorizontalSwipe(enableHorizontalSwipe);
|
|
260
|
-
manager.
|
|
261
|
-
manager.
|
|
471
|
+
manager.setPagingStride(pageStride);
|
|
472
|
+
manager.setViewportWidth(width);
|
|
262
473
|
manager.setHeight(height);
|
|
263
|
-
manager.setZoomSharedValues(
|
|
474
|
+
manager.setZoomSharedValues({
|
|
475
|
+
contentHeight,
|
|
476
|
+
contentWidth,
|
|
477
|
+
maxZoomScale,
|
|
478
|
+
scale,
|
|
479
|
+
translateX,
|
|
480
|
+
translateY,
|
|
481
|
+
});
|
|
264
482
|
manager.setResetTransformCallback(resetTransformState);
|
|
265
483
|
manager.setRotation(rotation);
|
|
266
484
|
manager.setEnableLoop(enableLoop);
|
|
485
|
+
|
|
486
|
+
if (shouldApplyInitialIndex) {
|
|
487
|
+
pendingIndexRef.current = initialIndex;
|
|
488
|
+
manager.setCurrentIndex(initialIndex);
|
|
489
|
+
|
|
490
|
+
if (shouldSyncInitialIndex) {
|
|
491
|
+
syncActiveContentDimensions(initialIndex);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
configuredManagerRef.current = manager;
|
|
267
496
|
manager.notifyStateChange();
|
|
268
497
|
}, [
|
|
269
498
|
dataLength,
|
|
270
499
|
enableHorizontalSwipe,
|
|
271
500
|
initialIndex,
|
|
272
501
|
manager,
|
|
502
|
+
pageStride,
|
|
273
503
|
width,
|
|
274
|
-
itemSpacing,
|
|
275
504
|
maxZoomScale,
|
|
276
505
|
enableLoop,
|
|
277
506
|
scale,
|
|
278
507
|
height,
|
|
508
|
+
contentWidth,
|
|
509
|
+
contentHeight,
|
|
279
510
|
resetTransformState,
|
|
511
|
+
syncActiveContentDimensions,
|
|
280
512
|
translateX,
|
|
281
513
|
translateY,
|
|
282
514
|
rotation,
|
|
515
|
+
usesLoopSentinels,
|
|
283
516
|
]);
|
|
284
517
|
|
|
285
518
|
useEffect(() => {
|
|
@@ -291,6 +524,35 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
291
524
|
}, [manager]);
|
|
292
525
|
|
|
293
526
|
useEffect(() => {
|
|
527
|
+
const currentPositionSnapshot = {
|
|
528
|
+
dataLength,
|
|
529
|
+
initialIndex,
|
|
530
|
+
pageStride,
|
|
531
|
+
usesLoopSentinels,
|
|
532
|
+
};
|
|
533
|
+
const {
|
|
534
|
+
didDataLengthChange,
|
|
535
|
+
didInitialIndexChange,
|
|
536
|
+
didLoopLayoutChange,
|
|
537
|
+
didPageStrideChange,
|
|
538
|
+
isInitial: isInitialPositionRender,
|
|
539
|
+
} = getViewerPositionChanges(listPositionSnapshotRef.current, currentPositionSnapshot);
|
|
540
|
+
const commitPositionSnapshot = () => {
|
|
541
|
+
listPositionSnapshotRef.current = currentPositionSnapshot;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const shouldResetToInitialIndex =
|
|
545
|
+
isInitialPositionRender ||
|
|
546
|
+
didInitialIndexChange ||
|
|
547
|
+
didDataLengthChange ||
|
|
548
|
+
didLoopLayoutChange;
|
|
549
|
+
const shouldRealignCurrentIndex = didPageStrideChange && !shouldResetToInitialIndex;
|
|
550
|
+
|
|
551
|
+
if (!shouldResetToInitialIndex && !shouldRealignCurrentIndex) {
|
|
552
|
+
commitPositionSnapshot();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
294
556
|
translateY.set(0);
|
|
295
557
|
translateX.set(0);
|
|
296
558
|
scale.set(1);
|
|
@@ -298,15 +560,44 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
298
560
|
startScale.set(1);
|
|
299
561
|
rotation.set(0);
|
|
300
562
|
|
|
301
|
-
if (
|
|
563
|
+
if (dataLength === 0 || !listRef.current) {
|
|
564
|
+
commitPositionSnapshot();
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (isInitialPositionRender && adjustedInitialIndex === 0) {
|
|
569
|
+
commitPositionSnapshot();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const logicalIndex = shouldResetToInitialIndex
|
|
574
|
+
? initialIndex
|
|
575
|
+
: clampIndex(
|
|
576
|
+
managerRef.current?.getState().currentIndex ?? pendingIndexRef.current,
|
|
577
|
+
dataLength,
|
|
578
|
+
);
|
|
579
|
+
const physicalIndex = getLoopPhysicalIndex(logicalIndex, dataLength, enableLoop);
|
|
580
|
+
|
|
581
|
+
if (shouldRealignCurrentIndex) {
|
|
582
|
+
pendingIndexRef.current = logicalIndex;
|
|
583
|
+
syncActiveContentDimensions(logicalIndex);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (shouldRealignCurrentIndex && physicalIndex === 0) {
|
|
587
|
+
commitPositionSnapshot();
|
|
302
588
|
return;
|
|
303
589
|
}
|
|
304
590
|
|
|
305
591
|
return scheduleInitialScroll(() => {
|
|
306
|
-
scrollTo(
|
|
592
|
+
scrollTo(physicalIndex, false);
|
|
593
|
+
commitPositionSnapshot();
|
|
307
594
|
});
|
|
308
595
|
}, [
|
|
309
596
|
adjustedInitialIndex,
|
|
597
|
+
dataLength,
|
|
598
|
+
enableLoop,
|
|
599
|
+
initialIndex,
|
|
600
|
+
pageStride,
|
|
310
601
|
translateY,
|
|
311
602
|
backdropOpacity,
|
|
312
603
|
translateX,
|
|
@@ -314,15 +605,36 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
314
605
|
startScale,
|
|
315
606
|
rotation,
|
|
316
607
|
scrollTo,
|
|
608
|
+
syncActiveContentDimensions,
|
|
609
|
+
usesLoopSentinels,
|
|
317
610
|
]);
|
|
318
611
|
|
|
319
612
|
useEffect(() => {
|
|
320
613
|
onAnimationCompleteRef.current = triggerAnimation?.onAnimationComplete;
|
|
321
614
|
}, [triggerAnimation?.onAnimationComplete]);
|
|
322
615
|
|
|
323
|
-
|
|
616
|
+
useLayoutEffect(() => {
|
|
617
|
+
// Retained virtualized-cell callbacks read only the most recently committed props.
|
|
324
618
|
dataRef.current = data;
|
|
325
|
-
|
|
619
|
+
getItemDimensionsRef.current = getItemDimensions;
|
|
620
|
+
getItemKeyRef.current = getItemKey;
|
|
621
|
+
viewportRef.current = { height, width };
|
|
622
|
+
loopConfigRef.current = { dataLength, enableLoop };
|
|
623
|
+
syncActiveContentDimensions();
|
|
624
|
+
}, [
|
|
625
|
+
data,
|
|
626
|
+
dataLength,
|
|
627
|
+
enableLoop,
|
|
628
|
+
getItemDimensions,
|
|
629
|
+
getItemKey,
|
|
630
|
+
height,
|
|
631
|
+
syncActiveContentDimensions,
|
|
632
|
+
width,
|
|
633
|
+
]);
|
|
634
|
+
|
|
635
|
+
useEffect(() => {
|
|
636
|
+
pruneItemDimensionsRegistry(itemDimensionsRef.current, dataLength);
|
|
637
|
+
}, [dataLength]);
|
|
326
638
|
|
|
327
639
|
useEffect(() => {
|
|
328
640
|
managerRef.current = manager;
|
|
@@ -757,6 +1069,8 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
757
1069
|
.numberOfTaps(2)
|
|
758
1070
|
.onEnd((event) => {
|
|
759
1071
|
applyTapZoomAtPoint({
|
|
1072
|
+
contentHeight: contentHeight.get(),
|
|
1073
|
+
contentWidth: contentWidth.get(),
|
|
760
1074
|
x: event.x,
|
|
761
1075
|
y: event.y,
|
|
762
1076
|
width,
|
|
@@ -767,7 +1081,17 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
767
1081
|
translateY,
|
|
768
1082
|
});
|
|
769
1083
|
}),
|
|
770
|
-
[
|
|
1084
|
+
[
|
|
1085
|
+
contentHeight,
|
|
1086
|
+
contentWidth,
|
|
1087
|
+
enableDoubleTapZoom,
|
|
1088
|
+
height,
|
|
1089
|
+
maxZoomScale,
|
|
1090
|
+
scale,
|
|
1091
|
+
translateX,
|
|
1092
|
+
translateY,
|
|
1093
|
+
width,
|
|
1094
|
+
],
|
|
771
1095
|
);
|
|
772
1096
|
|
|
773
1097
|
const tapGesture = useMemo(
|
|
@@ -811,32 +1135,36 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
811
1135
|
return Gesture.Native().requireExternalGestureToFail(dismissGestureRef);
|
|
812
1136
|
}, []);
|
|
813
1137
|
|
|
814
|
-
const { onMomentumScrollEnd, onScroll, onScrollBeginDrag, onWebClick } =
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1138
|
+
const { activeListIndex, onMomentumScrollEnd, onScroll, onScrollBeginDrag, onWebClick } =
|
|
1139
|
+
useGestureViewerPaging({
|
|
1140
|
+
adjustedInitialIndex,
|
|
1141
|
+
autoPlay,
|
|
1142
|
+
autoPlayInterval,
|
|
1143
|
+
contentHeight,
|
|
1144
|
+
contentWidth,
|
|
1145
|
+
currentIndex,
|
|
1146
|
+
dataLength,
|
|
1147
|
+
enableDoubleTapZoom,
|
|
1148
|
+
enableHorizontalSwipe,
|
|
1149
|
+
enableLoop,
|
|
1150
|
+
height,
|
|
1151
|
+
isRotated,
|
|
1152
|
+
isZoomed,
|
|
1153
|
+
itemSpacing,
|
|
1154
|
+
manager,
|
|
1155
|
+
maxZoomScale,
|
|
1156
|
+
onSingleTap: emitSingleTap,
|
|
1157
|
+
scale,
|
|
1158
|
+
scrollTo,
|
|
1159
|
+
syncCurrentIndex,
|
|
1160
|
+
syncPendingIndex,
|
|
1161
|
+
translateX,
|
|
1162
|
+
translateY,
|
|
1163
|
+
width,
|
|
1164
|
+
});
|
|
838
1165
|
|
|
839
1166
|
return {
|
|
1167
|
+
activeListIndex,
|
|
840
1168
|
animatedStyle,
|
|
841
1169
|
backdropStyle,
|
|
842
1170
|
dataLength,
|
|
@@ -853,6 +1181,7 @@ export const useGestureViewer = <ItemT, LC>({
|
|
|
853
1181
|
onScroll,
|
|
854
1182
|
|
|
855
1183
|
onScrollBeginDrag,
|
|
1184
|
+
setItemDimensions,
|
|
856
1185
|
zoomGesture,
|
|
857
1186
|
};
|
|
858
1187
|
};
|