Skip to content

Commit

Permalink
fix: better slow pan: if user intent is to stay on current page, _don…
Browse files Browse the repository at this point in the history
…'t_ change page (#731)
  • Loading branch information
dohooo authored Nov 29, 2024
1 parent 86142e0 commit 6e8cdb4
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 39 deletions.
5 changes: 5 additions & 0 deletions .changeset/wise-drinks-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-native-reanimated-carousel": patch
---

improve "slow pan" behavior: if it seems that the user intent is to stay on the current page (because they didn't pan very far; maybe they started panning one direction then reversed direction, etc.), _don't_ actually change page upon gesture completion
2 changes: 2 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
"example/app/node_modules/**/*",
"example/app/metro.config.js",
"example/app/babel.config.js",
"example/app/ios/**/*",
"example/app/android/**/*",
"scripts/**/*",
"package.json"
]
Expand Down
3 changes: 2 additions & 1 deletion example/app/app/demos/basic-layouts/left-align/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CarouselAdvancedSettingsPanel } from "@/components/CarouselAdvancedSettingsPanel";
import { defaultDataWith6Colors } from "@/components/CarouselBasicSettingsPanel";
import { window } from "@/constants/sizes";
import { useAdvancedSettings } from "@/hooks/useSettings";
import { CaptureWrapper } from "@/store/CaptureProvider";
import { renderItem } from "@/utils/render-item";
Expand All @@ -22,7 +23,7 @@ function Index() {
pagingEnabled: true,
snapEnabled: true,
vertical: false,
width: 430,
width: window.width,
},
});

Expand Down
3 changes: 2 additions & 1 deletion example/app/app/demos/basic-layouts/normal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CarouselAdvancedSettingsPanel } from "@/components/CarouselAdvancedSettingsPanel";
import { defaultDataWith6Colors } from "@/components/CarouselBasicSettingsPanel";
import { MAX_WIDTH, window } from "@/constants/sizes";
import { useAdvancedSettings } from "@/hooks/useSettings";
import { CaptureWrapper } from "@/store/CaptureProvider";
import { renderItem } from "@/utils/render-item";
Expand All @@ -24,7 +25,7 @@ function Index() {
pagingEnabled: true,
snapEnabled: true,
vertical: false,
width: 430,
width: window.width,
},
});

Expand Down
5 changes: 3 additions & 2 deletions example/app/app/demos/basic-layouts/parallax/demo.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { window } from "@/constants/sizes";
import { renderItem } from "@/utils/render-item";
import * as React from "react";
import { View } from "react-native";
Expand All @@ -18,9 +19,9 @@ function Index() {
loop={true}
pagingEnabled={true}
snapEnabled={true}
width={430}
width={window.width}
style={{
width: 430,
width: window.width,
}}
mode="parallax"
modeConfig={{
Expand Down
2 changes: 1 addition & 1 deletion example/app/app/demos/basic-layouts/parallax/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function Index() {
pagingEnabled: true,
snapEnabled: true,
vertical: false,
width: 430,
width: PAGE_WIDTH,
},
});

Expand Down
3 changes: 2 additions & 1 deletion example/app/app/demos/basic-layouts/stack/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Carousel, { ICarouselInstance } from "react-native-reanimated-carousel";
import { CustomSelectActionItem } from "@/components/ActionItems";
import { CarouselAdvancedSettingsPanel } from "@/components/CarouselAdvancedSettingsPanel";
import { defaultDataWith6Colors } from "@/components/CarouselBasicSettingsPanel";
import { window } from "@/constants/sizes";
import { useAdvancedSettings } from "@/hooks/useSettings";
import { CaptureWrapper } from "@/store/CaptureProvider";
import { renderItem } from "@/utils/render-item";
Expand All @@ -26,7 +27,7 @@ function Index() {
pagingEnabled: true,
snapEnabled: true,
vertical: false,
width: 430,
width: window.width,
},
});

Expand Down
63 changes: 33 additions & 30 deletions src/components/Carousel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,17 @@ describe("Test the real swipe behavior of Carousel to ensure it's working as exp
// Helper function to simulate swipe
const swipeToLeftOnce = (
options: {
itemWidth: number;
} = {
itemWidth: slideWidth,
}
itemWidth?: number;
velocityX?: number;
} = {}
) => {
const { itemWidth } = options;
const { itemWidth = slideWidth, velocityX = -slideWidth } = options;
fireGestureHandler<PanGesture>(getByGestureTestId(gestureTestId), [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: -itemWidth * 0.25 },
{ state: State.ACTIVE, translationX: -itemWidth * 0.5 },
{ state: State.ACTIVE, translationX: -itemWidth * 0.75 },
{ state: State.END, translationX: -itemWidth },
{ state: State.BEGAN, translationX: 0, velocityX },
{ state: State.ACTIVE, translationX: -itemWidth * 0.25, velocityX },
{ state: State.ACTIVE, translationX: -itemWidth * 0.5, velocityX },
{ state: State.ACTIVE, translationX: -itemWidth * 0.75, velocityX },
{ state: State.END, translationX: -itemWidth, velocityX },
]);
};

Expand Down Expand Up @@ -301,9 +300,9 @@ describe("Test the real swipe behavior of Carousel to ensure it's working as exp
await verifyInitialRender(getByTestId);

fireGestureHandler<PanGesture>(getByGestureTestId(gestureTestId), [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: -slideWidth * 0.15, velocityX: 0 },
{ state: State.END, translationX: -slideWidth * 0.25, velocityX: 0 },
{ state: State.BEGAN, translationX: 0, velocityX: -5 },
{ state: State.ACTIVE, translationX: -slideWidth * 0.15, velocityX: -5 },
{ state: State.END, translationX: -slideWidth * 0.25, velocityX: -5 },
]);

