Enhancing React Native Animations with Reanimated: A Step-by-Step Guide

ajmal_hasan

Ajmal Hasan

Posted on September 27, 2024

Enhancing React Native Animations with Reanimated: A Step-by-Step Guide

Image descriptionReact Native Reanimated is a powerful library for building smooth and complex animations in React Native applications. With its ability to run animations on the UI thread, it provides more control over animation behavior and offers a more efficient rendering experience, making your app look seamless and responsive.

In this blog, we’ll walk through a sample React Native application using Reanimated. We'll explore how to work with shared values, animated styles, and several animation methods such as withSequence, withDelay, and withRepeat. We will also demonstrate how to use runOnUI and runOnJS for integrating UI thread and JavaScript thread operations.


Getting Started

Before diving into the code, ensure you have Reanimated installed in your React Native project. If you haven't, install it via the following command:

npm install react-native-reanimated
Enter fullscreen mode Exit fullscreen mode

Then enable the Reanimated Babel plugin by adding the following to your babel.config.js:

module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: ['react-native-reanimated/plugin'],
};
Enter fullscreen mode Exit fullscreen mode

Code Walkthrough

Now, let’s break down the code and explain key concepts of Reanimated.

import React from 'react';
import { View, Button, Alert, StyleSheet } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  withSpring,
  withSequence,
  withDelay,
  withRepeat,
  runOnJS,
  runOnUI,
} from 'react-native-reanimated';
Enter fullscreen mode Exit fullscreen mode

This code imports necessary dependencies from React Native and Reanimated. Reanimated offers hooks such as useSharedValue and useAnimatedStyle to define animated properties and styles.

Defining Shared Values

const opacity = useSharedValue(1);
const scale = useSharedValue(1);
const rotation = useSharedValue(0);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
Enter fullscreen mode Exit fullscreen mode
  • Shared values are central to Reanimated. They act as dynamic variables that hold the state of animations.
  • Here, we define shared values for opacity, scale, rotation, and translation along the X and Y axes.

Animated Styles

const animatedBoxStyle = useAnimatedStyle(() => {
  return {
    opacity: opacity.value,
    transform: [
      { scale: scale.value },
      { rotateZ: `${rotation.value}deg` },
    ],
  };
});
Enter fullscreen mode Exit fullscreen mode

The useAnimatedStyle hook lets you map shared values to component styles. Here, the opacity, scale, and rotation shared values are linked to the component’s style.

Sequential Animation with withSequence

const runSequence = () => {
  opacity.value = withSequence(
    withTiming(0, { duration: 500 }),
    withTiming(1, { duration: 500 })
  );
  scale.value = withSequence(
    withTiming(1.5, { duration: 500 }),
    withTiming(1, { duration: 500 })
  );
  rotation.value = withSequence(
    withTiming(180, { duration: 500 }),
    withTiming(0, { duration: 500 })
  );
};
Enter fullscreen mode Exit fullscreen mode

The withSequence method allows you to run multiple animations one after another. Here, we animate opacity, scale, and rotation sequentially. The box fades out, scales up, and rotates by 180 degrees, before resetting to its original state.

Delayed Animation with withDelay

const runDelayedAnimation = () => {
  scale.value = withDelay(1000, withSpring(1.5, { damping: 5 }));
};
Enter fullscreen mode Exit fullscreen mode

With withDelay, you can add a delay before an animation starts. In this case, the box scales up after a 1-second delay using a spring animation.

Repeating Animations with withRepeat

const runRepeatedAnimation = () => {
  rotation.value = withRepeat(
    withTiming(360, { duration: 1000 }),
    -1,
    true
  );
};
Enter fullscreen mode Exit fullscreen mode

The withRepeat method allows an animation to repeat for a specified number of times. Setting -1 means the animation will repeat indefinitely, and setting true reverses the direction after each iteration.

Running Animations on the UI Thread

const runOnUiThread = () => {
  runOnUI(() => {
    'worklet';
    translateX.value = withSpring(50);
    translateY.value = withSpring(-50);
    opacity.value = withTiming(1, { duration: 1000 });
  })();
};
Enter fullscreen mode Exit fullscreen mode

Reanimated allows you to offload animations to the UI thread for better performance using runOnUI. In this example, the box moves to a new position and fades in, ensuring smooth animations without delays from the JS thread.

Triggering JavaScript from the UI Thread

const triggerJsAlert = () => {
  opacity.value = withTiming(0, { duration: 500 }, (finished) => {
    if (finished) {
      runOnJS(showAlert)();
    }
  });
};

const showAlert = () => {
  Alert.alert('Animation Complete!', 'The opacity animation has finished.');
  runOnUiThread();
};
Enter fullscreen mode Exit fullscreen mode

Since UI thread worklets can't directly interact with JS, runOnJS is used to trigger JavaScript functions. After an animation completes, a JS alert is shown and then triggers another UI thread animation.


Complete Code:

import React from 'react';
import { View, Button, Alert, StyleSheet } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  withSpring,
  withSequence,
  withDelay,
  withRepeat,
  runOnJS,
  runOnUI,
} from 'react-native-reanimated';

