Adding animations to your React Native app - Part 2: Transition animations
Tags: reactnative
Published: 2019-07-13 07:36:01

---
title: Adding animations to your React Native app - Part 2: Transition animations
published: true
description: In this tutorial you'll learn how to implement transition animations in React Native
tags: [react-native]
series: Adding animations to your React Native app
canonical_url: https://blog.pusher.com/animation-react-native-part-2/
---

Welcome to part two of a three-part series on adding animations to your React Native app. In this part, you’re going to learn how to add transition animations. Specifically, we’re going to:

- Animate the header when the user scrolls on a list.
- Customize the page transition animation that’s set by React Navigation.
- Use LayoutAnimation to animate the components that are affected by a state change.

## Prerequisites

To follow this tutorial, you need to know the basics of React and React Native.

Going through part one of this series is helpful but not required. Though this tutorial assumes that you know how to implement basic animations on React Native. You should already know how to implement scale, rotation, and sequence animations. We will be applying those same concepts when implementing transition animations.

We’ll also be using version two of [React Navigation](https://reactnavigation.org/). Knowledge of the Stack Navigator is helpful but not required.

## What you’ll be building

Here’s what you’ll be building:

![React Native Animations Part 2 Final Output (image credit: pokeapi.co, pokemondb.net, and pixabay.com)](https://d2mxuefqeaa7sj.cloudfront.net/s_C1809527E2B34D744AF2137B86B5994B06DACA31E57B532677A9809007467FAE_1529898864181_rn-animations2-final.gif)

You can find the full source code for this tutorial on its [GitHub repo](https://github.com/anchetaWern/RNRealworldAnimations). We will be making changes to the final source code from the first part. If at any time you feel confused as to what specific changes you need to make, be sure to check the commit history of the branch containing the final source code for this part of the series (`part2`).

## Setting up the project

To follow along, you first need to clone the repo:

```
git clone https://github.com/anchetaWern/RNRealworldAnimations.git
```

After that, switch to the `part1` branch and install the dependencies:

```
cd RNRealworldAnimations
git checkout part1
npm install
```

Next, setup the `android` and `ios` folders:

```
react-native upgrade
```

Link the native dependencies. In this case, it’s only the [React Native Vector Icons](https://github.com/oblador/react-native-vector-icons):

```
react-native link
```

Once that’s done, you should be able to run the app on your device or emulator:

```
react-native run-android
react-native run-ios
```

The `part1` branch contains the final output of the first part of this series. We will add new animations on top of the ones we’ve already implemented on the first part. The final source code for this part is available at the `part2` branch.

## Header scroll animation

First, let’s look at how we can implement header animations while the user is scrolling through a list:

![Header animation on scroll](https://d2mxuefqeaa7sj.cloudfront.net/s_C1809527E2B34D744AF2137B86B5994B06DACA31E57B532677A9809007467FAE_1529482087843_header-animation.gif)

To implement the animation, we need to make the following changes:

1.  Update the `App.js` file to add an animated value that will be interpolated to animate the header. This animated value will correlate directly to the current scroll position. Thus allowing us to perform different kinds of animations depending on the current scroll position.
2.  Create an `AnimatedHeader` component which implements the animations.

### Refactor the code

Before we proceed, let’s first refactor the code. In the `App.js` file, move the `getRandomInt` function to a separate file (`src/lib/random.js`):

```javascript
// src/lib/random.js
const getRandomInt = (max, min) => {
  return Math.floor(Math.random() * (max - min) + min);
};

export { getRandomInt };
```

Then import it into the `App.js` file:

```javascript
import { getRandomInt } from "./src/lib/random";
```

### Add the animated value

Now we can start with the implementation of the animated header.

Create a `src/settings/layout.js` file. This is where we keep the layout settings for the animated header:

- `HEADER_MAX_HEIGHT` - the maximum height of the animated header. This will be the header’s default height when the user still hasn’t scrolled through the list.
- `HEADER_MIN_HEIGHT` - the minimum height of the header. This will slightly differ based on the platform the app is currently running on because the height from the top of the screen down to the actual real estate where the app is rendered will be different between the two platforms.
- `HEADER_SCROLL_DISTANCE` - the height that the scroll needs to be scrolled in order for the `HEADER_MIN_HEIGHT` to be applied.

```javascript
// src/settings/layout.js
import { Platform } from "react-native";
const HEADER_MAX_HEIGHT = 250;
const HEADER_MIN_HEIGHT = Platform.OS === "ios" ? 40 : 53;
const HEADER_SCROLL_DISTANCE = HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT;

export { HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT, HEADER_SCROLL_DISTANCE };
```

Next, open the `App.js` file and import the modules that we need:

```javascript
import { View, ScrollView, Platform, Animated } from "react-native";
import { HEADER_MAX_HEIGHT } from "./src/settings/layout";
```

In the `constructor`, initialize the animated value that we will be using. By default, this should have a value of `0` because we haven’t really scrolled yet when the page loads. But iOS is different because of the [content inset](https://developer.apple.com/documentation/uikit/uiscrollview/1619406-contentinset). We have to use the negative equivalent of the maximum height of the animated header (`-HEADER_MAX_HEIGHT`) as the initial value. If we just specified `0`, the output will look like this:

![iOS animated header issue](https://d2mxuefqeaa7sj.cloudfront.net/s_C1809527E2B34D744AF2137B86B5994B06DACA31E57B532677A9809007467FAE_1529896096485_ios-animated-header-default.gif)

Here’s the code:

```javascript
// App.js
constructor(props) {
  super(props);
  this.pokemon_stats = [];

  // add this:
  this.nativeScrollY = new Animated.Value(
    Platform.OS === "ios" ? -HEADER_MAX_HEIGHT : 0
  );
}
// next: add code for render method
```

Next, inside the `render` method, we need to bring back the scroll value to `0`. We can do that by using the `Animated.add` method. This allows us to add a new value to an existing animated value, in this case, it’s `this.nativeScrollY`. The second argument is the value you want to add.

Earlier, we’ve set the default animated value for iOS to the negative equivalent of the header’s maximum height (`-HEADER_MAX_HEIGHT`). So we need to add the equivalent positive value to bring it back to `0`. I know it’s a bit hard to wrap your head around the idea, so consider it as a little hack to deal with the contentInset for the scrollbars in iOS. If you don’t apply it, you’ll get an issue similar to the demo earlier. Here’s the code:

```javascript
// App.js
render() {
  let nativeScrollY = Animated.add(
    this.nativeScrollY,
    Platform.OS === "ios" ? HEADER_MAX_HEIGHT : 0
  );

  // next: add code for rendering the CardList
}
```

Next, pass an `onScroll` prop to the `CardList` component. The value for this prop will be passed directly to the `onScroll` prop of the `ScrollView` component in the `src/components/CardList.js` file later on. What this does is bind the animated value to the `ScrollView`'s current scroll position. We do that by using `Animated.event` and passing in the mapping to the native event. In this case, the `nativeEvent.contentOffset.y` is mapped to `this.nativeScrollY`:

```javascript
// App.js
{this.nativeScrollY && (
  <CardList
    ...previously added props here...
    onScroll={Animated.event(
      [{ nativeEvent: { contentOffset: { y: this.nativeScrollY } } }],
      {
        useNativeDriver: true
      }
    )}
  />
)}
```

Next, open the `src/components/CardList.js` file and import the modules that we’ll need:

```javascript
import { View, FlatList, Animated, Platform } from "react-native";
import { HEADER_MAX_HEIGHT } from "../settings/layout";
```

Extract the the `onScroll` prop that we passed earlier:

```javascript
// src/components/CardList.js
const CardList = ({
  // ..previously extracted props
  onScroll // add this
}) => {
  // next: update render code
};
```

Next, replace `ScrollView` with `Animated.ScrollView` and supply the `onScroll` prop. We also need to supply a few other props:

- `scrollEventThrottle` - this controls how often the scroll event will be fired while scrolling. Information regarding the scroll is sent over the bridge so this needs to be kept as low as possible so the app’s performance will not be impacted. The lower value means that it will be sent less often.
- `contentInset` and `contentOffset` - the offset applied for the animated header. These settings only apply to iOS. This allows us to specify the height the header is going to be. That way, the header won’t overlap the list below it. If these two aren’t applied, here’s how it will look like:

![iOS content inset and offset issue](https://d2mxuefqeaa7sj.cloudfront.net/s_C1809527E2B34D744AF2137B86B5994B06DACA31E57B532677A9809007467FAE_1529898273551_ios-content-inset-offset-issue.gif)

See how it jumped to its final height the moment it was scrolled?

Here’s the code:

```javascript
// src/components/CardList.js
return (
  <Animated.ScrollView
    style={styles.scroll}
    scrollEventThrottle={1}
    onScroll={onScroll}
    contentInset={{
      top: HEADER_MAX_HEIGHT
    }}
    contentOffset={{
      y: -HEADER_MAX_HEIGHT
    }}
  >
    ..previously added scrollview contents
  </Animated.ScrollView>
);
```

Lastly, add the styles. The important thing to remember here is that a `flex` property should be applied to the animated ScrollView, and the `paddingTop` applied to Android. This is how we deal with positioning the CardList right below the header so they don’t overlap on Android. For iOS, it’s already been taken care with the content inset so we simply set the `paddingTop` to `0`:

```javascript
// src/components/CardList.js
const styles = {
  scroll: {
    flex: 1
  },
  scroll_container: {
    alignItems: "center",
    paddingTop: Platform.OS == "android" ? HEADER_MAX_HEIGHT : 0
  }
};
```

### Animate the header

Now we’re ready to add the code for animating the header. First import the modules that we need:

```javascript
// src/components/AnimatedHeader.js
import React from "react";
import { View, Text, Animated, Platform } from "react-native";
import { HEADER_MAX_HEIGHT, HEADER_SCROLL_DISTANCE } from "../settings/layout";
```

Here’s the component:

```javascript
const AnimatedHeader = ({ title, nativeScrollY }) => {
  if (nativeScrollY) {
    // next: add the animation code
  }
};
```

At this point, we can now animate based on the current value of `nativeScrollY`. This is the current scroll position that we will pass later on from the `App.js` file. As you have seen in the demo earlier, we’re actually animating multiple components at the same time:

- **Header** - animate the Y position.
- **Background image** - animate the Y position and the opacity.
- **Title** - animate the scale and the Y position.

From the breakdown above, you can see that we need to animate all the components involved. There’s no such thing as inheritance when it comes to animation. Animating the header’s container won’t actually animate its children the same way. So you have to apply the individual animations that you want to use for each component.

Here’s the code for animating the header’s Y position. From the code below, you can see that we’re relying on the `HEADER_SCROLL_DISTANCE` for the input and output ranges. This value is the maximum height the scrollbar needs to be scrolled in order for the header to animate to its final position. So we set it as the final value for the `inputRange`, while the final value for the `outputRange` will just be its inverse. Why? Because we’re animating the Y position, applying a negative value means the component will be moved to the top:

```javascript
// src/components/AnimatedHeader.js
const headerTranslate = nativeScrollY.interpolate({
  inputRange: [0, HEADER_SCROLL_DISTANCE],
  outputRange: [0, -HEADER_SCROLL_DISTANCE],
  extrapolate: "clamp" // so it wont go over the output range
});
```

Here’s the code for animating the background image:

```javascript
// src/components/AnimatedHeader.js
// for animating the opacity
const BGImageOpacity = nativeScrollY.interpolate({
  inputRange: [0, HEADER_SCROLL_DISTANCE / 2, HEADER_SCROLL_DISTANCE],
  outputRange: [1, 0.3, 0],
  extrapolate: "clamp"
});

// for animating the Y position
const BGImageTranslate = nativeScrollY.interpolate({
  inputRange: [0, HEADER_SCROLL_DISTANCE],
  outputRange: [0, 100],
  extrapolate: "clamp"
});
```

Here’s the code for animating the title:

```javascript
// for animating the scale
const titleScale = nativeScrollY.interpolate({
  inputRange: [0, HEADER_SCROLL_DISTANCE / 2, HEADER_SCROLL_DISTANCE],
  outputRange: [1, 0.8, 0.7],
  extrapolate: "clamp"
});

// for animating the Y position
const titleTranslateY = nativeScrollY.interpolate({
  inputRange: [0, HEADER_SCROLL_DISTANCE / 2, HEADER_SCROLL_DISTANCE],
  outputRange: [25, 35, 15],
  extrapolate: "clamp"
});
```

And here are the animated styles:

```javascript
const headerStyles = { transform: [{ translateY: headerTranslate }] };

const headerBarStyles = {
  transform: [{ scale: titleScale }, { translateY: titleTranslateY }]
};

const BGImageStyles = {
  opacity: BGImageOpacity,
  transform: [{ translateY: BGImageTranslate }]
};
```

Next, apply the animated styles to each of the components we want to target:

```javascript
return (
  <View style={styles.header_container}>
    <Animated.View pointerEvents="none" style={[styles.header, headerStyles]}>
      <Animated.Image
        style={[styles.header_bg, BGImageStyles]}
        resizeMode={"cover"}
        source={require("../img/team-instinct.jpg")}
      />
    </Animated.View>

    <Animated.View style={[styles.header_bar, headerBarStyles]}>
      <Text style={styles.header_text}>{title}</Text>
    </Animated.View>
  </View>
);
// next: add code for rendering default component if nativeScrollY isn't present
```

Next, we need to render a default component while `nativeScrollY` isn’t available:

```javascript
if (nativeScrollY) {
  // ...previously added code here
}

// add this:
return (
  <View style={styles.header}>
    <View>
      <Text style={styles.header_text}>{title}</Text>
    </View>
  </View>
);
```

Next, add the styles:

```javascript
const styles = {
  header_container: {
    ...Platform.select({
      ios: {
        zIndex: 1 // only applied to iOS, for some reason the cards is laid on top of the header when scrolling
      }
    })
  },
  header: {
    position: "absolute",
    top: 0, // so it's at the very top of its container
    left: 0, // for 100% width
    right: 0, // for 100% width
    backgroundColor: "#B4A608",
    overflow: "hidden", // for containing the background image because this container is absolutely positioned
    height: HEADER_MAX_HEIGHT, // needed for absolutely positioned elements
    zIndex: 1 // so the header will be laid on top of the list
  },
  header_bar: {
    backgroundColor: "transparent",
    height: 32,
    alignItems: "center",
    justifyContent: "center",
    position: "absolute",
    top: 0,
    left: 0,
    right: 0,
    zIndex: 1
  },
  header_bg: {
    position: "absolute",
    top: 0,
    left: 0,
    right: 0,
    width: null, // important so we can apply resizeMode=cover
    height: HEADER_MAX_HEIGHT
  },
  header_text: {
    color: "#FFF",
    fontSize: 25,
    fontWeight: "bold"
  }
};

export default AnimatedHeader;
```

Lastly, update `App.js` file to use the `AnimatedHeader` component instead of the `Header` component. Don’t forget to pass the `nativeScrollY` prop:

```javascript
// App.js
import AnimatedHeader from "./src/components/AnimatedHeader";
export default class App extends Component<Props> {
  render() {
    return (
      <View style={styles.container}>
        <AnimatedHeader title={"Poke-Gallery"} nativeScrollY={nativeScrollY} />
        ..previously added code here
      </View>
    );
  }
}
```

Once that’s done, you should be able to scroll the list and the header will be animated according to its current position.

## Page transition animations

The next animation we’re going to implement is page transition animation, and it looks like this:

![Custom page transition animation: spring animation](https://d2mxuefqeaa7sj.cloudfront.net/s_C1809527E2B34D744AF2137B86B5994B06DACA31E57B532677A9809007467FAE_1529637787360_custom-transition-animation.gif)

In case you didn’t notice, we’re performing a bit of a bouncing animation and an opacity animation as we navigate to the next page. We’re also doing the same as we navigate back to the previous page, but it becomes subtle because of we’re also animating the opacity back to zero.

For this animation, we’re going to use the [React Navigation](https://reactnavigation.org/) library. This library is like the de-facto standard for implementing navigation in React applications. So we’re going to take advantage of it instead of implementing our own navigation. This library already comes with default animations, but we’re going to customize it instead.

To implement custom page transition animations, we need to take care of the following first:

1.  Install the dependencies.
2.  Set up the main screen of the app.
3.  Create a new “Share” screen. This is where we’ll be navigating to so we can apply some animations.
4.  Set up the navigator for navigating between the Main and Share screen.
5.  Create a custom transition that will be applied when navigating between the two screens.

We will be moving around some code in this section. If at any time you feel unsure on what needs to be changed, you can check out the commits on the GitHub repo. I tried to be descriptive with the commit messages as much as possible, so you should be able to find the exact changes.

**Install the dependencies**

We will be needing the React Navigation library to implement navigation within the app. You can install it with the following command. It should already be installed if you’ve switched to the `part1` branch and installed the dependencies:

```
npm install --save react-navigation
```

### Set up the main screen

First, create a `screens` folder inside the `src` directory. This is where we’ll be putting all the screens used in the app.

Next, create a `Main.js` file and `Share.js` file inside the `screens` folder you just created.

Open the `App.js` file and copy all its contents over to the `Main.js` file. This will now serve as the default screen of the app.

In the `Main.js` file, update the paths to the files that we’re importing. Simply do a multi-search and replace on your text editor, seach for `./src/` and replace it with `../` and that should do the job for you. You don’t even have to mind the code below once you’ve done that:

```javascript
// src/screens/Main.js
import pokemon from "../data/pokemon";
import pokemon_stats from "../data/pokemon-stats";

import AnimatedHeader from "../components/AnimatedHeader";
import CardList from "../components/CardList";
import AnimatedModal from "../components/AnimatedModal";

Next, rename the component from `App` to `Main`:

// src/screens/Main.js
export default class Main extends Component<Props> {
  // next: add navigation options
}
```

Next, add the navigation options to be used by React Navigation:

```javascript
static navigationOptions = ({ navigation }) => {
  return {
    headerTitle: "", // empty because we're using the label inside the AnimatedHeader
    headerStyle: {
      elevation: 0, // only applied to Android to remove the shadow in the header
      shadowOpacity: 0, // for removing the shadow in the header
      backgroundColor: "#B4A608"
    },
    headerTitleStyle: {
      color: "#FFF"
    }
  };
};
```

As for the `App.js` file, remove all the code from it for now.

### Create a Share Screen

Next, let’s create the Share screen. I’ll just breeze through the code explanation for this screen because we’re only creating it so we could navigate to another screen. We won’t really be adding any animation code to this screen.

Here’s the code:

```javascript
// src/screens/Share.js
import React, { Component } from "react";
import { View } from "react-native";

import IconLabel from "../components/IconLabel";

type Props = {};
export default class Share extends Component<Props> {
  static navigationOptions = ({ navigation }) => {
    return {
      headerTitle: "Share",
      headerStyle: {
        backgroundColor: "#B4A608"
      },
      headerTitleStyle: {
        color: "#FFF"
      }
    };
  };

  render() {
    return (
      <View style={styles.container}>
        <IconLabel
          icon="facebook-f"
          label="Share to Facebook"
          bgColor="#4267b2"
        />

        <IconLabel
          icon="google-plus"
          label="Share to Google+"
          bgColor="#db4437"
        />

        <IconLabel icon="twitter" label="Share to Twitter" bgColor="#1B95E0" />

        <IconLabel
          icon="linkedin"
          label="Share to LinkedIn"
          bgColor="#0077B5"
        />
      </View>
    );
  }
}

const styles = {
  container: {
    flex: 1,
    padding: 20
  }
};
```

In the code above, we’re using an `IconLabel` component to render a button with an icon and label on it.

Here’s the code for the `IconLabel` component:

```javascript
// src/components/IconLabel.js
import React from "react";
import { Text, TouchableOpacity } from "react-native";
import Icon from "react-native-vector-icons/FontAwesome";

const IconLabel = ({ icon, label, bgColor }) => {
  let backgroundColor = { backgroundColor: bgColor };
  return (
    <TouchableOpacity
      onPress={this.share}
      style={[styles.shareButton, backgroundColor]}
    >
      <Icon name={icon} style={styles.icon} size={30} color="#fff" />
      <Text style={styles.label}>{label}</Text>
    </TouchableOpacity>
  );
};

const styles = {
  shareButton: {
    padding: 10,
    marginBottom: 10,
    flexDirection: "row",
    justifyContent: "space-between"
  },
  icon: {
    flex: 2
  },
  label: {
    flex: 8,
    marginTop: 5,
    color: "#fff",
    fontSize: 16,
    fontWeight: "bold"
  }
};

export default IconLabel;
```

Once that’s done, you can now update the `Main.js` file so it navigates to the Share screen:

```javascript
// src/screens/Main.js
shareAction = (pokemon, image) => {
  this.props.navigation.navigate("Share"); // add this inside the existing shareAction function
};
```

### Set up the navigator

Now we’re ready to set up the navigator. There are two parts to this. First, we update the `App.js` file to render the `Root` component. This component is where the navigator will be declared:

```javascript
// App.js
import React, { Component } from "react";
import { View, YellowBox } from "react-native";

import Root from "./Root";

YellowBox.ignoreWarnings([
  "Warning: isMounted(...) is deprecated",
  "Module RCTImageLoader"
]);

type Props = {};
export default class App extends Component<Props> {
  render() {
    return (
      <View style={styles.container}>
        <Root />
      </View>
    );
  }
}

const styles = {
  container: {
    flex: 1,
    backgroundColor: "#fff"
  }
};
```

Note that I have ignored the `isMounted(...) is deprecated` and `Module RCTImageLoader` warning. [Those issues](https://github.com/react-navigation/react-navigation/issues/3956) only seem to come up when React Navigation is installed. I haven’t found any solution for this so let’s ignore the warning for now.

Next, let’s proceed with the `Root.js` file. Start by importing the modules and screens that we will be working with:

```javascript
// Root.js
import React from "react";
import { Animated, Easing } from "react-native";
import { createStackNavigator } from "react-navigation";

import MainScreen from "./src/screens/Main";
import ShareScreen from "./src/screens/Share";
```

Next, create a [Stack Navigator](https://reactnavigation.org/docs/en/stack-navigator.html) for the two screens. The `transitionConfig` is an option that we can pass to the Stack Navigator. This is where we will be declaring the animation that we want to perform when navigating between the two screens:

```javascript
const RootStack = createStackNavigator(
  {
    Main: {
      screen: MainScreen
    },
    Share: {
      screen: ShareScreen
    }
  },
  {
    initialRouteName: "Main", // set the default page
    transitionConfig // the animation
  }
);
```

The `transitionConfig` is an object which contains the animation configuration. If you look closely, you’ll notice that it’s using the same options as the ones you use for the `Animated` API. The only difference is the `timing` property. This is the type of animation to use. We’ve previously used `Animated.timing` and `Animated.spring`, you can also use those.

Don’t forget to supply additional options for each type of animation if you want to customize it. For example, `Animated.spring` can have a property called `friction`:

```javascript
// Root.js
const transitionConfig = () => {
  return {
    transitionSpec: {
      duration: 400, // how long the transition will take
      easing: Easing.bounce, // easing function to use (https://facebook.github.io/react-native/docs/easing.html)
      timing: Animated.timing, // the type of animation to use (timing, spring, decay)
      useNativeDriver: true // delegate all the animation related work to the native layer
    },
    screenInterpolator: sceneProps => {
      // next: add code for customizing the transition animation
    }
  };
};
```

Below the general animation setting is the `screenInterpolator` function. This is where you will specify the actual animations that you’d like to perform while the app navigates from one screen to the next.

Start by extracting all the data we need from the `sceneProps`:

- `layout` - contains information about the screen layout. Things like the height and width of the screen. In this case, we’re only using it to determine the initial width (`initWidth`) of the current screen.
- `position` - the position of the current screen. This is the animated value that we can interpolate in order to perform animations.
- `scene` - contains information about the screen we’re navigating to. In this case, we’ll only be using it to get the index of the next screen. Remember, we’re using a Stack Navigator, so screens are just stacked on top of each other. This means the next screen will have a higher index than the current screen. Unless we’re navigating backward.

```javascript
// Root.js
const { layout, position, scene } = sceneProps;
const thisSceneIndex = scene.index; // the index of the current screen
const width = layout.initWidth; // the width of the current screen
```

At this point, we can now perform the animations. By default, the Stack Navigator uses transforms to translate the X position of the screen. If you’re navigating to a new screen, the new screen slides from right to left until it replaces the current screen. When navigating backward, the reverse is performed (the current screen slides left to right until it disappears from view. This reveals the entirety of the previous screen when it ends). The code below does the same thing, but we’re also animating the opacity. Aside from that, the transition will have a bit of bounce to it, since we applied `Easing.bounce` as the easing function. This will gradually fade into the next screen when moving forward, and fade the current screen out when going back:

```javascript
// Root.js
const translateX = position.interpolate({
  inputRange: [thisSceneIndex - 1, thisSceneIndex],
  outputRange: [width, 0],
  extrapolate: "clamp" // clamp so it doesn't go beyond the outputRange. Without this, you'll see a few black portions in the screen while navigating
});

const opacity = position.interpolate({
  inputRange: [thisSceneIndex - 1, thisSceneIndex - 0.5, thisSceneIndex],
  outputRange: [0, 0.2, 1],
  extrapolate: "clamp"
});
return { opacity, transform: [{ translateX }] }; // return the animated styles to be applied to the current view upon navigation
```

From the code above, you can see that we’re actually using the same Animated API that we’ve previously used. This means that you can actually apply the rotation, scale, spring, and all the other animations we’ve previously implemented. Your imagination is the limit! Don’t go overboard though, transition animations should be fast and simple to avoid annoying the user.

Note that we don’t have to specify how the `position` will be interpolated when going back to a previous page. React Navigation also takes care of this.

Lastly, don’t forget to export the `RootStack`:

```
export default RootStack;
```

### Adjust the header title

With the addition of the header added by React Navigation, we have to adjust the header title accordingly. Open the `src/components/AnimatedHeader.js` file and replace the following line:

```javascript
const titleTranslateY = nativeScrollY.interpolate({
  /*previously added inputRange and extrapolate value here*/
  outputRange: [25, 35, 15] // existing outputRange
});
```

With this line:

```javascript
const titleTranslateY = nativeScrollY.interpolate({
  outputRange: [0, -10, -8] // only replace existing outputRange with this line, other lines inside this are still intact
});
```

Because there is now a header above the animated header, we no longer have to add a Y position. We can even add a negative Y position and it won’t go over the status bar.

### Solving the header issue

At this point, you should now be able to try the transition out in your device or emulator. But if you try the animated modal that we’ve implemented on the first part, you’ll see this:

![Issue with header z-index](https://d2mxuefqeaa7sj.cloudfront.net/s_C1809527E2B34D744AF2137B86B5994B06DACA31E57B532677A9809007467FAE_1529560539029_issue-with-header.gif)

The issue above is that the parent of the `AnimatedModal` component has a lower z-index value than the header. That’s fine because we want the header to be on top of everything else. That is, except for modal windows. This is one of the benefits of using React Native’s `Modal` component because it is laid on top of everything else. But we opted out of it because we wanted full control of the animations.

To solve the issue, we’ll create a new screen which shows the same contents as the `AnimatedModal`. And then we’ll tell React Navigation to treat the new screen as a modal, not a screen.

The first step is to copy the contents of the `src/components/AnimatedModal.js` file into a new file called `src/screens/Details.js`. The following are the changes that need to be made:

- Remove all the code that animates the Animated Modal. The animations will now be implemented using React Navigation so we no longer have to implement it on our own.
- Update the code so it no longer uses the `Header` component. The new Details page will be headerless.
- Props should be coming from the navigation instead of supplied directly to the component.
- Add a button for closing the modal. This will simply navigate the user backward, but it will use the same animation as a modal. The screen can also be flicked downwards just like a modal does.

Once those changes are made, the code should now look like this:

```javascript
// src/screens/Details.js
import React, { Component } from "react";
import { View, Text, TouchableOpacity } from "react-native";

import BigCard from "../components/BigCard";

type Props = {};
export default class Details extends Component<Props> {
  render() {
    const { navigation } = this.props;
    const title = navigation.getParam("title");
    const image = navigation.getParam("image");
    const data = navigation.getParam("data");

    return (
      <View style={styles.container}>
        <View style={styles.modalContent}>
          <TouchableOpacity
            style={styles.closeButton}
            onPress={() => {
              navigation.goBack();
            }}
          >
            <Text style={styles.closeText}>Close</Text>
          </TouchableOpacity>
          <BigCard title={title} image={image} data={data} />
        </View>
      </View>
    );
  }
}

const styles = {
  container: {
    flex: 1,
    backgroundColor: "#fff"
  },
  modalContent: {
    flex: 1,
    alignItems: "stretch",
    paddingTop: 30
  },
  closeButton: {
    alignSelf: "flex-end"
  },
  closeText: {
    color: "#333",
    paddingRight: 10
  }
};
```

You also need to update `src/components/BigCard.js` so it only initiates the animation on `componentDidMount` instead of `componentDidUpdate`. This is because it no longer resides inside the `AnimatedModal` component which is simply hidden from view. To do that, simply replace `componentDidUpdate` with `componentDidMount`. All the code inside of it is still intact.

Next, update the `src/screens/Main.js` file so it no longer uses the `AnimatedModal` component when the **View** button is clicked. It should navigate to the Details screen instead:

```javascript
viewAction = (pokemon, image) => {
  this.props.navigation.navigate("Details", {
    title: pokemon,
    image: image,
    data: this.getPokemonStats()
  });
};
// next: add function declaration for getPokemonStats()
```

At this point, you can delete the `src/components/AnimatedModal.js` file as well.

Here’s the code for the `getPokemonStats` function:

```javascript
// src/screens/Main.js
// add right after viewAction
getPokemonStats = () => {
  let pokemon_stats_data = [];
  pokemon_stats.forEach(item => {
    pokemon_stats_data.push({
      label: item,
      value: getRandomInt(25, 150)
    });
  });

  return pokemon_stats_data;
};
```

Next, import the `DetailsScreen` from the `Root.js` file:

```javascript
import DetailsScreen from "./src/screens/Details";
```

Still on the `Root.js` file, we need to tell React Navigation that the Details screen should be treated as a modal. The following are the steps for doing so.

Create a new Stack Navigator. This contains the screens of the app. These are the same as what we have earlier, so all you have to do is copy the existing `RootStack` and paste the new one on top of the existing one then rename it to `MainStack`. Once that’s done, your code should look like this:

```javascript
// Root.js
const MainStack = createStackNavigator(
  {
    Main: {
      screen: MainScreen
    },
    Share: {
      screen: ShareScreen
    }
  },
  {
    initialRouteName: "Main",
    transitionConfig
  }
);

const RootStack = createStackNavigator({...}); //next: add config options in place of "..."
```

The `RootStack` will now have the `MainStack` and the `DetailsScreen` as its screens. We’re also supplying an object specifying that the `mode` should be `modal`, and if it is, then there will be no header displayed. This setup works because we’ve wrapped our screens (Main and Details) inside a Stack Navigator. Thus, only the screens which are used as is will be considered as a modal:

```javascript
const RootStack = createStackNavigator(
  {
    Main: {
      screen: MainStack // MainStack was previously MainScreen
    },
    Details: {
      // existing ShareScreen is remove and replaced with this one
      screen: DetailsScreen
    }
  },
  // replace initialRouteName and transitionConfig with below code:
  {
    mode: "modal",
    headerMode: "none" // don't display a header if modal
  }
);
```

The last step is to clean up the remaining AnimatedModal code on `src/screens/Main.js`. Make sure to remove this specific code from that file:

```javascript
<AnimatedModal
  title={"View Pokemon"}
  visible={this.state.isModalVisible}
  onClose={() => {
    this.setState({
      isModalVisible: false
    });
  }}
>
  <BigCard
    title={this.state.pokemon}
    image={this.state.image}
    data={this.state.stats}
  />
</AnimatedModal>
```

Once you’ve made the necessary changes, it will now look like this:

![React Navigation modal](https://d2mxuefqeaa7sj.cloudfront.net/s_C1809527E2B34D744AF2137B86B5994B06DACA31E57B532677A9809007467FAE_1529562631830_modal-react-navigation.gif)

## LayoutAnimation

The last animation that we’re going to implement is LayoutAnimation, and it looks like this:

![LayoutAnimation spring scaleXY](https://d2mxuefqeaa7sj.cloudfront.net/s_C1809527E2B34D744AF2137B86B5994B06DACA31E57B532677A9809007467FAE_1529569361615_layout-animation.gif)

React Native’s [LayoutAnimation](https://facebook.github.io/react-native/docs/layoutanimation.html) is only useful for simple animations. These animations are automatically applied to the components whose state was recently updated.

In the demo above, we’re shuffling the array of Pokemon data when the button in the header is clicked. The state is then updated with the newly ordered data. This results in the spring animation that you see above.

LayoutAnimation can be implemented in three steps:

1.  Import the `LayoutAnimation` module.
2.  Specify the animation you want to perform when the state is updated.
3.  Call `LayoutAnimation.configureNext` right before the state is updated.

Before we proceed, note that LayoutAnimation isn’t enabled on Android by default. This is because it’s still considered as an experimental feature in Android. For iOS, it should work by default.

Start by opening the `src/screens/Main.js` file and import the following:

```javascript
import { /* existing modules here */ Platform, UIManager } from "react-native";
```

Then check if the platform is Android then enable LayoutAnimation:

```javascript
if (Platform.OS === "android") {
  UIManager.setLayoutAnimationEnabledExperimental(true);
}
```

Now that that’s taken care of, let’s get into it.

First, import the `LayoutAnimation` module:

```javascript
// src/screens/Main.js
import { /* existing modules here */ LayoutAnimation } from "react-native";
```

Next, specify the type of animation. This requires the `type` and `property` to be supplied:

- `type` - the type of animation you want to perform. Currently, these includes the following preset values: `easeIn`, `easeInEaseOut`, `easeOut`, `linear`, `spring`.
- `property` - the property you want to animate. Currently, only `scaleXY` and `opacity` are available. `scaleXY` animates the width and height of the component, while `opacity` animates the opacity.
- `springDamping` - how much resistance you want to apply to the spring. This should be a value that’s less than or equal to one. The lesser the number, the more spring will be applied to the component.

```javascript
// src/screens/Main.js
const springAnimationProperties = {
  type: LayoutAnimation.Types.spring,
  property: LayoutAnimation.Properties.scaleXY,
  springDamping: 0.3
};
```

Next, construct the actual animation config. Specify the animations to be performed when a new component is rendered (`create`), when an existing component is updated (`update`), and when a component is deleted (`delete`). In this case, we’re specifying the same animation for all three. But you can also omit `update` and `delete` since we’ll only be using `create` as you’ll see later:

```javascript
const animationConfig = {
  duration: 500, // how long the animation will take
  create: springAnimationProperties,
  update: springAnimationProperties,
  delete: springAnimationProperties
};
```

Next, add an `onPress` prop to `AnimatedHeader`. The function you pass to it will be executed when the user clicks on the header button which we’ll be adding shortly:

```javascript
<AnimatedHeader
  title={"Poke-Gallery"}
  nativeScrollY={nativeScrollY}
  onPress={this.shuffleData}
/>
```

After that, update the `AnimatedHeader` so it wraps the header text with `TouchableOpacity`. We then pass the `onPress` prop to it so it gets executed when the a user clicks on it. Don’t forget to destructure `TouchableOpacity` out of `react-native` first, as well as destructure `onPress` from the props passed to `AnimatedHeader` before doing this:

```javascript
// src/components/AnimatedHeader.js
<Animated.View style={[styles.header_bar, headerBarStyles]}>
  <TouchableOpacity onPress={onPress}>
    <Text style={styles.header_text}>{title}</Text>
  </TouchableOpacity>
</Animated.View>
```

Going back to the Main screen, below is the `shuffleData` function. This is where the final piece of LayoutAnimation happens. Right before you update the state with the shuffled data, call `LayoutAnimation.configureNext()` and supply the `animationConfig` from earlier:

```javascript
// src/screens/Main.js
shuffleData = () => {
  LayoutAnimation.configureNext(animationConfig); // configure next LayoutAnimation
  let newArray = shuffleArray(this.state.pokemon); // randomly order the items in the array
  this.setState({
    pokemon: newArray
  });
};
```

Don’t forget to set the default `pokemon` array in the state:

```javascript
export default class Main extends Component<Props> {
  state = {
    pokemon: pokemon
  };
}
```

And then set it as the data source for the `CardList` component:

```javascript
// this is inside the render method of src/screens/Main.js
<CardList
  data={this.state.pokemon}
  ...previously added props
/>
```

The `shuffleArray` function is declared in the `src/lib/random.js` file:

```javascript
const shuffleArray = arr => {
  return arr
    .map(a => [Math.random(), a])
    .sort((a, b) => a[0] - b[0])
    .map(a => a[1]);
};
export { /* existing exports here */ shuffleArray };
```

Don’t forget to import it on the `src/screens/Main.js` file:

```javascript
import { /* existing imports here */ shuffleArray } from "../lib/random";
```

Once that’s done, the spring animation is performed when you click on the header text.

## Conclusion

That’s it! In this tutorial, you’ve learned how to add transition animations to your React Native app. As you have seen, the React Navigation library made it easy for us to implement custom page transition animations. You’ve also seen that when it comes to animations, there are a few changes for each platform that we need to deal with before we can implement the same animations.

The full source code for this tutorial is available on this [GitHub repo](https://github.com/anchetaWern/RNRealworldAnimations). Be sure to switch to the `part2` branch if you only want the final output for this part of the series.

_Originally published on the [Pusher blog](https://blog.pusher.com/animation-react-native-part-2/)_