Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] Animations don't always run on component initialization #3296

Open
1 of 3 tasks
computerjazz opened this issue Jun 14, 2022 · 28 comments · May be fixed by #3302
Open
1 of 3 tasks

[iOS] Animations don't always run on component initialization #3296

computerjazz opened this issue Jun 14, 2022 · 28 comments · May be fixed by #3302
Labels
Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snippet of code, snack or repo is provided

Comments

@computerjazz
Copy link

computerjazz commented Jun 14, 2022

Description

In reanimated 2.8.0, animations that run on component initialization don't always animate. I can't figure out why they work sometimes and don't work other times. The strangest thing is that adding a console.log within useAnimatedStyle seems to fix the issue. Wrapping the call that sets the shared value to kick off the initial animation in a setTimeout also seems to fix it (most of the time).

This bug does not occur in 2.3.1.

I believe this is the underlying issue behind this react-native-bottom-sheet issue: gorhom/react-native-bottom-sheet#925

Expected behavior

Animations run on component initialization.
(Below is with a console.log added in useAnimatedStyle):

RPReplay_Final1655231771.MP4

Actual behavior & steps to reproduce

Create a component that has animations that run on initialization. Show/hide component. Animations run sometimes, but sometimes they just jump to final value (or partially animate).

(Without console.log in useAnimatedStyle):

RPReplay_Final1655231740.MP4

Snack or minimal code example

Run on iOS device to see issue: https://snack.expo.dev/@computerjazz/reanimated-init-bug

Package versions

2.8.0

name version
react-native 0.68.2
react-native-reanimated 2.8.0
expo 45

Affected platforms

  • Android
  • iOS
  • Web
@computerjazz computerjazz added the Needs review Issue is ready to be reviewed by a maintainer label Jun 14, 2022
@github-actions github-actions bot added Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snippet of code, snack or repo is provided labels Jun 14, 2022
@graszka22 graszka22 removed the Needs review Issue is ready to be reviewed by a maintainer label Jun 20, 2022
@graszka22 graszka22 linked a pull request Jun 20, 2022 that will close this issue
6 tasks
@Titozzz
Copy link
Contributor

Titozzz commented Jun 21, 2022

I can confirm the bug, the console.log trick. It also affect moti. I'll test the PR and report back

EDIT: also affects android so the PR won't fix all

EDIT2: The PR doesn't fix this issue on iOS for me at least.

@Titozzz
Copy link
Contributor

Titozzz commented Jun 22, 2022

My issue is in fact the same as : #3209. Maybe related?

@computerjazz
Copy link
Author

A few clues (and maybe the reason why console.log fixes it): setting optimalization = 0 here also seems to fix it: https://github.com/software-mansion/react-native-reanimated/blob/main/src/reanimated2/hook/useAnimatedStyle.ts#L481 (or returning false here: https://github.com/software-mansion/react-native-reanimated/blob/main/src/reanimated2/hook/utils.ts#L167-L175)

I believe this is also why console.log fixes it -- the babel transform gives different values here depending on whether a console.log exists, which drives optimalization: https://github.com/software-mansion/react-native-reanimated/blob/main/plugin.js#L745 (isFunctionCall becomes true when a console.log exists)

@computerjazz
Copy link
Author

computerjazz commented Jul 2, 2022

Since setting optimalization = 0 seems to fix it, my gut says that the issues lies somewhere around here:

