Using React Native Reanimated for seamless UI transitions

Using React Native Reanimated To Create Seamless Ui Transitions

When using an application, the smooth movement of objects, pages, modals, and other components improves our UX and encourages users to return. No one wants to use an app that glitches and does not move properly.

Creating animations and object transitions might be a burden for frontend developers since we typically want to focus on writing code for our application and not bother calculating where to place an object or where to move that object when a user hits an event on our application.

Working with UI designers can also be a challenge, especially when expectations are misaligned — for example, when designers expect to see their complex animations recreated as-is. Finding a good tool and package to solve this issue is not so easy, either — but that is exactly why the react-native-reanimated package was built.

The react-native-reanimated npm package allows us to easily create both simple and complex, yet smooth and interactive animations. It might not be intuitive to use at first, but as we practice with the examples in this article, we should get a good idea of how to use the package.

Before we continue, I’m assuming you already have a knowledge of React Native and how it works. Also, React Native Reanimated v3 only supports React Native v0.64 or newer, so ensure you update or download the latest version of React Native to work with this tutorial.

Exploring React Native Reanimated v2

If you have ever used React Native Reanimated v1, keep in mind that v2 introduced some new concepts and breaking changes. Let’s explore some of these changes now.

Name changes

The breaking changes in Reanimated 2 include some name changes to keep in mind. For example, the interpolate method is now interpolateNode, while the Easing method is now EasingNode.

Worklets

In React Native Reanimated v2, animations are first-class citizens. Animations are written in pure JS in the form of worklets.

Worklets are pieces of JavaScript code that are picked from the main React Native code by the Reanimated Babel plugin. These pieces of code run in a separate thread using a separate JavaScript virtual machine context and are executed synchronously on the UI thread.

To create a worklet, you will explicitly add the worklet directive on top of your function. See the code below:

function someWorklet(greeting) {
  'worklet';
  console.log("Hey I'm running on the UI thread");
}

All the functions with the worklet directive are picked and run in a separate JavaScript thread. You can also run your function in the UI thread by calling or executing the function using runOnUI like so:

function FirstWorklet(greeting) {
  'worklet';
  console.log("Hey I'm running on the UI thread");
}

function secondFunction() {
  console.log("Hello World!");
}

function onPress() {
  runOnUI(secondFunction)();
}

In the code above, FirstWorklet is the only worklet function, so it will be picked by the Reanimated Babel plugin and run in a separate thread. Meanwhile, the other functions will run synchronously in the main JavaScript thread.

Shared Values

In Reanimated, Shared Values are primitive values that are written on the JavaScript side but are used to drive animations on the UI thread.

As we learned while exploring worklets, Reanimated runs our animation in a separate thread using a separate JavaScript virtual machine context. When we create a value in our main thread or in our JavaScript, Shared Values maintains a reference to that mutable value.

Since it is mutable, we can make changes to the Shared Value and see the referenced changes in the UI thread, along with a change in our animation.

See the code below:

import React from 'react';
import {Button, View} from 'react-native';
import {useSharedValue} from 'react-native-reanimated';

function App() {
  const sharedValue = useSharedValue(0);

  return (
    <View>
      <Button title="Increase Shared Value" onPress={() => (sharedValue.value += 1)} />
    </View>
  );
}

As we can see in our code above, Shared Value objects serve as references to pieces of shared data that can be accessed using their .value property. Therefore, to access and modify our shared data, use the .value property as seen above.

We use the useSharedValue hooks to create and store data in our Shared Value references.

The useAnimatedStyle Hook

The useAnimatedStyle Hook allows us to create an association between Shared Values and View properties to animate our View stylings using the Shared Values.

Let’s look at the code below:

import { useAnimatedStyle } from 'react-native-reanimated';

const Animation = useAnimatedStyle(() => {
    return {
     ...style animation code
    };
  });

The useAnimatedStyle Hook returns our updated animation styling, gets the Shared Value, and then executes the updates and the styles.

Just like the useEffect Hook, the useAnimatedStyle Hook accepts a dependency. When the dependency is omitted as we did above, the update is called only when there’s a change in the body.

