From 6708b12bc2e08037602cdf168cc665fa63fb62a7 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 24 Jan 2024 15:06:19 +0200 Subject: [PATCH 01/24] docs: correct example app commands --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d841de23..2916faa6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ yarn > While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`yarn`](https://classic.yarnpkg.com/), so you'll have an easier time if you use `yarn` for development. -While developing, you can run the [example app](/exampleExpo/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app. +While developing, you can run the [example app](/example/expo/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app. To start build: @@ -23,19 +23,19 @@ yarn dev To run the example app on Android: ```sh -yarn android +cd example/expo && yarn && yarn android ``` To run the example app on iOS: ```sh -yarn ios +cd example/expo && yarn && yarn ios ``` To run the example app on Web: ```sh -yarn web +cd example/expo && yarn && yarn web ``` Make sure your code passes TypeScript and ESLint. Run the following to verify: From de5bca3f71b035cd391d9abe289b6900139655f6 Mon Sep 17 00:00:00 2001 From: Le Van Thuan My <51257500+levanthuanmy@users.noreply.github.com> Date: Thu, 25 Jan 2024 10:30:56 +0000 Subject: [PATCH 02/24] refactor: using requestAnimationFrame instead of setTimeout to reduce frame skipping --- src/hooks/useAutoPlay.ts | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/hooks/useAutoPlay.ts b/src/hooks/useAutoPlay.ts index 64ef1d78..0799ad75 100644 --- a/src/hooks/useAutoPlay.ts +++ b/src/hooks/useAutoPlay.ts @@ -16,26 +16,37 @@ export function useAutoPlay(opts: { } = opts; const { prev, next } = carouselController; - const timer = React.useRef>(); + const lastTimestampRef = React.useRef(null); const stopped = React.useRef(!autoPlay); const play = React.useCallback(() => { if (stopped.current) return; - timer.current && clearTimeout(timer.current); - timer.current = setTimeout(() => { - autoPlayReverse - ? prev({ onFinished: play }) - : next({ onFinished: play }); - }, autoPlayInterval); + const currentTimestamp = Date.now(); + + if (lastTimestampRef.current) { + const elapsed = currentTimestamp - lastTimestampRef.current; + + if (elapsed >= (autoPlayInterval ?? 1000)) { + autoPlayReverse + ? prev({ onFinished: play }) + : next({ onFinished: play }); + lastTimestampRef.current = currentTimestamp; + } + } + else { + lastTimestampRef.current = currentTimestamp; + } + + requestAnimationFrame(play); }, [autoPlayReverse, autoPlayInterval, prev, next]); const pause = React.useCallback(() => { if (!autoPlay) return; - timer.current && clearTimeout(timer.current); + lastTimestampRef.current = null; stopped.current = true; }, [autoPlay]); @@ -44,7 +55,8 @@ export function useAutoPlay(opts: { return; stopped.current = false; - play(); + lastTimestampRef.current = Date.now(); + requestAnimationFrame(play); }, [play, autoPlay]); React.useEffect(() => { @@ -53,7 +65,10 @@ export function useAutoPlay(opts: { else pause(); - return pause; + return () => { + lastTimestampRef.current = null; + stopped.current = true; + }; }, [pause, start, autoPlay]); return { From c329bd6255acdf5068fd8ab6d24ec38992d64f78 Mon Sep 17 00:00:00 2001 From: Nate Massey Date: Sun, 24 Mar 2024 20:20:33 -0700 Subject: [PATCH 03/24] fix: race condition between onGestureStart and onGestureUpdate, using panOffset --- src/components/ScrollViewGesture.tsx | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/components/ScrollViewGesture.tsx b/src/components/ScrollViewGesture.tsx index 41e39b4b..7ca41554 100644 --- a/src/components/ScrollViewGesture.tsx +++ b/src/components/ScrollViewGesture.tsx @@ -65,7 +65,7 @@ const IScrollViewGesture: React.FC> = (props) => { const maxPage = dataLength; const isHorizontal = useDerivedValue(() => !vertical, [vertical]); const max = useSharedValue(0); - const panOffset = useSharedValue(0); + const panOffset = useSharedValue(undefined); // set to undefined when not actively in a pan gesture const touching = useSharedValue(false); const validStart = useSharedValue(false); const scrollEndTranslation = useSharedValue(0); @@ -290,6 +290,19 @@ const IScrollViewGesture: React.FC> = (props) => { const onGestureUpdate = useCallback((e: PanGestureHandlerEventPayload) => { "worklet"; + if (panOffset.value === undefined) { + // This may happen if `onGestureStart` is called as a part of the + // JS thread (instead of the UI thread / worklet). If so, when + // `onGestureStart` sets panOffset.value, the set will be asynchronous, + // and so it may not actually occur before `onGestureUpdate` is called. + // + // Keeping this value as `undefined` when it is not active protects us + // from the situation where we may use the previous value for panOffset + // instead; this would cause a visual flicker in the carousel. + console.warn("onGestureUpdate: panOffset is undefined"); + return; + } + if (validStart.value) { validStart.value = false; cancelAnimation(translation); @@ -334,6 +347,11 @@ const IScrollViewGesture: React.FC> = (props) => { const onGestureEnd = useCallback((e: GestureStateChangeEvent, _success: boolean) => { "worklet"; + if (panOffset.value === undefined) { + console.warn("onGestureEnd: panOffset is undefined"); + return; + } + const { velocityX, velocityY, translationX, translationY } = e; scrollEndVelocity.value = isHorizontal.value ? velocityX @@ -379,6 +397,8 @@ const IScrollViewGesture: React.FC> = (props) => { if (!loop) touching.value = false; + + panOffset.value = undefined; }, [ size, loop, From b06e214fab930f42cee54f1a09974ef784351537 Mon Sep 17 00:00:00 2001 From: Nate Massey Date: Sun, 24 Mar 2024 20:46:51 -0700 Subject: [PATCH 04/24] fix: gesture.onStart/onUpdate/onEnd functions aren't automatically workletified, so be explicit --- src/hooks/usePanGestureProxy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/hooks/usePanGestureProxy.ts b/src/hooks/usePanGestureProxy.ts index 82d2a62b..ff819826 100644 --- a/src/hooks/usePanGestureProxy.ts +++ b/src/hooks/usePanGestureProxy.ts @@ -78,18 +78,21 @@ export const usePanGestureProxy = ( // Setup the original callbacks with the user defined callbacks gesture .onStart((e) => { + "worklet"; onGestureStart(e); if (userDefinedConflictGestures.onStart) userDefinedConflictGestures.onStart(e); }) .onUpdate((e) => { + "worklet"; onGestureUpdate(e); if (userDefinedConflictGestures.onUpdate) userDefinedConflictGestures.onUpdate(e); }) .onEnd((e, success) => { + "worklet"; onGestureEnd(e, success); if (userDefinedConflictGestures.onEnd) From 17d38711aeba4e2311d784879286aa54f5efb033 Mon Sep 17 00:00:00 2001 From: Nate Massey Date: Sun, 24 Mar 2024 21:09:59 -0700 Subject: [PATCH 05/24] fix: tests rely on onBegin/onFinalize -- and so may users! Let's workletify and handle these, too --- src/hooks/usePanGestureProxy.ts | 37 ++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/hooks/usePanGestureProxy.ts b/src/hooks/usePanGestureProxy.ts index ff819826..72f4c895 100644 --- a/src/hooks/usePanGestureProxy.ts +++ b/src/hooks/usePanGestureProxy.ts @@ -27,20 +27,32 @@ export const usePanGestureProxy = ( // Save the original gesture callbacks const originalGestures = { + onBegin: gesture.onBegin, onStart: gesture.onStart, onUpdate: gesture.onUpdate, onEnd: gesture.onEnd, + onFinalize: gesture.onFinalize, }; // Save the user defined gesture callbacks const userDefinedConflictGestures: { + onBegin?: Parameters<(typeof gesture)["onBegin"]>[0] onStart?: Parameters<(typeof gesture)["onStart"]>[0] onUpdate?: Parameters<(typeof gesture)["onUpdate"]>[0] onEnd?: Parameters<(typeof gesture)["onEnd"]>[0] + onFinalize?: Parameters<(typeof gesture)["onFinalize"]>[0] } = { + onBegin: undefined, onStart: undefined, onUpdate: undefined, onEnd: undefined, + onFinalize: undefined, + }; + + const fakeOnBegin: typeof gesture.onBegin = (cb) => { + // Using fakeOnBegin to save the user defined callback + userDefinedConflictGestures.onBegin = cb; + return gesture; }; const fakeOnStart: typeof gesture.onStart = (cb) => { @@ -61,22 +73,38 @@ export const usePanGestureProxy = ( return gesture; }; + const fakeOnFinalize: typeof gesture.onFinalize = (cb) => { + // Using fakeOnFinalize to save the user defined callback + userDefinedConflictGestures.onFinalize = cb; + return gesture; + }; + // Setup the fake callbacks + gesture.onBegin = fakeOnBegin; gesture.onStart = fakeOnStart; gesture.onUpdate = fakeOnUpdate; gesture.onEnd = fakeOnEnd; + gesture.onFinalize = fakeOnFinalize; if (onConfigurePanGesture) // Get the gesture with the user defined configuration onConfigurePanGesture(gesture); // Restore the original callbacks + gesture.onBegin = originalGestures.onBegin; gesture.onStart = originalGestures.onStart; gesture.onUpdate = originalGestures.onUpdate; gesture.onEnd = originalGestures.onEnd; + gesture.onFinalize = originalGestures.onFinalize; // Setup the original callbacks with the user defined callbacks gesture + .onBegin((e) => { + "worklet"; + + if (userDefinedConflictGestures.onBegin) + userDefinedConflictGestures.onBegin(e); + }) .onStart((e) => { "worklet"; onGestureStart(e); @@ -97,7 +125,14 @@ export const usePanGestureProxy = ( if (userDefinedConflictGestures.onEnd) userDefinedConflictGestures.onEnd(e, success); - }); + }) + .onFinalize((e, success) => { + "worklet"; + + if (userDefinedConflictGestures.onFinalize) + userDefinedConflictGestures.onFinalize(e, success); + }) + ; return gesture; }, [ From 20f05efe93befa30a3589cc30362422c99e7b85f Mon Sep 17 00:00:00 2001 From: Nate Massey Date: Mon, 25 Mar 2024 09:43:25 -0700 Subject: [PATCH 06/24] fix: comment out console.warn messages --- src/components/ScrollViewGesture.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/ScrollViewGesture.tsx b/src/components/ScrollViewGesture.tsx index 7ca41554..c7925c85 100644 --- a/src/components/ScrollViewGesture.tsx +++ b/src/components/ScrollViewGesture.tsx @@ -299,7 +299,8 @@ const IScrollViewGesture: React.FC> = (props) => { // Keeping this value as `undefined` when it is not active protects us // from the situation where we may use the previous value for panOffset // instead; this would cause a visual flicker in the carousel. - console.warn("onGestureUpdate: panOffset is undefined"); + + // console.warn("onGestureUpdate: panOffset is undefined"); return; } @@ -348,7 +349,7 @@ const IScrollViewGesture: React.FC> = (props) => { "worklet"; if (panOffset.value === undefined) { - console.warn("onGestureEnd: panOffset is undefined"); + // console.warn("onGestureEnd: panOffset is undefined"); return; } From 2a66e99946171ddb8c5f0f6ac5d40f3e31d3ca6c Mon Sep 17 00:00:00 2001 From: Nate Massey Date: Mon, 25 Mar 2024 10:42:27 -0700 Subject: [PATCH 07/24] fix: usePanGestureProxy.test: add a test to ensure the console.error was not called --- src/hooks/usePanGestureProxy.test.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/hooks/usePanGestureProxy.test.tsx b/src/hooks/usePanGestureProxy.test.tsx index 4bd649c2..4e22f95d 100644 --- a/src/hooks/usePanGestureProxy.test.tsx +++ b/src/hooks/usePanGestureProxy.test.tsx @@ -140,6 +140,29 @@ describe("Using RNGH v2 gesture API", () => { expect.objectContaining({ translationX: 20 }), ); }); + + it("does not include console.error in the output", () => { + // if react-native-gesture-handler detects that some handlers are + // workletized and some are not, it will log an error to the + // console. We'd like to make sure that this doesn't happen. + + // The error that would be shown looks like: + // [react-native-gesture-handler] Some of the callbacks in the gesture are worklets and some are not. Either make sure that all calbacks are marked as 'worklet' if you wish to run them on the UI thread or use '.runOnJS(true)' modifier on the gesture explicitly to run all callbacks on the JS thread. + + const panHandlers = mockedEventHandlers(); + const panHandlersFromUser = mockedEventHandlersFromUser(); + + jest.spyOn(console, "error"); + + render(); + fireGestureHandler(getByGestureTestId("pan"), [ + { state: State.BEGAN }, + { state: State.ACTIVE }, + { state: State.END }, + ]); + + expect(console.error).not.toBeCalled(); + }); }); describe("Event list validation", () => { From d7a8245cfc5382b3826a1f5e0d1ec64704468a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Morales?= Date: Sat, 30 Mar 2024 23:16:52 -0500 Subject: [PATCH 08/24] :sparkles: feature(docs): add a explained documentation to the Ref section in Props. --- example/website/pages/props.mdx | 83 +++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/example/website/pages/props.mdx b/example/website/pages/props.mdx index 39e00168..f28fdd40 100644 --- a/example/website/pages/props.mdx +++ b/example/website/pages/props.mdx @@ -408,6 +408,89 @@ Slide direction ## Ref +By using these methods, remember you need to reference the component using [React useRef()](https://react.dev/reference/react/useRef). + +**JavaScript** + +```js +const ref = React.useRef(null) +``` + +If you're using **TypeScript**: + +You need to import: + +```ts +import type { ICarouselInstance } from "react-native-reanimated-carousel"; +``` + +and then: + +```ts +const ref = React.useRef(null); +``` + +Now, you only need to pass the ref to the Carousel component: + +```js +; +``` + +And now you can use these methods throughout your component. Here's an example of implementing a button to go to the next slide: + +```tsx +import React from "react"; +import Carousel from "react-native-reanimated-carousel"; +import type { ICarouselInstance } from "react-native-reanimated-carousel"; +import { Button, Text, View } from "react-native"; + +// 1. Create a data array with the slides +const data = [ + { + title: "Slide 1", + content: "Slide 1 Content", + }, + { + title: "Slide 2", + content: "Slide 2 Content", + }, + { + title: "Slide 3", + content: "Slide 3 Content", + }, +]; + +const Example = () => { + const ref = React.useRef(null); // 2. Create a ref for the Carousel component + + return ( + + {/* 3. Add the Carousel component with the ref */} + ( + + {item.title} + {item.content} + + )} + /> + {/* 5. Add a button to trigger the next slide */} +