react-nativereact-hooksreact-native-reanimated-v2

Reanimated 2 reusable animation in custom hook


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,
    ]}
  />
);

Solution

  • 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>
      )
    }