if (optimalizationLvl == 0) {
mapper->callWithThis(rt, *mapper); // call styleUpdater
} else {
#ifdef RCT_NEW_ARCH_ENABLED
jsi::Value newStyle = userUpdater->call(rt).asObject(rt);
#else
jsi::Object newStyle = userUpdater->call(rt).asObject(rt);
#endif
auto jsViewDescriptorArray = viewDescriptors->getValue(rt)
.getObject(rt)
.getProperty(rt, "value")
.asObject(rt)
.getArray(rt);
for (int i = 0; i < jsViewDescriptorArray.length(rt); ++i) {
auto jsViewDescriptor =
jsViewDescriptorArray.getValueAtIndex(rt, i).getObject(rt);
#ifdef RCT_NEW_ARCH_ENABLED
updateProps(
rt, jsViewDescriptor.getProperty(rt, "shadowNodeWrapper"), newStyle);
#else
updateProps(
rt,
static_cast<int>(jsViewDescriptor.getProperty(rt, "tag").asNumber()),
jsViewDescriptor.getProperty(rt, "name"),
newStyle);
#endif

maybe some kind of race condition with jsViewDescriptors being initialized on mount? I have verified that forcing the non-optimized path by hardcoding if(true){ on line 25 also fixes it.

unfortunately i don't know C++ (yet 😛) so this may be about as far as i can go

@computerjazz
Copy link
Author

@piaskowyk I think the issue has something to do with the performance optimazations or shared styles here: #1470
#1879

@notjosh
Copy link
Contributor

notjosh commented Jul 12, 2022

Can you try it again now that #3374 is merged? It looks like the same behaviour.

creature-water-valley added a commit to ws-4020/mobile-app-crib-notes that referenced this issue Jul 22, 2022
…ker component. (#889)

## ✅ What's done

- [x] Fixed an issue where animations were not executed in the Picker component.
  - related issue
    - software-mansion/react-native-reanimated#3296
- [x] Change DateTimePicker animation duration to  the same value(300msc) as SelectPicker
@naftalibeder
Copy link
Contributor

I can confirm. The explanation @computerjazz gave here looks correct, because any function call (not just console.log) fixes it. Even executing a no-op like

const style = useAnimatedStyle(() => {
  (() => {})();

  return {
    ...
  };
});

causes the style to update propertly.

I applied the changes here, but it did not fix the problem.

@andreibahachenka
Copy link

has anyone found a fix for that?

@lightrow
Copy link

You can use v1 until they fix v2 in v3

@andreibahachenka
Copy link

andreibahachenka commented Oct 17, 2022

it worked with version 2.2.1

@lightrow
Copy link

I mean you can use v1 API with the latest version of reanimated, it doesn't have this bug, at least i haven't been able to reproduce it once i rewrote the affected component with V1 API.

@lightrow
Copy link

lightrow commented Oct 25, 2022

https://docs.swmansion.com/react-native-reanimated/docs/1.x.x/declarative

the same API is still available in 2.x, except Easing functions for v1 are now exported as EasingNode

@andreibahachenka
Copy link

got it!
but I use moti and moti uses reanimated under the hood

@lightrow
Copy link

well, not sure why you'd need to use another abstraction when reanimated itself is already very straightforward to use

@andreibahachenka
Copy link

make the process even easier :)
it’s weird that a few people have this bug although reanimated used by a lot

@lightrow
Copy link

maybe not that many people try to animate height/width of components

@freeboub
Copy link

Short update to notice that this issue can be reproduced on 2.11.0 and also affects android device (I use android tv, but should not impact behavior). workaround here: #3296 (comment) also fix the issue.

@sadia-onyxtec
Copy link

sadia-onyxtec commented Dec 30, 2022

I am facing the same issue in v2.8.0.

@lightrow
Copy link

lightrow commented Feb 16, 2023

simplest reproduction case

const Collapsible = () => {
    const childrenHeight = useSharedValue(0);

    const animStyle = useAnimatedStyle(() => {
        return {
            height: childrenHeight.value,
        };
    }, []);

    useEffect(() => {
        childrenHeight.value = 500;
    }, []);

    return <Animated.View style={[{ width: 500, backgroundColor: 'red' }, animStyle]} />;

};

delaying childrenHeight.value = 500; with setTimeout helps, but the timeout duration has to be substantial (not 0, but 50-100) for it to be a reliable "fix", and at that point the jump becomes too noticeable for users. It also seems to happen more often when JS thread is busy with other stuff (e.g. running effects with API calls)

adding (() => {})(); inside useAnimatedStyle doesn't help in this case

const Collapsible = () => {
    const childrenHeight = useValue(0);

    useEffect(() => {
        childrenHeight.setValue(500);
    }, []);

    return <Animated.View style={[{ width: 500, backgroundColor: 'red', height: childrenHeight }]} />;
};

this works perfectly fine
please don't deprecate V1 without fixing this breaking issue, there is no other reliable workaround for this!

@vancerbtw
Copy link

vancerbtw commented Apr 11, 2023

Any update on this? I am suffering from the same issue with a fade in animation that is triggered on component mount in useEffect. console.logging the shared value is my only current work around. just triggering a () => {} function call does not solve my issue

@davoam
Copy link

davoam commented May 9, 2023

This still happens in reanimated version 3.1.0

@vancerbtw
Copy link

I had this crash happen when changing the content size of a scroll view when it was being animated and the content size changing affecting the current viewport. I solved this by delaying my content change until the scroll animation was completed. still not ideal :(

@Parveshdhull
Copy link

Parveshdhull commented Jun 2, 2023

Weird workaround

(hopefully it will work for you)

Observation:

Problems are with sudden changes during initialization.
For example, in @lightrow's example, he is changing childrenHeight from 0 to 500 instantly.

  • He could have directly initialize the shared value to 500, because in use effect it also just changes to 500(without any animation, so the same effect).
  • And animations also work, like withTiming(500);

Solution

(for sudden changes)

Not so good

childrenHeight.value = withSequence(withTiming(0, {duration: 0}), withTiming(500, {duration: 0}))

This solution works for me, as it also has 0 total duration, and somehow using sequence fixes issue.

Problem: This works great when you are creating/rendering view, but if view is already there like on hot reload, screen will flicker. Because we are hiding view and again showing

Better solution

childrenHeight.value = withSequence(withTiming(501, {duration: 0}), withTiming(500, {duration: 0}))

@lightrow
Copy link

lightrow commented Jun 27, 2023

tried the above, at first it looked like it worked, but it wasn't reliable and sometimes the height still wouldn't be set. Flickering was also still noticable when component fails to set height for the first part of the sequence. All in all it's the same as adding a timeout, only in this case the duration is random and depends on how long the 0 duration animation took to finish

edit: nevermind, i had another component that i forgot to apply this method to inbetween two other components. It seems to be working reliably now!

edit2: well it works but there is visible flickering sometimes, like 1 in 10 cases

edit3: i don't know what happened or changed, but i cannot reproduce this anymore, not even my own example above, which now works fine without any workarounds, on both iOS 16.2 simulator and physical iPhone 13 Pro on iOS 15.7. I'm still on reanimated v3.3.0,

edit4: no it still happens when JS thread is busy, with or without workarounds

@ngokevin
Copy link

ngokevin commented Sep 26, 2023

I ran into this issue on certain Android phones. It's really a race condition that manifests reliably on some phones. In my case, it was with Moti which uses Reanimated. I was on Reanimated v3.3.0.

const anim = useDynamicAnimation(() => ({ opacity: 0 });

useEffect(function () {
  anim.animateTo({ opacity: 1});
}, []);

return anim;

It did help to add delays, or only run the initial onMount animation after a view's onLayout, but it still manifested. Just wanted to comment it wasn't an iOS-only issue for me.

I'll update to see if I can trigger it with Reanimated raw API.

@henrymoulton
Copy link

@piaskowyk any ideas on what we can do to give a better repro given it's a race condition?

@AlexSapoznikov
Copy link

AlexSapoznikov commented May 27, 2024

Has anyone figured out a workaround for it so far? Above workarounds do not work always as @lightrow described.

Edit: Here is a "hackish" workaround I came up with until it gets fixed. I run checks to see if the animation is applied inside an interval with a retry limit. It animates x, y position but can be adjusted to use width and height.

// util: setIntervalWithLimit.ts

export type SetIntervalWithLimitFnStop = () => void;
export type SetIntervalWithLimitFnArgs = {
  stop: SetIntervalWithLimitFnStop;
  sequel: number;
};

export type SetIntervalWithLimitFn = (args: SetIntervalWithLimitFnArgs) => void;

export type SetIntervalWithLimitResponse = {
  ref: NodeJS.Timeout;
  stop: SetIntervalWithLimitFnStop;
};

export const setIntervalWithLimit = (
  fn: SetIntervalWithLimitFn,
  maxLimit: number,
  interval: number,
  cb?: () => void,
): SetIntervalWithLimitResponse => {
  let sequel = 0;

  const stopFn = (intervalRef: NodeJS.Timeout) => () => {
    clearInterval(intervalRef);
    cb?.();
  };

  const ref = setInterval(() => {
    const stop = stopFn(ref);
    sequel++;

    // Run function.
    fn({ stop, sequel });

    // If limit reached, stop.
    if (sequel >= maxLimit) {
      stop();
    }
  }, interval);

  return { ref, stop: stopFn(ref) };
};


// Your component
...
  const ref = useRef<AnimatedView>(null);
  const mountIntervalRef = useRef<SetIntervalWithLimitResponse>();
  
    // update position when item size changes
  useEffect(() => {
    // Sometimes animations won't run because re-animated has a bug https://github.com/software-mansion/react-native-reanimated/issues/3296#issuecomment-1573900172:
    activeX.value = x; // your actual target x
    activeY.value = y; // your actual target y

    // So we set an interval to try execute animation until it actually completes with a limit of X times in order to avoid infinite loop.
    mountIntervalRef.current?.stop();
    mountIntervalRef.current = setIntervalWithLimit(
      ({ stop, sequel }) => {
        ref.current?.measure((originX, originY, _width, _height, _pageX, _pageY) => {
          // Not correct values.
          if (originX !== activeX.value && originY !== activeY.value) {
            // If still broken, try using this:
            activeX.value = withSequence(withTiming(x - 1, { duration: 0 }), withTiming(x, { duration: 0 }));
            activeY.value = withSequence(withTiming(y - 1, { duration: 0 }), withTiming(y, { duration: 0 }));
            return;
          }

          // Success.
          stop();
        });
      },
      20, // Duration,
      50, // Max times we run interval.
    );
  }, [width, height, activeX, activeY]);

  const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [{ translateX: activeX.value }, { translateY: activeY.value }],
    };
  });
  
  return (
    <Animated.View ref={ref} style={[{ position: 'absolute', width, height }, animatedStyle]}>
      {children}
    </Animated.View>
  )

@piaskowyk
Copy link
Member

I know that this is an old issue, but could someone confirm whether this problem still exists in the latest version of Reanimated or if it has been resolved?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snippet of code, snack or repo is provided
Projects
None yet
Development

Successfully merging a pull request may close this issue.