Back to Blog

Custom Layout Animations with Reanimated - Part 1

Ditching the old LayoutAnimation and embracing a new reanimated primitive

January 31st, 2024

6 min read

by @rgommezz

Photo by Sebastian Svenson

Introduction

Layout Animations are one of the most powerful, yet overlooked, features to help create aesthetic and enjoyable experiences for your mobile users.

Since I started my career as a mobile developer, I've been obsessed with creating delightful user experiences and fascinated by the power of animations and interactions to provide identity, increase engagement and make applications stand out against the competition.

Layout Animations is a poorly understood topic in React Native. Firstly, there is barely any documentation on the subject, and secondly, the API is not very intuitive and comes as experimental on Android.

But what are Layout animations? Let's start from first principles and explain the differences between individual animations, which are the ones you are most used to, and layout animations.

Individual Animations

Individual animations typically focus on moving or transforming individual elements on the screen and are usually easier and quicker to implement. They are also more performant, as they don't require the layout engine to recompute the layout of the entire view hierarchy. As a result, you can only animate non-layout properties, such as opacity, scale, translateX, translateY, rotate, etc which are properties that don't affect the layout of the element's surrounding views.

That way, the layout engine can skip the entire layout pass and only recompute the layout of the view you are trying to animate on the screen. Examples of this are parallax effects, dragging and moving elements around the screen, or animating the opacity of a view.

Picture what would happen if you tried to animate the width or height of a view instead. Since most of the elements are normally positioned relative to each other, the layout engine would also have to recompute the new dimensions and positions of all the other elements on the screen that are affected by the change.

Layout Animations

Layout animations refer to changes to the overall layout and structure of the interface, often involving multiple elements and requiring more coordination and planning than individual animations. They definitely play a larger role in shaping the overall user experience by improving the flow between interface elements, the aesthetic of the transitions, and the overall feel of the app. This is going to be the focus of this article.

How did everyone do it before?

React Native introduced the LayoutAnimation API in 2016, as part of v0.26, which was a great step forward in terms of layout animations.

The LayoutAnimation module allows you to animate the changes in the layout of all components that are being updated, such as their size or position, in a coordinated way. The API provides a simple way to define the animation that should occur and automatically handles the animation process for you.

Let's illustrate it with an example, where a dynamic list of boxes is rendered.

Initially, the list is empty, and you can add boxes to the list by pressing a button. Once the number of boxes reaches 5, you can remove them one by one until there are no more boxes left. Then the process repeats.

We'll use the LayoutAnimation API to define an animation that should occur when boxes are added or removed from the list.

Note that in order to get this to work on Android you need to enable the setLayoutAnimationEnabledExperimental flag via UIManager:

import React, { useState, useEffect } from "react"; import { View, StyleSheet, LayoutAnimation, Button, UIManager, Platform, } from "react-native"; if ( Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental ) { UIManager.setLayoutAnimationEnabledExperimental(true); } const colors = ["blue", "green", "orange", "purple", "red"]; const App = () => { const [count, setCount] = useState(0); const [sign, setSign] = useState(1); useEffect(() => { if (count === 5) { setSign(-1); } else if (count === 0) { setSign(1); } }, [count]); const handleAdd = () => { LayoutAnimation.configureNext({ duration: 500, create: { type: "linear", property: "opacity" }, update: { type: LayoutAnimation.Types.spring, springDamping: 0.7, }, delete: { type: "linear", property: "opacity", }, }); setCount((c) => c + 1 * sign); }; return ( <View style={styles.container}> {Array.from({ length: count }) .map((v, i) => ( <View style={[ styles.item, { backgroundColor: colors[i % colors.length] }, ]} key={i} /> )) .reverse()} <View style={styles.buttonContainer}> <Button title={sign === 1 ? "Add item" : "Remove item"} onPress={handleAdd} /> </View> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, alignItems: "center", paddingVertical: 64, }, item: { backgroundColor: "tomato", height: 32, width: 200, marginBottom: 16, }, buttonContainer: { position: "absolute", backgroundColor: "white", bottom: 16, left: 64, right: 64, }, }); export default App;
import React, { useState, useEffect } from "react"; import { View, StyleSheet, LayoutAnimation, Button, UIManager, Platform, } from "react-native"; if ( Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental ) { UIManager.setLayoutAnimationEnabledExperimental(true);} const colors = ["blue", "green", "orange", "purple", "red"]; const App = () => { const [count, setCount] = useState(0); const [sign, setSign] = useState(1); useEffect(() => { if (count === 5) { setSign(-1); } else if (count === 0) { setSign(1); } }, [count]); const handleAdd = () => { LayoutAnimation.configureNext({ duration: 500, create: { type: "linear", property: "opacity" }, update: { type: LayoutAnimation.Types.spring, springDamping: 0.7, }, delete: { type: "linear", property: "opacity", }, }); setCount((c) => c + 1 * sign); }; return ( <View style={styles.container}> {Array.from({ length: count }) .map((v, i) => ( <View style={[ styles.item, { backgroundColor: colors[i % colors.length] }, ]} key={i} /> )) .reverse()} <View style={styles.buttonContainer}> <Button title={sign === 1 ? "Add item" : "Remove item"} onPress={handleAdd} /> </View> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, alignItems: "center", paddingVertical: 64, }, item: { backgroundColor: "tomato", height: 32, width: 200, marginBottom: 16, }, buttonContainer: { position: "absolute", backgroundColor: "white", bottom: 16, left: 64, right: 64, }, }); export default App;