If the dependency is included, however, the useAnimatedStyle Hook runs the update whenever there’s a change in the dependency value, like so:

import { useAnimatedStyle } from 'react-native-reanimated';

const Animation = useAnimatedStyle(() => {
    return {
     ...style animation code
    };
  }, [ dependency] );

Using new React Native Reanimated concepts

Now that we have explored some of the new concepts introduced in React Native Reanimated v2, we will now use them to create animations in our application with these new concepts.

To use the React Native Reanimated library, we will have to install the library first. Run either of the commands below to install the package:

// yarn
yarn add react-native-reanimated

// npm
npm i react-native-reanimated --save

Next, go into your babel.config.js file and add the plugin as shown below:

module.exports = {
    presets: [
      ...
    ],
    plugins: [
      ...
      'react-native-reanimated/plugin',
    ],
  };

Note that the React Native Reanimated plugin has to be added last.

Next, in your App.js file, let’s create a simple page with some header and body content:

import React from 'react';
import {View, Button, Text, StyleSheet} from 'react-native';
const App = () => {
  return (
    <View style={styles.parent}>
      <Text style={styles.header}>React Native Reanimated Tutorial</Text>
      <View style={styles.box}>
        <Button title="View more" />
        <View style={{marginTop: 20}}>
          <Text style={styles.textBody}>
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Pariatur
            magnam necessitatibus dolores qui sunt? Mollitia nostrum placeat
            esse commodi modi quaerat, et alias minima, eligendi ipsa
            perspiciatis, totam quod dolorum.
            {'\n'}
            {'\n'}
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Pariatur
            magnam necessitatibus dolores qui sunt? Mollitia nostrum placeat
            esse commodi modi quaerat, et alias minima, eligendi ipsa
            perspiciatis, totam quod dolorum.
          </Text>
        </View>
      </View>
    </View>
  );
};
export default App;
const styles = StyleSheet.create({
  parent: {
    flex: 1,
    paddingTop: 40,
    paddingHorizontal: 20,
  },
  header: {
    fontSize: 24,
    marginBottom: 20,
    textAlign: 'center',
  },
  box: {
    backgroundColor: '#000',
    borderRadius: 15,
    padding: 20,
  },
  textBody: {
    fontSize: 20,
    marginBottom: 20,
  },
});

The goal is to create animated dropdown text that will display when we click a button:

React Native App In Dark Mode Displaying A Widget With Two Paragraphs Of Dummy Text Underneath A Blue Button That Says View More

Let’s achieve our desired animation with the useAnimatedStyle Hook and Shared Values, as shown below:

import React, {useState} from 'react';
import {View, Button, Text, StyleSheet} from 'react-native';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';
const App = () => {
  const boxHeight = useSharedValue(60);
  const [maxLines, setMaxLines] = useState(2);

  const truncatedAnimation = useAnimatedStyle(() => {
    return {
      height: withTiming(boxHeight.value, {duration: 1000}),
    };
  }, []);

  function showText() {
    setTimeout(() => {
      maxLines === 2 ? setMaxLines(0) : setMaxLines(2);
    }, 400);
    boxHeight.value === 60 ? (boxHeight.value = 150) : (boxHeight.value = 60);
  }

  return (
    <View style={styles.parent}>
      <Text style={styles.header}>React Native Reanimated Tutorial</Text>
      <View style={styles.box}>
        <Button
          title={maxLines === 2 ? 'View more' : 'Close Dropdown'}
          onPress={showText}
        />
        <Animated.View style={[{marginTop: 20}, truncatedAnimation]}>
          <Text style={styles.textBody} numberOfLines={maxLines}>
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Pariatur
            magnam necessitatibus dolores qui sunt? Mollitia nostrum placeat
            esse commodi modi quaerat, et alias minima, eligendi ipsa
            perspiciatis, totam quod dolorum.
            {'\n'}
            {'\n'}
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Pariatur
            magnam necessitatibus dolores qui sunt? Mollitia nostrum placeat
            esse commodi modi quaerat, et alias minima, eligendi ipsa
            perspiciatis, totam quod dolorum.
          </Text>
        </Animated.View>
      </View>
    </View>
  );
};
export default App;
const styles = StyleSheet.create({
  parent: {
    flex: 1,
    paddingTop: 40,
    paddingHorizontal: 20,
  },
  header: {
    fontSize: 24,
    marginBottom: 20,
    textAlign: 'center',
  },
  box: {
    backgroundColor: '#000',
    borderRadius: 15,
    padding: 20,
  },
  textBody: {
    fontSize: 20,
    marginBottom: 20,
  },
});

