3

How can I create a reusable React hook with animation style with Reanimated 2? I have an animation that is working on one element, but if I try to use the same animation on multiple elements on same screen only the first one registered is animating. It is too much animation code to duplicate it everywhere I need this animation, so how can I share this between multiple components on the same screen? And tips for making the animation simpler is also much appreciated.

import {useEffect} from 'react';
import {
  cancelAnimation,
  Easing,
  useAnimatedStyle,
  useSharedValue,
  withRepeat,
  withSequence,
  withTiming,
} from 'react-native-reanimated';

const usePulseAnimation = ({shouldAnimate}: {shouldAnimate: boolean}) => {
  const titleOpacity = useSharedValue(1);
  const isAnimating = useSharedValue(false);

  useEffect(() => {
    if (shouldAnimate && !isAnimating.value) {
      isAnimating.value = true;
      titleOpacity.value = withRepeat(
        withSequence(
          withTiming(0.2, {duration: 700, easing: Easing.inOut(Easing.ease)}),
          withTiming(
            1,
            {duration: 700, easing: Easing.inOut(Easing.ease)},
            () => {
              if (!shouldAnimate) {
                cancelAnimation(titleOpacity);
              }
            },
          ),
        ),
        -1,
        false,
        () => {
          if (titleOpacity.value < 1) {
            titleOpacity.value = withSequence(
              withTiming(0.2, {
                duration: 700,
                easing: Easing.inOut(Easing.ease),
              }),
              withTiming(
                1,
                {duration: 700, easing: Easing.inOut(Easing.ease)},
                () => {
                  isAnimating.value = false;
                },
              ),
            );
          } else {
            titleOpacity.value = withTiming(
              1,
              {
                duration: 700,
                easing: Easing.inOut(Easing.ease),
              },
              () => {
                isAnimating.value = false;
              },
            );
          }
        },
      );
    } else {
      isAnimating.value = false;
      cancelAnimation(titleOpacity);
    }
  }, [shouldAnimate, isAnimating, titleOpacity]);

  const pulseAnimationStyle = useAnimatedStyle(() => {
    return {
      opacity: titleOpacity.value,
    };
  });

  return {pulseAnimationStyle, isAnimating: isAnimating.value};
};

export default usePulseAnimation;

And I am using it like this inside a component:

const {pulseAnimationStyle} = usePulseAnimation({
  shouldAnimate: true,
});

return (
  <Animated.View
    style={[
      {backgroundColor: 'white', height: 100, width: 100},
      pulseAnimationStyle,
    ]}
  />
);
user2602152
  • 687
  • 7
  • 24

2 Answers2

2

The approach that I've taken is to write my Animations as wrapper components.

This way you can build up a library of these animation components and then simply wrap whatever needs to be animated.

e.g.

//Wrapper component type:
export type ShakeProps = {
  // Animation:
  children: React.ReactNode;
  repeat?: boolean;
  repeatEvery?: number;
}

// Wrapper component:
const Shake: FC<ShakeProps> = ({
  children,
  repeat = false,
  repeatEvery = 5000,
}) => {
  
  const shiftY = useSharedValue(0);

  const animatedStyles = useAnimatedStyle(() => ({
    //Animation properties...
  }));

  const shake = () => {
    //Update shared values...
  }

  // Loop every X seconds:
  const repeatAnimation = () => {
    shake();

    setTimeout(() => {
      repeatAnimation();
    }, repeatEvery);
  }

  // Start Animations on component Init:
  useEffect(() => {
    // Run animation continously:
    if(repeat){
      repeatAnimation();
    }
    // OR ~ call once:
    else{
      shake();
    }
  }, []);

  return (
    <Animated.View style={[animatedStyles]}>
      {children}
    </Animated.View>
  )
}

export default Shake;

Wrapper Component Usage:

import Shake from "../../util/animated-components/shake";

const Screen: FC = () => {
  return (
    <Shake repeat={true} repeatEvery={5000}>
      {/* Whatever needs to be animated!...e.g. */}
      <Text>Hello World!</Text>
    </Shake>
  )
}
0

From their docs: CAUTION Animated styles cannot be shared between views.

To work around this you can generate multiple useAnimatedStyle in top-level loop (number of iterations must be static, see React's Rules of Hooks for more information).

https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level