Notice how the LayoutAnimation.configureNext function is invoked before the state setter to define the animation that should occur. This placement is important, as it ensures that the animation is applied to the next layout change.

LayoutAnimation.configureNext({ duration: 500, create: { type: "linear", property: "opacity" }, update: { type: LayoutAnimation.Types.spring, springDamping: 0.7, }, delete: { type: "linear", property: "opacity", }, }); // Always call configureNext before updating the state. setCount((c) => c + 1 * sign);
LayoutAnimation.configureNext({ duration: 500, create: { type: "linear", property: "opacity" }, update: { type: LayoutAnimation.Types.spring, springDamping: 0.7, }, delete: { type: "linear", property: "opacity", }, }); // Always call configureNext before updating the state. setCount((c) => c + 1 * sign);

The parameters passed to configureNext define an animation that will last for 500ms, and will be applied to the creation, update, and deletion of the elements.

  • New views that are added to the list will fade in
  • Existing views will slide up and down to their new positions with a spring animation
  • Views that are removed will fade out

That all looks great and dancey, but there are a few problems to consider:

  1. The API is imperative and not declarative. You have to call LayoutAnimation.configureNext every time you want to animate a layout change
  2. It's a global API, which means that it affects all the layout animations on your screen, not just the ones you want to animate. If you just wanted to animate a specific set of views, you wouldn't be able to do that.
  3. Limited customization for entering and exiting animations, meaning when new nodes are added or removed from the view hierarchy. In those scenarios you can only animate the next properties: opacity, scaleX, scaleY and scaleXY. There is no possibility to animate the position of the view that's entering or exiting, for instance 😔
  4. Experimental on Android (as stated before), which could cause unpredictable behavior, specially on older devices.

Reanimated Transition API

Reanimated v1 introduced the Transition API, an experimental API which served the purpose of animating between two states of the view hierarchy. It was conceptually similar to the LayoutAnimation from React Native, but giving you much better control of what and how was going to be animated.

It used a component model to define the animations, making it declarative, more flexible and being able to easily scope the views that needed to be animated.

This was a great step forward, but it was still not perfect. The API was still experimental and with the release of Reanimated v2, it was quickly deprecated.

The new way: Reanimated v2/v3 Layout Animations

The reanimated team has been working hard to improve the layout animations API and make it more flexible and powerful. After iterating on the API for a while, they have finally released the new Layout Animations API in Reanimated v2 and v3 🤩

What if I told you that you can animate all layout changes for a specific view by just adding a single property to the view? That's right, as simple as that. And to top it all off, it's fully customizable.

Let's see it in action.

For that, I am going to showcase the same example as before but using reanimated v2/v3.