In our code above, we first imported our react-native-reanimated package. Next, we used the Animated options for our View — which we imported from react-native-reanimated — to wrap the view that we want to animate.

After that, we set our box’s initial height to 60 using useSharedValue. We also have a maxLines state that we’re setting to 2. The maxLines state determines the number of lines to which we are truncating our text, which in this case is a maximum of 2 lines.

The showText function checks if the height is 60 when the button is clicked, and if so, it increases the height to 150. If the height is already 150, it will decrease to 60 when the button is clicked instead.

We are also setting the maxLines state to 0, meaning we want the full text to be shown when we click on our “View more” button.

With the useAnimatedStyle Hook, we created our animation association between our shared value and the View that we’re animating.

You can see the result below:

Animation Created With React Native Reanimated To Expand Text Widget On Press Of Blue Button That Says View More. When Expanded, Button Changes To Read Close Dropdown, Which Closes Widget On Press To Show Two Lines Of Text Before Cutting Off

Introducing a new feature in React Native Reanimated v3

React Native Reanimated v3 does not introduce any breaking changes, as was the case in v2. Hence, every code written in v2 will work fine in v3. However, an additional feature was introduced, which we will look at extensively. This feature is called Shared Element Transitions.

Understanding Shared Element Transitions

This new feature introduced in v3 is a transition feature that allows you to animate views between navigation screens. sharedTransitionTag is the attribute that allows us to animate our screens between navigations.

To create a shared transition animation between screens, simply assign the same sharedTransitionTag to both components. When you navigate between screens, the shared transition animation will automatically play.

To see how it works, we will be animating an image screen.

First, create four files — ImageDescription.js, Home.js, SharedElements.js, and our root file, App.js. Copy and paste the relevant code for each file as shown below.

In your App.js file:

import * as React from 'react';
import {NavigationContainer} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import Home from './src/Home';
import ImageDescription from './src/ImageDescription';
const Stack = createNativeStackNavigator();
function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen
          name="Home"
          component={Home}
          options={{
            title: 'Posts',
            headerTintColor: '#000',
            headerTitleAlign: 'center',
            headerTitleStyle: {
              fontWeight: 'bold',
            },
          }}
        />
        <Stack.Screen
          name="ImageDescription"
          component={ImageDescription}
          options={{
            title: 'Image',
            headerTintColor: '#000',
            headerTitleAlign: 'center',
            headerTitleStyle: {
              fontWeight: 'bold',
            },
          }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}
export default App;

The App.js file houses our navigation components. We have two screens — the Home and ImageDescription screens. We are also customizing our navigation header here to our preferred stylings.

Next, in your Home.js file:

