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;
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.
.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
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:
- The API is imperative and not declarative. You have to call
LayoutAnimation.configureNext
every time you want to animate a layout change - 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.
- 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
andscaleXY
. There is no possibility to animate the position of the view that's entering or exiting, for instance 😔 - 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;
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}
/>
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! ❤️