await waitFor(() => expect(progress.current).toBe(0));
Expand All @@ -314,9 +313,9 @@ describe("Test the real swipe behavior of Carousel to ensure it's working as exp
await verifyInitialRender(getByTestId);

fireGestureHandler<PanGesture>(getByGestureTestId(gestureTestId), [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: -slideWidth * 0.15, velocityX: 0 },
{ state: State.END, translationX: -slideWidth * 0.25, velocityX: 0 },
{ state: State.BEGAN, translationX: 0, velocityX: -1000 },
{ state: State.ACTIVE, translationX: -slideWidth * 0.15, velocityX: -1000 },
{ state: State.END, translationX: -slideWidth * 0.25, velocityX: -1000 },
]);

await waitFor(() => expect(progress.current).toBe(1));
Expand Down Expand Up @@ -354,9 +353,9 @@ describe("Test the real swipe behavior of Carousel to ensure it's working as exp
await verifyInitialRender(getByTestId);

fireGestureHandler<PanGesture>(getByGestureTestId(gestureTestId), [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: slideWidth / 2 },
{ state: State.END, translationX: slideWidth },
{ state: State.BEGAN, translationX: 0, velocityX: 1000 },
{ state: State.ACTIVE, translationX: slideWidth / 2, velocityX: 1000 },
{ state: State.END, translationX: slideWidth, velocityX: 1000 },
]);

await waitFor(() => {
Expand All @@ -377,9 +376,9 @@ describe("Test the real swipe behavior of Carousel to ensure it's working as exp
await verifyInitialRender(getByTestId);

fireGestureHandler<PanGesture>(getByGestureTestId(gestureTestId), [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: slideWidth / 2 },
{ state: State.END, translationX: slideWidth },
{ state: State.BEGAN, translationX: 0, velocityX: 1000 },
{ state: State.ACTIVE, translationX: slideWidth / 2, velocityX: 1000 },
{ state: State.END, translationX: slideWidth, velocityX: 1000 },
]);

await waitFor(() => {
Expand Down Expand Up @@ -407,9 +406,9 @@ describe("Test the real swipe behavior of Carousel to ensure it's working as exp
});

fireGestureHandler<PanGesture>(getByGestureTestId(gestureTestId), [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: -slideWidth / 2 },
{ state: State.END, translationX: -slideWidth },
{ state: State.BEGAN, translationX: 0, velocityX: -1000 },
{ state: State.ACTIVE, translationX: -slideWidth / 2, velocityX: -1000 },
{ state: State.END, translationX: -slideWidth, velocityX: -1000 },
]);

await waitFor(() => {
Expand All @@ -419,21 +418,25 @@ describe("Test the real swipe behavior of Carousel to ensure it's working as exp
});

it("`fixedDirection` prop: should swipe to the correct direction when fixedDirection is positive", async () => {
const progress = { current: 0 };
const Wrapper = createCarousel(progress);
{
const progress = { current: 0 };
const Wrapper = createCarousel(progress);
const { getByTestId } = render(<Wrapper fixedDirection="positive" />);
await verifyInitialRender(getByTestId);

swipeToLeftOnce();
await waitFor(() => expect(progress.current).toBe(3));
swipeToLeftOnce({ velocityX: slideWidth });
await waitFor(() => {
expect(progress.current).toBe(3);
});
}

{
const progress = { current: 0 };
const Wrapper = createCarousel(progress);
const { getByTestId } = render(<Wrapper fixedDirection="negative" />);
await verifyInitialRender(getByTestId);

swipeToLeftOnce();
swipeToLeftOnce({ velocityX: -slideWidth });
await waitFor(() => expect(progress.current).toBe(1));
}
});
Expand Down
19 changes: 16 additions & 3 deletions src/components/ScrollViewGesture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,27 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
*
* `page size` equals to `size` variable.
* */

// calculate target "nextPage" based on the final pan position and the velocity of
// the pan gesture at termination; this allows for a quick "flick" to indicate a far
// off page change.
const nextPage = -Math.round((origin + velocity * 2) / size);

if (pagingEnabled) {
// we'll never go further than a single page away from the current page when paging
// is enabled.

// distance with direction
const offset = -(scrollEndTranslationValue >= 0 ? 1 : -1); // 1 or -1
const computed = offset < 0 ? Math.ceil : Math.floor;
const page = computed(-origin / size);

if (loop) {
const velocityDirection = -Math.sign(velocity);
if (page === nextPage || velocityDirection !== offset) {
// not going anywhere! Velocity was insufficient to overcome the distance to get to a
// further page. Let's reset gently to the current page.
finalTranslation = withSpring(withProcessTranslation(-page * size), onFinished);
} else if (loop) {
const finalPage = page + offset;
finalTranslation = withSpring(withProcessTranslation(-finalPage * size), onFinished);
} else {
Expand All @@ -162,8 +176,7 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {

if (!pagingEnabled && snapEnabled) {
// scroll to the nearest item
const nextPage = Math.round((origin + velocity * 0.4) / size) * size;
finalTranslation = withSpring(withProcessTranslation(nextPage), onFinished);
finalTranslation = withSpring(withProcessTranslation(-nextPage * size), onFinished);
}
}

Expand Down

0 comments on commit 6e8cdb4

Please sign in to comment.