Skip to content

Commit 4515106

Browse files
committed
better swipe feeling
1 parent 0ffe0f6 commit 4515106

3 files changed

Lines changed: 35 additions & 14 deletions

File tree

src/components/ImageGallery.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
DEFAULT_SLIDE_DURATION,
1717
DEFAULT_SLIDE_INTERVAL,
1818
DEFAULT_SWIPE_THRESHOLD,
19-
DEFAULT_SWIPING_TRANSITION_DURATION,
2019
} from "src/components/constants";
2120
import BottomNav from "src/components/controls/BottomNav";
2221
import Fullscreen from "src/components/controls/Fullscreen";
@@ -154,7 +153,6 @@ const ImageGallery = forwardRef<ImageGalleryRef, ImageGalleryProps>(
154153
startIndex = 0,
155154
stopPropagation = false,
156155
swipeThreshold = DEFAULT_SWIPE_THRESHOLD,
157-
swipingTransitionDuration = DEFAULT_SWIPING_TRANSITION_DURATION,
158156
thumbnailPosition = "bottom",
159157
useBrowserFullscreen = true,
160158
useTranslate3D = true,
@@ -329,7 +327,7 @@ const ImageGallery = forwardRef<ImageGalleryRef, ImageGalleryProps>(
329327
}
330328

331329
const swipingTransition: CSSProperties = {
332-
transition: `transform ${swipingTransitionDuration}ms ease-out`,
330+
transition: "none",
333331
};
334332

335333
setCurrentSlideOffset(side * slideOffset);
@@ -345,7 +343,6 @@ const ImageGallery = forwardRef<ImageGalleryRef, ImageGalleryProps>(
345343
galleryWidth,
346344
galleryHeight,
347345
slideVertically,
348-
swipingTransitionDuration,
349346
swipingUpDown,
350347
swipingLeftRight,
351348
setCurrentSlideOffset,
@@ -354,7 +351,7 @@ const ImageGallery = forwardRef<ImageGalleryRef, ImageGalleryProps>(
354351
);
355352

356353
const handleOnSwipedTo = useCallback(
357-
(swipeDirection: number, isFlick: boolean) => {
354+
(swipeDirection: number, isFlick: boolean, velocity: number) => {
358355
let slideTo = currentIndex;
359356

360357
if ((sufficientSwipe() || isFlick) && !isTransitioning) {
@@ -368,11 +365,35 @@ const ImageGallery = forwardRef<ImageGalleryRef, ImageGalleryProps>(
368365
slideTo = currentIndex;
369366
}
370367

371-
slideToIndexCore(slideTo);
368+
// Compute duration from swipe velocity so the animation continues
369+
// at the same speed the user's finger was moving.
370+
// velocity is in px/ms from the swipe library.
371+
const swipedPercent = Math.abs(currentSlideOffset);
372+
const remainingPercent =
373+
slideTo !== currentIndex
374+
? 100 - swipedPercent // traveling to next/prev slide
375+
: swipedPercent; // snapping back to current slide
376+
const dimension = slideVertically ? galleryHeight : galleryWidth;
377+
const remainingPx = (remainingPercent / 100) * dimension;
378+
// Clamp: at least 80ms (not jarring), at most slideDuration
379+
const velocityDuration =
380+
velocity > 0
381+
? Math.min(
382+
slideDuration,
383+
Math.max(80, Math.round(remainingPx / velocity))
384+
)
385+
: slideDuration;
386+
387+
slideToIndexCore(slideTo, undefined, false, velocityDuration);
372388
},
373389
[
374390
currentIndex,
391+
currentSlideOffset,
375392
isTransitioning,
393+
slideDuration,
394+
slideVertically,
395+
galleryWidth,
396+
galleryHeight,
376397
sufficientSwipe,
377398
canSlideLeft,
378399
canSlideRight,
@@ -399,7 +420,7 @@ const ImageGallery = forwardRef<ImageGalleryRef, ImageGalleryProps>(
399420

400421
const isFlick = slideVertically ? isTopDownFlick : isLeftRightFlick;
401422

402-
handleOnSwipedTo(swipeDirection, isFlick);
423+
handleOnSwipedTo(swipeDirection, isFlick, velocity);
403424
},
404425
[
405426
disableSwipe,

src/components/constants.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
66
// Animation & Timing
77
export const DEFAULT_SLIDE_DURATION = 550;
88
export const DEFAULT_SLIDE_INTERVAL = 3000;
9-
// Must be 0 for responsive finger tracking - the swipe offset is updated continuously
10-
// by the swipe library, so any transition would cause the image to lag behind the finger
11-
export const DEFAULT_SWIPING_TRANSITION_DURATION = 0;
129

1310
// Thresholds
1411
export const DEFAULT_FLICK_THRESHOLD = 0.4;

src/components/hooks/useGalleryNavigation.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ interface UseGalleryNavigationReturn {
4848
slideToIndexCore: (
4949
index: number,
5050
event?: React.SyntheticEvent | Event,
51-
isPlayPause?: boolean
51+
isPlayPause?: boolean,
52+
customDuration?: number
5253
) => void;
5354
slideToIndexWithStyleReset: (
5455
nextIndex: number,
@@ -193,10 +194,12 @@ export function useGalleryNavigation({
193194
(
194195
index: number,
195196
_event?: React.SyntheticEvent | Event,
196-
isPlayPause = false
197+
isPlayPause = false,
198+
customDuration?: number
197199
): void => {
198200
if ((isTransitioning || isJumpingRef.current) && !isPlayPause) return;
199201

202+
const duration = customDuration ?? slideDuration;
200203
const slideCount = totalSlides - 1;
201204
let nextIndex = index;
202205
let nextDisplayIndex: number;
@@ -238,7 +241,7 @@ export function useGalleryNavigation({
238241
setIsTransitioning(nextIndex !== currentIndex || willWrap);
239242
setCurrentSlideOffset(0);
240243
setSlideStyle({
241-
transition: `transform ${slideDuration}ms ${DEFAULT_EASING}`,
244+
transition: `transform ${duration}ms ${DEFAULT_EASING}`,
242245
});
243246

244247
// Schedule clone jump if we're wrapping in infinite mode
@@ -251,7 +254,7 @@ export function useGalleryNavigation({
251254
// Jump to the real slide after animation completes
252255
const realDisplayIndex = realToDisplayIndex(nextIndex);
253256
handleCloneJump(nextIndex, realDisplayIndex);
254-
}, slideDuration + 20);
257+
}, duration + 20);
255258
}
256259
},
257260
[

0 commit comments

Comments
 (0)