Skip to content

Commit 9e5a6bd

Browse files
authored
refactor(loop): replace timeout with event-driven loop animation (#53)
- Remove hardcoded 300ms timeout dependency - Use onMomentumScrollEnd for accurate animation completion detection - Implement callback-based approach for better timing control - Handle user scroll interruption during loop transitions
1 parent 4be63a3 commit 9e5a6bd

File tree

3 files changed

+54
-26
lines changed

3 files changed

+54
-26
lines changed

.changeset/bitter-beers-arrive.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"react-native-gesture-image-viewer": patch
3+
---
4+
5+
refactor(loop): replace timeout with event-driven loop animation
6+
7+
- Remove hardcoded 300ms timeout dependency
8+
- Use onMomentumScrollEnd for accurate animation completion detection
9+
- Implement callback-based approach for better timing control
10+
- Handle user scroll interruption during loop transitions

src/GestureViewerManager.ts

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,20 @@ class GestureViewerManager {
1212
private dataLength = 0;
1313
private width = 0;
1414
private height = 0;
15-
private scale: SharedValue<number> | null = null;
16-
private translateX: SharedValue<number> | null = null;
17-
private translateY: SharedValue<number> | null = null;
1815
private maxZoomScale = 2;
19-
private listRef: any | null = null;
2016
private enableSwipeGesture = true;
2117
private enableLoop = false;
22-
private listeners = new Set<(state: GestureViewerControllerState) => void>();
18+
private listRef: any | null = null;
19+
20+
private scale: SharedValue<number> | null = null;
2321
private rotation: SharedValue<number> | null = null;
24-
private eventListeners = new Map<GestureViewerEventType, Set<(data: any) => void>>();
25-
private animationTimeout: NodeJS.Timeout | null = null;
22+
private translateX: SharedValue<number> | null = null;
23+
private translateY: SharedValue<number> | null = null;
2624

27-
private static readonly LOOP_ANIMATION_DURATION = 300;
25+
private loopCallback: (() => void) | null = null;
26+
27+
private listeners = new Set<(state: GestureViewerControllerState) => void>();
28+
private eventListeners = new Map<GestureViewerEventType, Set<(data: any) => void>>();
2829

2930
private notifyListeners() {
3031
const state = this.getState();
@@ -227,34 +228,34 @@ class GestureViewerManager {
227228
return;
228229
}
229230

230-
this.cancelAnimation();
231+
this.loopCallback = null;
231232

232233
const { scrollTo } = createScrollAction(this.listRef, this.width);
233234

234235
if (this.enableLoop && this.dataLength > 1) {
235236
if (index < 0) {
236-
scrollTo(0, true);
237-
238-
this.animationTimeout = setTimeout(() => {
237+
this.loopCallback = () => {
239238
scrollTo(this.dataLength, false);
240239
this.updateCurrentIndex(this.dataLength - 1);
241-
}, GestureViewerManager.LOOP_ANIMATION_DURATION);
240+
this.loopCallback = null;
241+
};
242+
243+
scrollTo(0, true);
242244
return;
243245
}
244246

245247
if (index >= this.dataLength) {
246-
scrollTo(this.dataLength + 1, true);
247-
248-
this.animationTimeout = setTimeout(() => {
248+
this.loopCallback = () => {
249249
scrollTo(1, false);
250250
this.updateCurrentIndex(0);
251-
}, GestureViewerManager.LOOP_ANIMATION_DURATION);
251+
this.loopCallback = null;
252+
};
253+
254+
scrollTo(this.dataLength + 1, true);
252255
return;
253256
}
254257

255-
const scrollIndex = index + 1;
256-
257-
scrollTo(scrollIndex, true);
258+
scrollTo(index + 1, true);
258259
this.updateCurrentIndex(index);
259260

260261
return;
@@ -268,11 +269,22 @@ class GestureViewerManager {
268269
this.updateCurrentIndex(index);
269270
};
270271

271-
cancelAnimation = () => {
272-
if (this.animationTimeout) {
273-
clearTimeout(this.animationTimeout);
274-
this.animationTimeout = null;
272+
handleMomentumScrollEnd = (scrollIndex: number) => {
273+
if (!this.loopCallback) {
274+
return false;
275+
}
276+
277+
if (scrollIndex === 0 || scrollIndex === this.dataLength + 1) {
278+
this.loopCallback();
279+
return true;
275280
}
281+
282+
this.loopCallback = null;
283+
return true;
284+
};
285+
286+
handleScrollBeginDrag = () => {
287+
this.loopCallback = null;
276288
};
277289

278290
goToPrevious = () => {
@@ -284,7 +296,7 @@ class GestureViewerManager {
284296
};
285297

286298
cleanUp() {
287-
this.cancelAnimation();
299+
this.loopCallback = null;
288300
this.listeners.clear();
289301
this.listRef = null;
290302
this.enableSwipeGesture = true;

src/useGestureViewer.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,12 @@ export const useGestureViewer = <T = any>({
228228
const contentOffset = event.nativeEvent.contentOffset;
229229
const scrollIndex = Math.round(contentOffset.x / (width + itemSpacing));
230230

231+
const isLoopHandled = manager?.handleMomentumScrollEnd(scrollIndex);
232+
233+
if (isLoopHandled) {
234+
return;
235+
}
236+
231237
const { realIndex, needsJump, jumpToIndex } = getLoopAdjustedIndex(scrollIndex, dataLength, enableLoop);
232238

233239
if (needsJump && jumpToIndex !== undefined) {
@@ -482,7 +488,7 @@ export const useGestureViewer = <T = any>({
482488
}, [animateBackdrop]);
483489

484490
const onScrollBeginDrag = useCallback(() => {
485-
manager?.cancelAnimation();
491+
manager?.handleScrollBeginDrag();
486492
}, [manager]);
487493

488494
return {

0 commit comments

Comments
 (0)