export default function App() {
  // Shared values for animations
  const opacity = useSharedValue(1);    // Controls opacity
  const scale = useSharedValue(1);      // Controls scale
  const rotation = useSharedValue(0);   // Controls rotation in degrees
  const translateX = useSharedValue(0); // Controls horizontal translation
  const translateY = useSharedValue(0); // Controls vertical translation

  // ---- Animated Styles ----

  // Style for fading, scaling, and rotating the box
  const animatedBoxStyle = useAnimatedStyle(() => {
    return {
      opacity: opacity.value,
      transform: [
        { scale: scale.value },
        { rotateZ: `${rotation.value}deg` }, // Rotation in degrees
      ],
    };
  });

  // Style for dragging the box
  const dragStyle = useAnimatedStyle(() => {
    return {
      transform: [
        { translateX: translateX.value },
        { translateY: translateY.value },
      ],
    };
  });

  // ---- Animation Functions ----

  // 1. Sequence Animation (withSequence)
  // This function fades the box out, scales it up, and rotates it sequentially.
  const runSequence = () => {
    opacity.value = withSequence(
      withTiming(0, { duration: 500 }), // First fade out
      withTiming(1, { duration: 500 })  // Then fade in
    );
    scale.value = withSequence(
      withTiming(1.5, { duration: 500 }), // Scale up
      withTiming(1, { duration: 500 })    // Scale back to original
    );
    rotation.value = withSequence(
      withTiming(180, { duration: 500 }), // Rotate 180 degrees
      withTiming(0, { duration: 500 })    // Rotate back to original
    );
  };

  // 2. Delayed Animation (withDelay)
  // This function scales up the box after a delay of 2000ms.
  const runDelayedAnimation = () => {
    scale.value = withDelay(1000, withSpring(1.5, { damping: 5 })); // Delayed scale up
  };

  // 3. Repeated Animation (withRepeat)
  // This function continuously rotates the box between 0 and 360 degrees.
  const runRepeatedAnimation = () => {
    rotation.value = withRepeat(
      withTiming(360, { duration: 1000 }), // Rotate 360 degrees in 1 second
      -1, // Repeat forever (-1 means infinite)
      true // Reverse the direction on each iteration
    );
  };

  /**
   * 4. Running animations on the UI thread using a worklet
   *
   * Worklets run directly on the UI thread for optimal performance. They are useful for smooth animations
   * and interactions that should not be affected by the JS thread.
   *
   * In React Native, the UI thread is responsible for rendering and updating the visual elements, whereas the JavaScript thread handles the logic and data. Worklets avoid JS thread delays by running animations on the UI thread.
   * 
   * runOnUI() allows you to invoke a worklet from the JavaScript thread and run it on the UI thread.
   *
   * Use this for smooth gestures, animations, or any UI updates that need to happen independently of JavaScript logic.
   */
  const runOnUiThread = () => {
    runOnUI(() => {
      'worklet';  // This function is marked as a worklet, executed on the UI thread
      translateX.value = withSpring(50);  // Move the box 50px to the right
      translateY.value = withSpring(-50); // Move the box 50px upwards
      opacity.value = withTiming(1, { duration: 1000 }); // Fade in
    })();
  };

  /**
   * 5. Triggering a JavaScript function after an animation completes (runOnJS)
   *
   * Worklets cannot directly call JS functions since they run on the UI thread.
   * runOnJS() allows you to invoke a JS function from within a worklet after an animation or interaction finishes.
   *
   * Useful for triggering JS logic (e.g., showing alerts, navigating) after an animation or gesture.
   */
  const triggerJsAlert = () => {
    opacity.value = withTiming(0, { duration: 500 }, (finished) => {
      if (finished) {
        runOnJS(showAlert)(); // Call a JS function after animation finishes
      }
    });
  };

  // JS Alert function to be called after animation completes
  const showAlert = () => {
    Alert.alert('Animation Complete!', 'The opacity animation has finished.');
    // Optionally trigger another UI thread animation after the alert
    runOnUiThread();
  };

  return (
      <View style={styles.container}>
        {/* Animated box with fade, scale, rotation, and drag */}
        <Animated.View style={[styles.box, animatedBoxStyle, dragStyle]} />

        <View style={styles.buttonContainer}>
          <Button title="Run Sequence" onPress={runSequence} />
          <Button title="Run Delayed Scale" onPress={runDelayedAnimation} />
          <Button title="Run Repeated Rotation" onPress={runRepeatedAnimation} />
        </View>

        {/* Run UI thread worklet */}
        <Button title="Run on UI Thread (Move)" onPress={runOnUiThread} />

        {/* Run JS alert after animation */}
        <Button title="Trigger JS Alert after animation" onPress={triggerJsAlert} />
      </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    width: 100,
    height: 100,
    backgroundColor: 'tomato',
    marginVertical: 20,
  },
  buttonContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    width: '90%',
    marginVertical: 10,
  },
});

Enter fullscreen mode Exit fullscreen mode

Conclusion

With React Native Reanimated, creating smooth and complex animations has become simpler and more efficient. By leveraging shared values, animated styles, and running animations on the UI thread, you can significantly improve your app's performance and user experience.

💖 💪 🙅 🚩
ajmal_hasan
Ajmal Hasan

Posted on September 27, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related