axelra-ag / react-native-animateable-text

🆎 A fork of React Native's <Text/> component that supports Animated Values!
https://www.npmjs.com/package/react-native-animateable-text
MIT License
360 stars 27 forks source link

How to animate the value over time #26

Open mxmzb opened 2 years ago

mxmzb commented 2 years ago

I am trying to animate the text value over time, like this:

import React from "react";
import AnimateableText from "react-native-animateable-text";
import { useSharedValue, useAnimatedProps, Easing, withTiming } from "react-native-reanimated";
import { SafeAreaView } from "react-native-safe-area-context";

const formatScore = (score = 0) => {
  "worklet";
  return "" + score.toFixed(0);
};

const AnimateScoreTextValueScreen = () => {
  const score = useSharedValue(0);

  React.useEffect(() => {
    score.value = withTiming(500, {
      duration: 1000,
      easing: Easing.inOut(Easing.ease),
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const scoreAnimatedProps = useAnimatedProps(() => ({
    text: formatScore(score.value),
  }));

  return (
    <SafeAreaView>
      <AnimateableText animatedProps={scoreAnimatedProps} />
    </SafeAreaView>
  );
};

export default AnimateScoreTextValueScreen;

With very similar code, I was able to animate paths from react-native-svg. Shouldn't this work?

PS.: Thank you so much for your efforts and for creating this component, much appreciated!

JonnyBurger commented 2 years ago

It's weird, but can you try to first convert it to a derived value using useDerivedValue() and then use it as a prop?

This is some internal thing that I also don't quite understand.

mxmzb commented 2 years ago

I'm not sure how to do that, because useDerivedValue() returns a readonly value. The following seems not to work, but I cannot animate animatedText with withTiming, because it's readonly.

import React from "react";
import AnimateableText from "react-native-animateable-text";
import {
  useSharedValue,
  useAnimatedProps,
  useDerivedValue,
  Easing,
  withTiming,
} from "react-native-reanimated";
import { SafeAreaView } from "react-native-safe-area-context";

const formatScore = (score = 0) => {
  "worklet";
  return "" + score.toFixed(0);
};

const AnimateScoreTextValueScreen = () => {
  const score = useSharedValue(0);

  React.useEffect(() => {
    score.value = withTiming(500, {
      duration: 1000,
      easing: Easing.inOut(Easing.ease),
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const animatedText = useDerivedValue(() => score.value);

  const scoreAnimatedProps = useAnimatedProps(() => ({
    text: formatScore(animatedText.value),
  }));

  return (
    <SafeAreaView>
      <AnimateableText animatedProps={scoreAnimatedProps} />
    </SafeAreaView>
  );
};

export default AnimateScoreTextValueScreen;
jacobmolby commented 2 years ago

Try to call formatScore inside the useDerivedValue hook, and then use the return value directly to text inside useAnimatedProps.

mxmzb commented 2 years ago

Try to call formatScore inside the useDerivedValue hook, and then use the return value directly to text inside useAnimatedProps.

Unfortunately, it doesn't work:

import React from "react";
import AnimateableText from "react-native-animateable-text";
import {
  useSharedValue,
  useAnimatedProps,
  useDerivedValue,
  Easing,
  withTiming,
} from "react-native-reanimated";
import { SafeAreaView } from "react-native-safe-area-context";

const formatScore = (score = 0) => {
  "worklet";

  return "" + score.toFixed(0);
};

const AnimateScoreTextValueScreen = () => {
  const score = useSharedValue(0);

  React.useEffect(() => {
    score.value = withTiming(500, {
      duration: 1000,
      easing: Easing.inOut(Easing.ease),
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const animatedText = useDerivedValue(() => formatScore(score.value));

  // also doesn't work:
  // const animatedText = useDerivedValue(() => "" + score.value.toFixed(0));

  // const scoreAnimatedProps = useAnimatedProps(() => ({
  //   text: formatScore(animatedText.value),
  // }));

  return (
    <SafeAreaView>
      <AnimateableText
        text={animatedText.value}
        // animatedProps={scoreAnimatedProps}
      />
    </SafeAreaView>
  );
};

export default AnimateScoreTextValueScreen;
jacobmolby commented 2 years ago

What if you do it like this?

const progress = useSharedValue(0.5);
const textProps = useAnimatedProps(() => {
    return { text: `${(progress.value * 100).toFixed(0)}%` };
  }, []);

return (<AnimateableText style={styles.text} animatedProps={textProps} />)
mxmzb commented 2 years ago

Unfortunately, doesn't work as well (still animating the progress.value in a useEffect(..., []) with withTiming).

Edit: Hmm, at this point, it feels like I'm doing something wrong...

JonnyBurger commented 2 years ago

What about:

import React from "react";
import AnimateableText from "react-native-animateable-text";
import {
  useSharedValue,
  useAnimatedProps,
  useDerivedValue,
  Easing,
  withTiming,
} from "react-native-reanimated";
import { SafeAreaView } from "react-native-safe-area-context";

const formatScore = (score = 0) => {
  "worklet";
  return "" + score.toFixed(0);
};

const AnimateScoreTextValueScreen = () => {
  const score = useSharedValue(0);

  React.useEffect(() => {
    score.value = withTiming(500, {
      duration: 1000,
      easing: Easing.inOut(Easing.ease),
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const animatedText = useDerivedValue(() => formatScore(score.value));

  const scoreAnimatedProps = useAnimatedProps(() => ({
    text: animatedText.value,
  }));

  return (
    <SafeAreaView>
      <AnimateableText animatedProps={scoreAnimatedProps} />
    </SafeAreaView>
  );
};

export default AnimateScoreTextValueScreen;

I don't know how these rules apply either, it's weird. But the demo code in the README should work, therefore I adjusted your example a bit and this is what I suggest.

mxmzb commented 2 years ago

Nope, it just stays at 0. Am I missing any steps before maybe? I followed the reanimated installation guide, have the babel plugin, anything else?

The only way this works for me is just by arbitrarily changing the return of the useDerivedValue hook (e.g.

const animatedText = useDerivedValue(() => "this is some new change" + score.value);

). I've also tried to set random values with a setTimeout loop, that didn't work either.

mikehuebner commented 1 year ago

@mxmzb It has been a year but I came across this same problem! So I spent my entire day trying a ton of solutions.

Here is what I came up with that works.

const CurrencyDisplay = ({ value: currencySharedValue, ...props }: CurrencyDisplayProps) => {
  // NativeBase issue as I like having my style props >:(
  // This should be at the top of the file but for whatever reason, it doesn't work like that.
  const AnimateableText = Factory(NativeAnimateableText);
  const intl = useIntl();

  // Have our pseudo value
  const animatedValue = useSharedValue('0');

  // This isn't a worklet, but it works.
  const formatNumber = useCallback((formatValue: number) => {
    // Attach formatted number to the above value when ran
    animatedValue.value = intl.formatNumber(formatValue, {
      style: 'currency',
      currency: 'USD',
      minimumFractionDigits: 0,
      maximumFractionDigits: 0,
    });
  }, []);

  // After one second, start at 0, go to whatever value you set
  const animateToValue = useDerivedValue(() =>
    withDelay(
      1000,
      withSequence(
        withTiming(0, {
          easing: Easing.inOut(Easing.ease),
        }),
        // Just solved this but wanted to add a decay animation, and decay can't be used for something like this
        withSpring(currencySharedValue.value, { damping: 95, stiffness: 1 })
      )
    )
  );

  // When values update, update this using the JS thread so we can use intl
  useDerivedValue(() => {
    runOnJS(formatNumber)(animateToValue.value);
  });

  const scoreAnimatedProps = useAnimatedProps(() => ({
    text: animatedValue.value,
  }));

  return <AnimateableText animatedProps={scoreAnimatedProps} {...props} />;
};

Let me know if that works or makes sense!