Declare Peace with React Native Animations

Animations make your mobile app stand out, look more interactive, and help you increase engagement. Slack would be just another chat app without so many eye candy details.

I’ll try to explain how to think about animations, show what are possibilities for animations in React Native, and expose some of my tricks on how to make them reusable all over the app across various components with a different type of interactions.

About a year ago, when we started making the first Shoutem extensions, our design team asked me if it’s possible to make the following animation. Make details screen in News extension with bounce zoom and parallax animations on a header image, and a color changing animation on the navigation bar.

I have immediately said “Yes, of course”, but I soon realized it isn’t going to be easy to implement.

Breaking it to basics

First of all, when a designer gives you a prototype, try to look at the animations one by one. Animations in their essence are just presentations of each step of change between two states. Looking at it like this, we could examine each animation as a composition of changes between two different states of various properties, so thatany animation could be described as a change of:

  • opacity (some object fades in or out)
  • position (object translates from one part of the screen to another)
  • size (object grows or shrinks in one or both dimensions)
  • rotation (object rotates towards the x, y or z-axis for a given angle)
  • color (object changes color of text, background or border from one to the other)

There are also some other animations that could be described with transformation matrices (e.g. perspective), but they are essentially the composition of size, position, and rotation with a different point of gravity.

Having the previous definition in mind, we could break down our animations into two groups: Navigation bar and header animation.

  1. Navigation bar:
    - title and icons change color from white to black
    - container changes its background from transparent to white.

2. Header animation: 
- title animates its opacity, from 0 when the scroll bounces to 1 when the scroll is about 0 and to the opacity of 0 when the scroll continues. 
- image grows when scroll bounces and has some parallax when scroll continues

If we consider that scroll bounce is just scrolling from a negative value, the image actually shrinks in size as the scroll is being changed from negative to 0, and then stays in the same scale ever after.

Parallax is just a compensation of scroll, so if the scroll is moving with speed x we should move slower or faster than that. On our prototype, the image is scrolling slower than content. To achieve the desired animation, we should translate through the y-axis with the negative shift.

So, now when we understand all animations, in theory, let’s see how we can achieve that with React Native.

React Native animations

Currently there are 3 types of animations available in React Native at the moment:

  • LayoutAnimation
  • Animated API
  • Navigation transitions

LayoutAnimation

LayoutAnimation typically animates views to their new positions when the next layout happens. A common way to use this API is to call it before calling setState which causes layout change. This will give you nice and smooth animations on the native thread, but sometimes (in 10% cases) it will result with unexpected animations.

For example, if you want to create a fade in the animation of an element, the common way to do it with LayoutAnimation. Add the element to your layout after you configure layout animations for the next layout change, by calling `LayoutAnimation.easeInEaseOut()`. It will produce the fade in effect, but sometimes newly added element will just slide into its position. And that happens in a nondeterministic way.

Animated API

The simplest workflow to work with Animated API is to create an Animated.Value. Hook it up to one or more style attributes of an animated component, and then drive updates either via animations, such as Animated.timing or by hooking into gestures like panning or scrolling via Animated.event.

Animated.Value can also bind to props other than style and can be interpolated as well. Animations could be driven by JS or native thread, depending on what style prop you want to interpolate. This API is what you will use to create most of the animations.

Navigation transitions

The navigation transition is the animations between app screens. It differs a lot from library to library. Some libraries, like native-navigation from AirbnbEng, supports shared element transitions.

Solution no. 1: plain React Native

Year ago, just before react-europe 2016, we created the first prototype of our extensions which we showed at our booth at the time. Visitors were impressed with animations in our demo app, but the code for our prototype News details screen looked like this:

This produced a screen which looked and worked as we wanted, but the code was not reusable. We needed to duplicate a lot of things if we wanted to have any similar screen to this one. The idea of extensions was to provide developers with an easy and reusable way of making nice apps. At the time being we were focused on releasing our UI Toolkit, but we haven’t come up with a solution for animations yet, and this amount of code wasn’t acceptable for us.

Solution no. 2: Declarative animations

The first idea we implemented, to reduce the amount of boilerplate animation code, was to extract them in React components. With this approach, we could have a component for each basic animation and just compose them to create the complex one.

Besides creating React components that represent animations, we also had to determine how to setup the animated values that drive those animations. For example, animations from our example screen are usually controlled by a ScrollView. We wanted to allow users to easily connect their animations to any source they want to use.

The idea of Driver

Let’s think about animations for a moment. We said animation is a change of some state, and that change should be driven by something. In UI, animations are driven by time, force and user interaction. In this particular case, the animation is caused by user interaction of scrolling.

To support generic animation components we had to introduce the concept of an animation Driver. According to our documentation, animation Driver is:

An animation is driven by the driver. It encapsulates the creation of animation input events, making React Native animations even more declarative. Drivers are attached to animation components which are set to listen to specific driver inputs.

In this case, we need to create animations driven by scrolling, so we need a ScrollDriver which should handle all scrolling related animation handling.

Declarative animations

With all these we were able to create components for all basic animation types:

  • FadeIn/FadeOut (animating opacity)
  • SlideIn/SlideOut (animating translation transform)
  • Rotate (animating rotation transform)
  • ZoomIn/ZoomOut (animating scale transform)
  • Parallax (animating translation on scroll)
  • HeroHeader (just a wrapper for ZoomOut + Parallax effect)

Take a look at these components and notice, that they always animate opacity or transform style property.

Remember, always try to bring down animations on a change of transform or opacity style prop. Change of these props doesn’t cause layout recalculations and will help you to keep animations on 60fps. These props are also supported by native animations, which run on the native thread.

