Setting up transitionConfig

Here’s a simple implementation of transitionConfig that slides in new screens from the right, much like the default animation:

Let’s step through it

transitionConfig is a function that returns an object with two parameters: transitionSpec and screenInterpolator . We’ll configure animation timing properties like duration and easing in the transitionSpec , and we’ll configure our layout transformations in screenInterpolator .

There’s nothing too special about transitionSpec . In fact, it looks a lot like a standard React Native Animated example. We set the duration of our transition and the easing profile, configure it to be a timing-based animation rather than a spring, and to use the native driver for performance.

The screenInterpolator is where the magic happens. screenInterpolator is a function that React Navigation calls with an argument we’ll call sceneProps . screenInterpolator(sceneProps) is called for each screen in the stack and the return value is used to configure its transition.

So, if we want the screens to slide up instead of from the side, we’ll return a translateY transformation. If we want screens to fade in and out, we’ll return an opacity transformation.

sceneProps is an object that contains information about the transition as well as an animated value called position . Let’s run our app and log out sceneProps to get a better feel for what’s going on. The first thing we’ll notice when we press “Go Forward!” from our initial screen is that screenInterpolator gets called four times:

Why four? Well, we have two screens in our stack: the screen we started on and the screen we’re navigating to. It turns out the screenInterpolator is called for each scene both at the beginning and at the end of the transition.

(I’m not exactly sure why screenInterpolator gets called again after the transition completes but I think it’s due to a re-render within React Navigation due to the change in navigation props)

Let’s take a look at the value of the sceneProps object the very first time screenInterpolator(sceneProps) is called:

Oof, that’s a lot of data. Luckily, we don’t care about most of it. For now, let’s take note of a few important values contained within sceneProps :

index: tells us the route index where we’re navigating to, in this case we’re going from index 0 to 1 , so index: 1

tells us the route index where we’re navigating to, in this case we’re going from index to , so position: is a shared animated value that will animate from fromIndex to toIndex over the course of the entire transition. In this case position will range from 0 to 1 and sweep through all decimal values in between. The animation will take 750 milliseconds to complete, which we set in transitionSpec .

is a shared animated value that will animate from to over the course of the entire transition. In this case will range from to and sweep through all decimal values in between. The animation will take 750 milliseconds to complete, which we set in . scene: an object containing data about one scene on the stack. Since we’re looking at the first call to screenInterpolator , scene represents the initial route in our stack, which is the route we’re navigating from. Immediately after this, screenInterpolator(sceneProps) will be called again and scene will be the route we’re navigating to. scene also contains a route object that contains any navigation params.

an object containing data about one scene on the stack. Since we’re looking at the first call to , represents the initial route in our stack, which is the route we’re navigating from. Immediately after this, will be called again and will be the route we’re navigating to. also contains a object that contains any navigation params. scenes: the state of the route stack at the time the transition was triggered.

To get a feel for what’s going on under the hood, imagine looping through the route stack, setting up each screen by calling screenInterpolator() on it, then kicking off the transition animation. It might look something like this:

(Here’s the actual code if you’re interested)

Let’s take a moment to prove to ourselves that scene is being set the way we think it is. Here are the same console.logs posted above, but I’ve popped open the first two:

As expected, the first time screenInterpolator(sceneProps) is called, sceneProps.scene is our initial route ( index: 0 ). The second time it is called, sceneProps.scene is the route we are navigating to ( index: 1 ).

Creating custom transition animations

Now that we know the index we’re navigating to, we have a reference to our position animated value, and we know how to access each individual scene being passed through the screenInterpolator , we can build our first custom animation. We’ll keep it simple for now and replace the slide-in-from-the-right behavior with a fade-in from opacity: 0 to opacity: 1 :

Now is a good time to take a closer look at the position.interpolate() interpolation, which will drive all of our animations. Remember, position is an animated value that will range from the fromIndex to toIndex over the duration of our transition (750ms in our case). This gets slightly math-y, and if you’re like me you’ll have to reread it a few times before it sinks in, but it’ll pay off!

Interpolation

If you’re not familiar with the concept of interpolation, check out the React Native docs, which can explain it better than I can.

When we configure an interpolation, we define how we want an inputRange to map to an outputRange . Consider the following interpolation:

animatedValue.interpolate({

inputRange: [0, 1],

outputRange: [0, 100]

})

The inputRange refers to the value of animatedValue , which we expect to swing between 0–1 (notated as [0, 1] ) over the course of its animation. This range gets mapped to our outputRange , which we’ve set to the range 0–100. For example, when our animation is halfway through, animatedValue has value 0.5 and the output will be 50 , or halfway through the outputRange we’ve specified.

If animatedValue goes below 0 or above 1 the interpolation will continue to output using whatever mapping was applied at 0 or 1 (i.e. an animatedValue of -1 would output -100 ).

We can add more ‘anchor points’ to the arrays to further customize the mapping:

animatedValue.interpolate({

inputRange: [0, 0.5, 1],

outputRange: [0, 10, 100]

})

The above snippet will output the range from 0 — 10 as the animatedValue moves from 0 to 0.5 , then as animatedValue increases from 0.5 — 1 , the output will be values from 10 — 100 .

Here’s what that looks like plotted on a line chart:

We can fine-tune our animations by creating custom interpolations.

Interpolating the value of ‘position’

Ok, now back to our app. Let’s take a look at the position interpolation, which we’re using to set each screen’s opacity :

const opacity = position.interpolate({

inputRange: [thisSceneIndex - 1, thisSceneIndex],

outputRange: [0, 1],

})

We’re saying “as the value of position increases from thisSceneIndex — 1 to thisSceneIndex , I want this scene’s opacity value to ramp up from 0 to 1 ”.

This was a big aha moment for me so let’s make sure it sinks in: we’re configuring each screen to animate from some ‘disabled’ state (i.e. opacity: 0 ) to some ‘active’ state ( opacity: 1 ) as the animated position value approaches and then equals that screen’s own index.

Here’s how a screen with index: 2 would be handled by our sceneInterpolator :

Whenever position is less than or equal to scene.index — 1 its opacity is at opacity: 0.0 . However, as position increases beyond 1.0 and approaches 2.0 , our opacity value ramps up as defined in our interpolation. It reaches its full value of opacity: 1.0 when position === index (i.e. position: 2.0 ).

Here’s what our opacity-based screenInterpolator looks like in our app:

Note that screenInterpolator doesn’t apply to the header

An over-the-top example

Let’s make a screenInterpolator where the first four screens slide in from the bottom, then any other screens added will start 4x larger than normal and scale down while fading in.

We’ll add some randomness too. We’ll tweak our ‘GoScreen’ component so that 25% of the time a plain navigation param is set to true . When plain: true , we’ll simply slide that screen in from the right. Got all that? Here we go!

First, we’ll add the code to our component that randomly sets a plain: true param whenever Math.random() is more than .75 (i.e. ~25% of the time):

Then we’ll set our screenInterpolator to output different transformations based on our navigation state and params:

The screens that slide in from the right will be different each time since they are based on random numbers:

Great! We’ve now seen that we can customize screens based on their index on the stack and based on custom navigation params we pass to them.