Enhancing React Native Animations with Reanimated: A Step-by-Step Guide
Ajmal Hasan
Posted on September 27, 2024
React 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
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'],
};
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';
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);
- 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 theX
andY
axes.
Animated Styles
const animatedBoxStyle = useAnimatedStyle(() => {
return {
opacity: opacity.value,
transform: [
{ scale: scale.value },
{ rotateZ: `${rotation.value}deg` },
],
};
});
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 })
);
};
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 }));
};
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
);
};
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 });
})();
};
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();
};
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,
},
});
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.
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
November 28, 2024
November 18, 2024