import React from 'react';
import {
  Image,
  StyleSheet,
  Dimensions,
  SafeAreaView,
  TouchableOpacity,
} from 'react-native';
import Animated from 'react-native-reanimated';
import {sharedElementTransition} from './SharedElements';
import Img1 from '../assets/image1.jpg';
import Img2 from '../assets/image2.jpg';
import Img3 from '../assets/image3.jpg';
import Img4 from '../assets/image4.jpg';
const imageArray = [
  {
    id: 1,
    img: Img1,
    description: `Image 1. To create a shared transition animation between two components on different screens, simply assign the same sharedTransitionTag to both components. When you navigate between screens, the shared transition animation will automatically play. The shared transition feature works by searching for two components that have been registered with the same sharedTransitionTag. If you want to use more than one shared view on the same screen, be sure to assign a unique shared tag to each component.`,
  },
  {
    id: 2,
    img: Img2,
    description: `Image 2. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
  },
  {
    id: 3,
    img: Img3,
    description: `Image 3. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
  },
  {
    id: 4,
    img: Img4,
    description: `Image 4. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
  },
  {
    id: 5,
    img: Img3,
    description: `Image 5. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
  },
  {
    id: 8,
    img: Img2,
    description: `Image 8. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
  },
  {
    id: 7,
    img: Img1,
    description: `Image 7. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
  },
  {
    id: 6,
    img: Img4,
    description: `Image 6. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
  },
];
export default function Home({navigation}) {
  return (
    <SafeAreaView style={styles.container}>
      {imageArray.map(image => (
        <TouchableOpacity
          key={image.id}
          onPress={() =>
            navigation.navigate('ImageDescription', {
              image: image,
            })
          }>
          <Animated.View
            style={styles.imageContainer}
            sharedTransitionTag="animateImageTag"
            sharedTransitionStyle={sharedElementTransition}>
            <Image source={image.img} style={styles.image} />
          </Animated.View>
        </TouchableOpacity>
      ))}
    </SafeAreaView>
  );
}
const windowWidth = Dimensions.get('window').width;
const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexWrap: 'wrap',
    flexDirection: 'row',
    alignItems: 'center',
  },
  imageContainer: {
    height: 140,
    width: windowWidth / 3,
  },
  image: {
    flex: 1,
    resizeMode: 'cover',
    width: '100%',
  },
});

In the Home component, we have some images that we are mapping and displaying on our screen, like so:

React Native App Displaying Image Gallery With Seven Images In Three Columns

When an image is clicked, it navigates the user to another screen — the ImageDescription component — that shows the image clicked, a header title, and a description underneath the image, like so:

React Native App With One Image From Gallery Expanded To Show Image Description Component

To set this component up, copy the below into your ImageDescription.js file:

import React from 'react';
import Animated from 'react-native-reanimated';
import {sharedElementTransition} from './SharedElements';
import {View, Text, StyleSheet, Image, Dimensions} from 'react-native';
export default function ImageDescription({route}) {
  const {image} = route.params;
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Shared Elements Transition</Text>
      <Animated.View
        style={styles.imageContainer}
        sharedTransitionTag="animateImageTag"
        sharedTransitionStyle={sharedElementTransition}>
        <Image source={image.img} style={styles.image} />
      </Animated.View>
      <Text style={styles.description}>{image.description}</Text>
    </View>
  );
}
const windowWidth = Dimensions.get('window').width;
const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#000',
    marginHorizontal: 10,
    marginVertical: 5,
  },
  imageContainer: {
    height: 500,
    width: windowWidth,
  },
  image: {
    height: '100%',
    width: '100%',
  },
  description: {
    fontSize: 20,
    color: '#000',
    marginHorizontal: 10,
    marginVertical: 5,
  },
});

Next, we’ll introduce our Animated View from react-native-reanimated. The Animated View is wrapped around the section you want to animate and in our case, the images. Animated.View takes some options like the sharedTransitionTag and the sharedTransitionStyle.

Add the below to your SharedElements.js file:

import {SharedTransition, withSpring} from 'react-native-reanimated';
const CONFIG = {
  mass: 1,
  stiffness: 100,
  damping: 200,
};

export const sharedElementTransition = SharedTransition.custom(values => {
  'worklet';
  return {
    height: withSpring(values.currentHeight, CONFIG),
    width: withSpring(values.currentWidth, CONFIG),
    originX: withSpring(values.currentOriginX, CONFIG),
    originY: withSpring(values.currentOriginY, CONFIG),
  };
});

In the code above, the sharedTransitionTag lets Reanimated detect the components to animate. So, components with the same sharedTransitionTag share the animation between them.

Meanwhile, sharedTransitionStyle allows you to write custom stylings for the height, width, originY, and originX of the animated components.

The complete app will look like this:

Animation Created With React Native Reanimated To Transition Between Image Gallery And Image Description Component Upon Tapping Image

Conclusion

We have learned about changes introduced in React Native Reanimated v2 and v3, the core features, and how to use these core features. We have also seen how to create smooth transitions and animations using the concepts we have learned in the article.

The Share Elements Transition feature is still experimental, which means that a more stable version is being worked on. In the meantime, you can use it and see how it works, although you may not want to use it in production yet.

No comments:

Post a Comment