React Native Animations: Part 1

computer screen with coffee mug

At Raizlabs we need our apps to look great and be easy to use. One of the ways we add surprise and delight to an app is by using animation. Lately, we’ve been building cross-platform React Native apps: this gives us new challenges and opportunities. While you can do some pretty complex stuff with React Native animations, they’re not very well documented, and not always intuitive. Here are some of the things we’ve learned.

How do animations work in React Native?

First, a look under the hood. Reading through the (surprisingly readable!) React Native codebase has helped us figure out how to build advanced animations and avoid laggy interfaces. In the underlying code, you’ll see references to a few different types of animation objects: Animated Values, Transforms, Drivers, and Props are laced together as nodes in a graph.

Animated Values

Animated values are the building blocks of animations in React Native. They point to a real number, and will eventually be converted back into a real number when passed to a component. They can be created manually: new Animated.Value(42) or by running an animation like this:

In this example, valueToAnimate is an Animated Value that will equal 42 after 1000 milliseconds. It’s not always simple to get the value of an Animated Value at a specific moment—more on this shortly.

We use Animated Values to set view properties like opacity or position, as if they were regular numbers:

But make sure you’re using them with an Animated component or React will throw an error.

Animation Drivers

Drivers are nodes in the graph that change Animated Values every frame. These are things like Animated.Timing, which drives a value over time, or Animated.Decay, which reduces a value every frame. We can connect an Animated.Event to an onScroll event: this drives a value as the user scrolls. We can start or stop a Driver, and connect it to other Animated Values or Drivers. When the driver updates each frame, its new value propagates down through the graph, updating its child Animation Drivers or Animated Values and eventually updating view properties.

In the decay animation above, our Animated Value starts at a velocity, and slows over time.

Transform Operations

We convert Animated Values to new Animated Values through Transform operations.

Animated.add() , Animated.multiply() or Animated.interpolate() are examples of Transforms. We can run a Transform on any other node in the Animated graph: an animated value (new Animated.Value(42)).interpolate(...), or another transform: animatedPosition.interpolate(...).interpolate(...).

In one of our apps, we show a large illustration in the navbar, and minimize it as the screen scrolls. This gives the user more space to be productive. To do this, we pass the scroll distance (an Animated Value) as a prop on the NavBar component. The NavBar interpolates the scroll distance into a position or opacity, fading itself out and moving it up and off the screen.

When the scrollView has been scrolled 20 points, the navbarOpacity Animated Value will be equal to 1.0. (Don’t forget to clamp the animation, or it will continue to increase as you scroll further.)

You can add multiple child node interpolations on this Animated.Value. This is useful if you need multiple animations to run based on the scroll position.

Animated Props

This is a special node that maps an Animated Value to a prop on a component. These are built when you render an Animated.View and assign it properties.

In the above example, an Animated Prop is added to the graph that converts the value to a property. If a driver were to update the value of opacity, the change would trickle down to the view’s property.

How they all work together

These 4 components are connected to each other in a graph. An Animation Driver changes an Animated Value every frame. The change is passed along through any child Values or Transformations and into an Animated Prop, where the value is resolve as a prop of the view (like opacity or a transform value).

Then JavaScript hands things off to the native side. JS updates the view by calling setNativeProps.  This passes the value over to iOS or Android, which updates the UIView or android.View.

If you inspect an Animated.Value, you can see this graph for yourself. The Animated Value object has a _children property: an array of Animated Nodes that are dependent on this value. The array might include more AnimatedValues or AnimatedChildren, which may have nested _children. The children might also include an AnimatedStyle, which contains AnimatedProps: the style prop of a view is dependent on this animated node.

How do I get the actual value of an animated value?

Sometimes it’s useful to know the actual value of an Animated Value. You might need to perform a boolean check (like scrollDistance > 20). While inspecting an Animated.Value, you will see that it has a _value property, and a __getValue() method. Unfortunately these are private (denoted by the underscores) for a reason. A note from the React Native documentation:

Animated is designed to be fully serializable so that animations can be run in a high performance way, independent of the normal JavaScript event loop. This does influence the API, so keep that in mind when it seems a little trickier to do something compared to a fully synchronous system.

__getValue() might get you what you need, but changes in future React Native releases might break it.

There is a workaround, although the docs note that it too may affect performance in the future. You can add a listener to an Animated.Value, with a callback that will be triggered periodically. In theory it will be called every time the value changes, but in practice heavy CPU usage may delay or drop these callbacks. The callback receives the value, which you can use or store in your component state.

We came up with this higher-order component that converts any animated value to a number. This simplifies your components by separating out the listener logic.

GitHub Gist: https://gist.github.com/pangolingo/15c26a8d6d015cb858c1924f72a4e719

How do you use Animated.Event?

You can map an event (like a scroll or pan) to an Animated Value using Animated.event. This is a function: when called, it returns an event handler function. When this event handler is called (in an onScroll event, perhaps), it updates the Animated Value you specify. Animated.event can use a native driver—then it won’t have to touch JavaScript-land at all.

Here’s a convenient function we use to add an Animated.event scroll listener. It allows you to choose between native and non-native animations.

We set our this.state.scrollDist variable equal to the event’s nativeEvent.contentOffset.y—the vertical distance that the ScrollView has been scrolled.

When we’re using native scroll events, we don’t want to throttle the event sending speed at all: we set it to 1. Otherwise we throttle the events to avoid overwhelming the JavaScript thread. Our value will be updated at this frequency.

The function returns an object, which we can expand as props:

<Animated.ScrollView {...scrollProps()} />

Making animations reusable

If we have a lot of animations occurring, our graph of animated nodes is going to get complex. Timers will be linked to interpolations which will link to other interpolations, which will be set as props.

When animating multiple properties, we create a baseline animation, running between 0 and 1: start and finish. Then we can interpolate this to create values for each of our properties. You can see the code here. 

Coming up next: Native Animations

There’s plenty to learn about animation in React Native. You can read more in React Native’s performance docs or by looking at the code. It’s very well commented here. 

In a future blog post we’ll take a look at how to improve performance by using native animations.


Do you have a project in mind? We’d love to work with you. If you’d like an opportunity to work on projects with us, check out our Careers page. We’re hiring!

Leave a Comment