import React, { useState, useEffect } from "react"; import { View, StyleSheet, Button } from "react-native"; import Animated, { Layout, SlideInLeft, SlideOutRight, } from "react-native-reanimated"; const colors = ["blue", "green", "orange", "purple", "red"]; const App = () => { const [count, setCount] = useState(0); const [sign, setSign] = useState(1); useEffect(() => { if (count === 5) { setSign(-1); } else if (count === 0) { setSign(1); } }, [count]); const handleAdd = () => { setCount((c) => c + 1 * sign); }; return ( <View style={styles.container}> {Array.from({ length: count }) .map((v, i) => ( <Animated.View entering={SlideInLeft} exiting={SlideOutRight} layout={Layout} style={[ styles.item, { backgroundColor: colors[i % colors.length] }, ]} key={i} /> )) .reverse()} <View style={styles.buttonContainer}> <Button title={sign === 1 ? "Add item" : "Remove item"} onPress={handleAdd} /> </View> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, alignItems: "center", paddingVertical: 64, }, item: { backgroundColor: "tomato", height: 32, width: 200, marginBottom: 16, }, buttonContainer: { position: "absolute", backgroundColor: "white", bottom: 16, left: 64, right: 64, }, }); export default App;
import React, { useState, useEffect } from "react"; import { View, StyleSheet, Button } from "react-native"; import Animated, { Layout, SlideInLeft, SlideOutRight,} from "react-native-reanimated"; const colors = ["blue", "green", "orange", "purple", "red"]; const App = () => { const [count, setCount] = useState(0); const [sign, setSign] = useState(1); useEffect(() => { if (count === 5) { setSign(-1); } else if (count === 0) { setSign(1); } }, [count]); const handleAdd = () => { setCount((c) => c + 1 * sign); }; return ( <View style={styles.container}> {Array.from({ length: count }) .map((v, i) => ( <Animated.View entering={SlideInLeft} exiting={SlideOutRight} layout={Layout} style={[ styles.item, { backgroundColor: colors[i % colors.length] }, ]} key={i} /> )) .reverse()} <View style={styles.buttonContainer}> <Button title={sign === 1 ? "Add item" : "Remove item"} onPress={handleAdd} /> </View> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, alignItems: "center", paddingVertical: 64, }, item: { backgroundColor: "tomato", height: 32, width: 200, marginBottom: 16, }, buttonContainer: { position: "absolute", backgroundColor: "white", bottom: 16, left: 64, right: 64, }, }); export default App;

If you have spotted it, the code is much shorter and simpler than before. The only thing that changed is the addition of the entering, exiting and layout props to the Animated.View component.

This is great cause it hooks into the declarative React model, using just props to define the animations.

Those properties allow you to specify the animations that should be applied to the view when it is entering (mounted), exiting (unmounted) or being updated on the view hierarchy.

SlideInLeft, SlideOutRight and Layout are just some of the predefined animations that Reanimated offers out of the box.

  • SlideInLeft defines an entry animation based on a horizontal moving of an object, from the left to the right.
  • SlideOutRight defines an exit animation based on a horizontal moving of an object, from left to the right.
  • Layout defines a linear transition and animates both position and dimension in the same way.

All predefined animations can be customised via a builder pattern. For example, if you wished to add a small delay to the start of the SlideInLeft animation and change its duration, you could do it like this:

<Animated.View entering={SlideInLeft.delay(200).duration(1000)} exiting={SlideOutRight} layout={Layout} />
<Animated.View entering={SlideInLeft.delay(200).duration(1000)} exiting={SlideOutRight} layout={Layout} />

And, as opposed to LayoutAnimation, there are around 100 presets for you ready to use! You can find more information about those presets in the official reanimated documentation.

Conclusion

🎉 Woohoo! If you've stuck around till here, big props to you! You've now got a decent grasp on jazzing up your React Native layouts with some cool animations, along with the essential know-how to pull it off.

But hey, let's not kid ourselves, you've barely nicked the surface of the animation iceberg. What if none of the predefined animations work for your use-case?

In part 2 of this journey, I'll dive deep into the realm of crafting your very own, tailor-made animations, by showcasing how to build a snazzy counter like the one below 😉

Happy coding! ❤️

Raul Gomez

Written by Raúl Gómez Acuña

Raúl is a Product Engineer based in Madrid, Spain. He specialises in building mobile apps with React Native and TypeScript, while also sharing all his knowledge and experiences through the React Native University platform.

Back to Blog

* We aim to release new free content once a week. Be the first one to know!

React Native University • © 2024