Finally, with these components our screen became more reusable and looked like this:

This solution has a lot less code and is easy for everyone to write or learn. We wanted to provide an even simpler way of doing these animations.

Solution no. 3: connectAnimation HOC

We wanted a way of reusing complex animations, not just pre-exported compositions of basic animations such as HeroHeader component. We wanted to reuse more complex animations that could animate any style prop and be changed over the theme. We also didn’t want to declare ScrollDrivers all over again on each ScrollView.

To solve this, we created connectAnimation, component that enables components reusage of multiple animations. Each connected component may have multiple animations defined through its style, and those animations can be activated by specifying their name through the animationName prop:

<Image animationName="hero" source={{ uri: article.imageUrl }} />

animationName is a name of animation function which is going to be used to create animated style. Hero animation on Image component then looks like this:

heroAnimation(driver, { layout, options }) {
return {
transform: [
{
scale: driver.interpolate({
inputRange: [-0.9 * layout.height, 0],
outputRange: [3, 1],
extrapolateRight: 'clamp',
useNativeDriver: true,
}),
}, {
translateY: driver.interpolate({
inputRange: [-100, 100],
outputRange: [-50, 50],
extrapolateLeft: 'clamp',
useNativeDriver: true,
}),
},
],
};
}

Each animation function is named as animationName + ”Animation” suffix to distinct animation functions when they are defined in style. Driver argument represents driver which will be used to drive animation and second argument is an object that holds layout and options.

layout is measured layout on each onLayout event of the component and has height, width, x, and y.

options represent an object that is passed as animatedOptions to the connected component. For example, if we wanted to pass maximum scale factor to hero animation we would do it like this:

<Image animationName="hero" animationOptions={{ maxScaleFactor: 5 }} source={{ uri: article.imageUrl }} />

and use it in function like this:

heroAnimation(driver, { layout, options }) {
return {
transform: [
{
scale: driver.interpolate({
inputRange: [-0.9 * layout.height, 0],
outputRange: [options.maxScaleFactor, 1],
extrapolateRight: 'clamp',
useNativeDriver: true,
}),
}, {
translateY: driver.interpolate({
inputRange: [-100, 100],
outputRange: [-50, 50],
extrapolateLeft: 'clamp',
useNativeDriver: true,
}),
},
],
};
}

Having connectAnimation and all components connected ArticleDetailsScreen now becomes as it is today:

This solution didn’t only give us a simpler way of reusing animations but enabled us to customize each animation through style which could result with a whole different experience in different application themes. Now, to change the animation we should only customize its animation function. To see it in action and how did we do that in our themes, check out this repo:

Conclusion

You are now familiar with the power of React Native Animated API. You have seen what needs to be done to make an animation with it. You have seen the evolution of our code. But what is the real power of @shoutem/animation? Could you do anything with it? What needs to be done yet to have real superpowers?

As I have mentioned it before @shoutem/animation provides you with these animation drivers:

this.driver = new TimingDriver({
duration: 400, // animation will last 400ms
delay: 200, // animation will be delayed for 200ms
easing: 'ease-in', // change will not be uniform trough time
useNativeDriver: true, // native animations will be used
});
this.driver = new ScrollDriver({ useNativeDriver: false });
this.driver = new TouchableDriver();

And these animation components:

render() {
return (
<FadeIn driver={this.driver}>
<Text>I will fade in</Text>
</FadeIn>
<FadeOut driver={this.driver}>
<Text>I will fade out</Text>
</FadeOut>
<SlideIn driver={this.driver} from="top right">
<Text>I will slide in from top right</Text>
</SlideIn>
<SlideOut driver={this.driver} from="bottom left">
<Text>I will slide out from bottom left</Text>
</SlideOut>
<ZoomIn driver={this.driver} maxFactor={3}>
<Text>I will zoom in</Text>
</ZoomIn>
<ZoomOut driver={this.driver} maxFactor={3}>
<Text>I will zoom out</Text>
</ZoomOut>
<Rotate driver={this.driver} axis="y" angle="180deg">
<Text>I will rotate around y-axis for 180 degrees</Text>
</Rotate>
<Parallax driver={this.driver} scrollSpeed={1.2}>
<Text>I scroll 20% than others</Text>
</Parallax>
<HeroHeader>
<Text>
I will zoom when scroll bounces and have parallax after
</Text>
</HeroHeader>
);
}

But real power is in higher order components:

measure(MyComponent) // adds layout { x, y, height, width, pageX, pageY } object to MyComponent's state
connectAnimation(MyComponents, {
fadeOutAnimation(driver, context) {
return {
opacity: driver.interpolate({
inputRange: [0, context.layout.height],
outputRange: [1, 0]
})
}
}
}) // now your component can use animationName="fadeOut"

If you take a look at this file, you will figure out how all our animated components are just a simple React Native View connected with connectAnimation.

Let’s see what else remains to be done. Maybe this will inspire you to contribute and submit a pull request to the repo :)

I think that we need more Driver types. That will enable more animation types. What first comes to my mind is:

  • PanDriver — Driver that will encapsulate usual PanResponder handlers
  • SpringDriver — Driver that will behave like Animated.spring (I think I will do it soon)
  • ComputedDriver — Driver that could combine various values or drivers to calculate new value on each change of included arguments.
Any your ideas are welcome, we are waiting for it.

Beside animations, I hope that this article showed you that sometimes:

It is worth to solve a problem on a higher level and give that solution to the community.

I work at Shoutem where I help creating tools to supercharge your React Native development.