react-native-reanimated-carousel icon indicating copy to clipboard operation
react-native-reanimated-carousel copied to clipboard

fix: flicker when starting pan: `panOffset` value race condition

Open nmassey opened this issue 11 months ago • 20 comments

What: the bug

When onGestureUpdate is called, it may be using the previous value for panOffset (rather than the value just set in onGestureStart). This seems to cause a flicker on slow devices since a translation is shown that is from a different screen.

I see this flicker most obviously on my Android simulator (please see video demonstration below), but the problem exists on other devices as well.

Why

This is due to updates to shared values created via useSharedValue() updating asynchronously (see https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#remarks ).

This flicker seemed to get worse after this commit https://github.com/dohooo/react-native-reanimated-carousel/commit/b654f439e905bc1d45f5cbb1fd291f3a82848368 - "feat: replaced onScrollBegin with onScrollStart" (I'm not sure if there's a corresponding PR?). This is due to the order of the handlers being triggered: onBegin then onStart then onUpdate ...

  • Before this commit, the panOffset was saved during onBegin -- which would often happen much longer before onUpdate.
  • After this commit, the panOffset is saved during onStart -- which might happen immediately before onUpdate and thus may not allow enough time for the asynchronous update to occur.

What: proposed fix

Let's add a check to make sure that panOffset has a valid value before we try to use it.

  1. When a pan is not active, panOffset will have a value of undefined.
  2. (Upon pan start, in onGestureStart, set panOffset to its correct value. This is not a change from existing code.)
  3. In onGestureUpdate and onGestureEnd, check to ensure that panOffset is not undefined before attempting to use it.
  4. In onGestureEnd, reset panOffset to undefined (since pan is no longer active).

Screengrab video recordings

These recordings were taken on an Android simulator.

buggy behavior on 4.0.0-alpha.10 (unpatched)

Notice the flicker to a different "page" when starting pan (i.e. starting touch).

https://github.com/dohooo/react-native-reanimated-carousel/assets/589004/94d33111-2d01-42af-a182-458e83e4d086

improved behavior with patch

https://github.com/dohooo/react-native-reanimated-carousel/assets/589004/f3cd5968-136e-433b-b5ce-152bf4720dc9

nmassey avatar Mar 25 '24 16:03 nmassey

🦋 Changeset detected

Latest commit: a99f069b5c4bace6e0cf42d457d3d3363321dd7f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
react-native-reanimated-carousel Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

changeset-bot[bot] avatar Mar 25 '24 16:03 changeset-bot[bot]

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-native-reanimated-carousel ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 6, 2024 4:31pm

vercel[bot] avatar Mar 25 '24 16:03 vercel[bot]

@nmassey have you found a workaround while you wait this to be merged? I have a release blocked because of this (I guess)

BrodaNoel avatar Apr 21 '24 23:04 BrodaNoel

@BrodaNoel wrote:

@nmassey have you found a workaround while you wait this to be merged? I have a release blocked because of this (I guess)

I use yarn patch to get this in (along with a couple of other tweaks I have).

Are you on the Discord?

nmassey avatar Apr 22 '24 03:04 nmassey

@nmassey I just joined that Discord, but if you can share the patch here, will be awesome!

BrodaNoel avatar Apr 22 '24 14:04 BrodaNoel

I had created and applied this patch, but it doesn't fix the issue I mention in https://github.com/dohooo/react-native-reanimated-carousel/issues/590 (which I thiiiink it's the same as you are trying to fix here)

diff --git a/node_modules/react-native-reanimated-carousel/src/components/ScrollViewGesture.tsx b/node_modules/react-native-reanimated-carousel/src/components/ScrollViewGesture.tsx
index 41e39b4..f71128f 100644
--- a/node_modules/react-native-reanimated-carousel/src/components/ScrollViewGesture.tsx
+++ b/node_modules/react-native-reanimated-carousel/src/components/ScrollViewGesture.tsx
@@ -65,7 +65,7 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
   const maxPage = dataLength;
   const isHorizontal = useDerivedValue(() => !vertical, [vertical]);
   const max = useSharedValue(0);
-  const panOffset = useSharedValue(0);
+  const panOffset = useSharedValue<number | undefined>(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,20 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (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 +348,10 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
   const onGestureEnd = useCallback((e: GestureStateChangeEvent<PanGestureHandlerEventPayload>, _success: boolean) => {
     "worklet";
 
+    if (panOffset.value === undefined) {
+      return;
+    }
+
     const { velocityX, velocityY, translationX, translationY } = e;
     scrollEndVelocity.value = isHorizontal.value
       ? velocityX
@@ -379,6 +397,8 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
 
     if (!loop)
       touching.value = false;
+
+    panOffset.value = undefined;
   }, [
     size,
     loop,

BrodaNoel avatar Apr 22 '24 14:04 BrodaNoel

I had created and applied this patch, but it doesn't fix the issue I mention in #590 (which I thiiiink it's the same as you are trying to fix here) ...

In addition to patching the source code, you'll also need to patch the relevant built lib/ files in the plugin. (When you use the plugin from your code, those are the actual JS files that get used.)

You can either patch the source code by hand (possible, but annoying), or patch the target plugin in its own repo and rebuild it to generate the relevant lib/ code (and then copy those files over). I sent some instructions for how I do this on Discord.

Note that you can confirm that your patch is running by adding some easily-recognizable code to the relevant lib/ file. e.g. you can add console.log('hello 123') to a specific lib/ file to make sure that it is getting executed in your local development build.

I hope this helps! 🤓🙏 And, catch you on Discord!

nmassey avatar Apr 22 '24 16:04 nmassey

@nmassey can you please share the versions of all the packages you are using? I applied your PRs but still some small issues (which we talked in Discord) and I'm thinking it may be related to other libraries' versions

BrodaNoel avatar May 03 '24 19:05 BrodaNoel

when it would be released? 😄

sieeniu avatar May 06 '24 10:05 sieeniu

Incredible work, thank you! But before merging, could you gen a changeset for this PR?

npx changeset

dohooo avatar May 06 '24 11:05 dohooo

so it is now ready to merge imho

sieeniu avatar May 08 '24 12:05 sieeniu

^up

sieeniu avatar May 10 '24 15:05 sieeniu

I also see this problem on Alpha 12, on both emulator and real devices, but only android.

alessandro-bottamedi avatar Jun 04 '24 09:06 alessandro-bottamedi

When will this patch be released?

paulokaome avatar Jun 18 '24 02:06 paulokaome

Waiting for this patch sooo much too!

alfilimonov avatar Jun 18 '24 06:06 alfilimonov

Any update for this issue? Would be nice to start using 4.0 but Alpha 12 still has flickering issues.

keenan35i avatar Jul 04 '24 20:07 keenan35i

@dohooo any chance this can be merged soon?

RikSchefferAmsterdam avatar Jul 29 '24 13:07 RikSchefferAmsterdam

Subscribing to know when this is merged.

Going back to old RNCarousel temporarily 🥺

Crizzooo avatar Aug 22 '24 17:08 Crizzooo

hi @Crizzooo - I've been using yarn patch to unblock me while waiting for these PRs to be merged 😕 Are you on the Discord?

nmassey avatar Aug 22 '24 19:08 nmassey

hi @Crizzooo - I've been using yarn patch to unblock me while waiting for these PRs to be merged 😕 Are you on the Discord?

One of our engineers just joined and is applying the patch for us :)

Crizzooo avatar Aug 22 '24 19:08 Crizzooo