Overview
React Native has a FlatList
component that takes a list of items, and then smartly renders an element for each item in the list. At first glance, it seems like this component is just for rendering large lists of elements. It turns out, React Native FlatList
s can do a ton of cool stuff! In this post, we'll look at one of those things: building a carousel that has a fancy tilt effect. See the sample below.
You can find the source code for this example here.
A Little Setup
Let's start with a little setup. We'll create a list of data that we'll use to render our carousel slides. We'll also use the Dimensions
API from React Native to get the width and height of the screen.
import { Dimensions } from "react-native";
// Set up our slides
type Slide = {
image: any;
title: string;
subtitle: string;
};
const SLIDES: Slide[] = [
{
image: require("./img/summer.jpg"),
title: "Summer",
subtitle: "Warm days, fun nights.",
},
{
image: require("./img/fall.jpg"),
title: "Fall",
subtitle: "Sweater weather, baby.",
},
{
image: require("./img/winter.jpg"),
title: "Winter",
subtitle: "The season to be jolly.",
},
{
image: require("./img/spring.jpg"),
title: "Spring",
subtitle: "April showers, may flowers.",
},
];
// Utils
const { width, height } = Dimensions.get("window");
Building the Carousel
Now that we have our data setup, let's go ahead and put together the bones of our carousel. This will consist of a FlatList
that will take in our SLIDES
data, and a custom component CarouselSlide
that is used to render each slide of the carousel. There's a styles
object that holds styles for the carousel, but let's not worry about those details right now.
/**
* Tilt Carousel View
*/
export const TiltCarousel: React.FC = () => {
return (
<View style={styles.background}>
<FlatList
data={SLIDES}
renderItem={({ item, index }: { item: Slide; index: number }) => (
<CarouselSlide slide={item} />
)}
horizontal
snapToInterval={width}
showsHorizontalScrollIndicator={false}
scrollEventThrottle={16}
decelerationRate={-1}
bounces={true}
keyExtractor={(slide: Slide) => slide.title}
/>
</View>
);
};
/**
* Slide item
*/
const CarouselSlide: React.FC<{
slide: Slide;
}> = ({ slide }) => {
return (
<View style={styles.cardContainer}>
<ImageBackground source={slide.image} style={{ flex: 1 }}>
<Spacer height={0.7 * height} />
<View style={styles.cardContentContainer}>
<AppText style={styles.title}>{slide.title}</AppText>
<AppText style={styles.subtitle}>{slide.subtitle}</AppText>
</View>
</ImageBackground>
</View>
);
};
There are a few things to note here:
- We'll use the
horizontal
prop on theFlatList
to make our list render horizontally. - We'll set
snapToInterval={width}
so that theFlatList
"snaps" at certain intervals. Importantly: we'll snap towidth
, the width of the screen. Each carousel slide has a width ofwidth
as well, so that we can snap each carousel slide into view. - We'll use
scrollEventThrottle={16}
anddecelerationRate={-1}
to control the carousel's slide physics. - At this point, the
SlideCarousel
just renders an image background using the slide's image, and some text. I'm using customAppText
andSpacer
components that render text and space, accordingly.
At this point, we've got a pretty good start. This is what we have:
However, we're going to spice this thing up with some custom animation!
Sprinkling in Some Animation
We're going to give our carousel slides a nice "tilt" effect when they come in and out of view. To do this, we need to do a couple things:
- Keep track of the user's scroll position in the
FlatList
. - Use this scroll position to animate some transforms on each carousel slide.
Keeping Track of the User's Scroll Position.
To add animation around our FlatList
scroll, we need to create and track an Animated
value that corresponds to how far the user has scrolled along in the FlatList
. React Native has an Animated.Value
API for creating an animated value (that we can animate the value of and use in styles to created animated effects). We'll use this animated value to track how far the user has scrolled along in the list. To do this, we'll use the onScroll
prop of the FlatList
and the Animated.event
API. We'll go ahead and pass the scrollX
value on to each <CarouselSlide />
element.
export const TiltCarousel: React.FC = () => {
+ const scrollX = React.useRef(new Animated.Value(0)).current;
return (
<View style={styles.background}>
<Animated.FlatList
data={SLIDES}
renderItem={({ item, index }: { item: Slide; index: number }) => (
- <CarouselSlide slide={item} />
+ <CarouselSlide slide={item} scrollX={scrollX} index={index} />
)}
horizontal
snapToInterval={width}
showsHorizontalScrollIndicator={false}
scrollEventThrottle={16}
decelerationRate={-1}
bounces={true}
keyExtractor={(slide: Slide) => slide.title}
+ onScroll={Animated.event(
+ [{ nativeEvent: { contentOffset: { x: scrollX } } }],
+ { useNativeDriver: true },
+ )}
/>
</View>
);
};
const CarouselSlide: React.FC<{
slide: Slide;
+ scrollX: Animated.Value;
+ index: number;
-}> = ({ slide }) => {
+}> = ({ slide, scrollX, index }) => {
This change hasn't added any visual changes, but now we have access to scrollX
, which keeps track of the user's horizontal scroll position - and we'll use this to animate the carousel slides.
Animate the Carousel Slides
We're going to add a little "tilt" effect to the carousel slides.
To do this, we're going to use the value of scrollX
(how far the user has scrolled along the FlatList
) and the value of index
(the index for each slide item). We'll apply a few transformations to the carousel slide that we'll eventually animate. For now, let's just do a little setup.
const CarouselSlide: React.FC<{
slide: Slide;
scrollX: Animated.Value;
index: number;
}> = ({ slide, scrollX, index }) => {
// Transforms
const opacity = 1;
const scale = 1;
const perspective = 800;
const translateX = 0;
const rotateY = '0deg';
return (
<Animated.View
style={[
styles.cardContainer,
{
opacity,
transform: [{ scale }, { perspective }, { translateX }, { rotateY }],
},
]}
>
<ImageBackground source={slide.image} style={{ flex: 1 }}>
<Spacer height={0.7 * height} />
<View style={styles.cardContentContainer}>
<AppText style={styles.title}>{slide.title}</AppText>
<AppText style={styles.subtitle}>{slide.subtitle}</AppText>
</View>
</ImageBackground>
</Animated.View>
);
};
In the above code snippet, we're setting some base values for opacity
, scale
, perspective
, translateX
, and rotateY
- and then applying these to our slide wrapper. We also turned the View
into an Animated.View
, since we'll be turning the aforementioned property values into animated values.
Now, let's talk about these properties that we'll be animating - and why.
- We'll animate
opacity
so that as the slide comes in/out of view, we can add a bit of a "disappearing" effect. - We'll animate
scale
so that as the slide comes in/out of view, it shrinks/grows a little bit. - We'll animate
perspective
,translateX
, androtateY
to give the slide a "tilt" effect. If you swipe the card out to the left, it should "tilt" around the left side of the screen. If you swipe the card out to the right, it should "tilt" around the right side of the screen.
Let's fill these animated values in, and then look at the logic behind them.
const inputRange = [(index - 1) * width, index * width, (index + 1) * width];
const opacity = scrollX.interpolate({
inputRange,
outputRange: [0, 1, 0],
});
const scale = scrollX.interpolate({
inputRange,
outputRange: [0.6, 1, 0.6],
});
const perspective = scrollX.interpolate({
inputRange,
outputRange: [1200, 800, 1200],
});
const translateX = Animated.subtract(scrollX, index * width);
const rotateY = scrollX.interpolate({
inputRange,
outputRange: ["-45deg", "0deg", "45deg"],
});
At this point, we've created the "tilt" effect! But we need to loop back and take a look at how we constructed these animated values.
Animated Interpolation
React Native animated values have an interpolate
method that allows us to transform an animated value. This is a tremendously powerful tool! We're going to be transforming our scrollX
value. The interpolate
method takes an inputRange
and an outputRange
that defines the transformation.
In our case, we have this line:
const inputRange = [(index - 1) * width, index * width, (index + 1) * width];
We can think of this as three "breakpoints" that we'll map from relative to each slide. For example, for the first slide, we can think of this as follows:
As you swipe left, scrollX
will vary from index * width
to (index - 1) * width
. If you were to swipe right, it'd vary from index * width
to (index + 1) * width
. Therefore, we can think of this inputRange
as defining the breakpoints for when the slide is all the way out of the screen's view (to the left), when the slide is fully centered in the screen's view, and all the way out of the screen's view (to the right).
We can then think about how to transform other values based on these "breakpoints". For example, when a slide is out of the screen's view (either to the left or to the right), we want it to "disappear" by applying opacity: 0
. When the slide is in the screen's view, we want opacity: 1
. Therefore, we can define opacity
with the following transformation on scrollX
:
const opacity = scrollX.interpolate({
inputRange,
outputRange: [0, 1, 0],
});
In a similar manner, we want the slide to sort of "shrink" as it goes out of the view, so we can define:
const scale = scrollX.interpolate({
inputRange,
outputRange: [0.6, 1, 0.6],
});
We can think of this like:
- Slide is swiped out to the left?
scale: 0.6
. - Slide is fully in view?
scale: 1
. - Slide is swiped out to the right?
scale: 0.6
. - Smoothly animated everything in-between (using linear interpolation).
Transformations for the Tilt
Now all that's left to unwrap is:
const perspective = scrollX.interpolate({
inputRange,
outputRange: [1200, 800, 1200],
});
const translateX = Animated.subtract(scrollX, index * width);
const rotateY = scrollX.interpolate({
inputRange,
outputRange: ["-45deg", "0deg", "45deg"],
});
I don't want to go into boring details with this, but to create the "tilt" effect around the edges of the screen, we need to perform three consecutive transformations (and order matters!). From a high-level, we can think of these transforms in the following way:
- Use
perspective
to make it seem like the slide is being "pushed away" from the user as the slide is moved off of screen. - Use
translateX
to shift the slide left/right to determine the axis of rotation for the "tilt". - Use
rotateY
to create the rotation effect.
With these in place, we get a "tilt" effect! And that concludes this tutorial.
Conclusion
Although the whole "tilt" effect has some complicated math/transformations, I hope that the setup to these transformations is helpful and gives you the tools to start creating your own fancy animations on top of React Native FlatList
s! For the purpose of thoroughness, here's the whole code:
import * as React from "react";
import {
Animated,
Dimensions,
ImageBackground,
StyleSheet,
View,
} from "react-native";
import { AppText } from "../../components/AppText";
import { Spacer } from "../../components/Spacer";
// Set up our slides
type Slide = {
image: any;
title: string;
subtitle: string;
};
const SLIDES: Slide[] = [
{
image: require("./img/summer.jpg"),
title: "Summer",
subtitle: "Warm days, fun nights.",
},
{
image: require("./img/fall.jpg"),
title: "Fall",
subtitle: "Sweater weather, baby.",
},
{
image: require("./img/winter.jpg"),
title: "Winter",
subtitle: "The season to be jolly.",
},
{
image: require("./img/spring.jpg"),
title: "Spring",
subtitle: "April showers, may flowers.",
},
];
// Utils
const { width, height } = Dimensions.get("window");
/**
* Tilt Carousel View
*/
export const TiltCarousel: React.FC = () => {
const scrollX = React.useRef(new Animated.Value(0)).current;
return (
<View style={styles.background}>
<Animated.FlatList
data={SLIDES}
renderItem={({ item, index }: { item: Slide; index: number }) => (
<CarouselSlide slide={item} scrollX={scrollX} index={index} />
)}
horizontal
snapToInterval={width}
showsHorizontalScrollIndicator={false}
scrollEventThrottle={16}
decelerationRate={-1}
bounces={true}
keyExtractor={(slide: Slide) => slide.title}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{ useNativeDriver: true },
)}
/>
</View>
);
};
/**
* Slide item
*/
const CarouselSlide: React.FC<{
slide: Slide;
scrollX: Animated.Value;
index: number;
}> = ({ slide, scrollX, index }) => {
const inputRange = [(index - 1) * width, index * width, (index + 1) * width];
const opacity = scrollX.interpolate({
inputRange,
outputRange: [0, 1, 0],
});
const scale = scrollX.interpolate({
inputRange,
outputRange: [0.6, 1, 0.6],
});
const perspective = scrollX.interpolate({
inputRange,
outputRange: [1200, 800, 1200],
});
const translateX = Animated.subtract(scrollX, index * width);
const rotateY = scrollX.interpolate({
inputRange,
outputRange: ["-45deg", "0deg", "45deg"],
});
return (
<Animated.View
style={[
styles.cardContainer,
{
opacity,
transform: [{ scale }, { perspective }, { translateX }, { rotateY }],
},
]}
>
<ImageBackground source={slide.image} style={{ flex: 1 }}>
<Spacer height={0.7 * height} />
<View style={styles.cardContentContainer}>
<AppText style={styles.title}>{slide.title}</AppText>
<AppText style={styles.subtitle}>{slide.subtitle}</AppText>
</View>
</ImageBackground>
</Animated.View>
);
};
/**
* Styling
*/
const styles = StyleSheet.create({
background: { flex: 1, backgroundColor: "rgba(30,30,30,0.8)" },
cardContainer: {
width,
flex: 1,
justifyContent: "center",
borderRadius: 30,
overflow: "hidden",
},
cardContentContainer: {
alignItems: "center",
backgroundColor: "rgba(0,0,0,0.3)",
padding: 16,
},
title: {
color: "white",
fontWeight: "bold",
fontSize: 64,
textShadowColor: "black",
textShadowRadius: 4,
textShadowOffset: {
width: 1,
height: 1,
},
},
subtitle: {
color: "rgb(230,230,230)",
fontWeight: "600",
fontSize: 18,
},
});
Top comments (1)
Thank you for the great article!
Here is an expo snack for this demo if anyone wants to try it: snack.expo.io/@ysfu/tilt-carousel