Hey there! Are you wondering how to make a custom animated carousel using React Native? You have come to the right place.
So, the library that we will use here is called react-native-reanimated
. It's like the default Animated library, but better.
Step 1: Understanding the logic
Imagine a carousel (or just take a look at the image below). Here we define a few constants before we dive into the code.
CARD_LENGTH - The length of the card in focus ( no 2 in the image below).
SIDECARD_LENGTH - Length of the side card that is partially visible (no 1 and 3).
SPACING - Space between the side card and the main card.
SRC_WIDTH - The width of the screen
Now that you know the layout of the carousel, all we need to do now is to make it awesome. Adding animations will help us do that, but let's not get ahead of ourselves.
As you might have noticed in the image, the card which is focused is bigger than the partially visible ones. Well, that's how our animation will be. As we scroll horizontally the side card will come into focus and gradually expand. Similarly, the main card will shrink and move to the side.
To do all this, We need to know one very important parameter, which I call "scrollX
". This will give us the magnitude that we have scrolled horizontally. (Ex. at the start, the scrollX
will be 0 and it will increase as we scroll).
Step 2: Setting up (the boring part)
create an empty react-native project -
npx react-native init AwesomeProject
NOTE - I am using TypeScript throughout this app, but JavaScript should not be much different
Install react-native-reanimated
library using -
yarn add react-native-reanimated
IMPORTANT - You have to add its plugin in the "babel.config" file. It should be the last plugin in the array. It should look like this -
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin'], // <-- ADD THIS
};
};
Step 3: Diving into the code (Finally!!)
Start by creating a functional component called "Carousel". Next, we add a simple FlatList
that can be scrolled horizontally.
<View>
<FlatList
data={DATA}
horizontal={true}
renderItem={({item, index})=>{
return(
<Item index={index} /> // discussed below
)
}}
keyExtractor={(item) => item.id}
/>
</View>
Now define the constants that we talked about in the beginning at the global level -
const SRC_WIDTH = Dimensions.get("window").width;
const CARD_LENGTH = SRC_WIDTH * 0.8;
const SPACING = SRC_WIDTH * 0.02;
const SIDECARD_LENGTH = (SRC_WIDTH * 0.18) / 2;
const DATA = [
{
id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba",
title: "First Item",
},
{
id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63",
title: "Second Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d72",
title: "Third Item",
},
];
We have our horizontal list, and the constants defined. Now let's create another component that will act as a renderItem
in our FlatList.
function Item({index} : itemProps){
return(
<View style={[styles.card, {
marginLeft: index == 0 ? SIDECARD_LENGTH : SPACING,
marginRight: index == DATA.length ? SIDECARD_LENGTH: SPACING,
}]}>
<Image
source={require("./images/img1.jpg")}
style={{width: "100%", height: "100%"}}
/>
</View>
)
}
const styles = StyleSheet.create({
card: {
width: CARD_LENGTH,
height: 150,
overflow: "hidden",
borderRadius: 15,
}
});
Note that we have given margin as SIDECARD_LENGTH
if the index is 0 or last (i.e. for the first and last item we will not have more cards on the left and right respectively, so we will have a bigger margin instead)
We should have a horizontally scrollable list with images now. (As shown above)
Now before we start animating it, we will need the "scrollX
" parameter that we discussed earlier by simply creating a state to store it.
const [scrollX, setScrollX] = useState(0);
Next, add an onScroll
parameter to the FlatList
like this -
onScroll={(event)=>{
setScrollX(event.nativeEvent.contentOffset.x);
}}
Pass this as props to the Item component -
renderItem={({item, index})=>{
return(
<Item index={index} scrollX={scrollX} />
)
}}
Now that we have everything ready, we can start animating our carousel -
This is how react-native-reanimated works, we define our component as <Animated.Component>
instead of <Component>
. So we change <View>
to <Animated.View>
, but some components like <FlatList />
are not defined in this library. So we have another method to convert it into an animated FlatList.
Add this at the top of your file -
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
Your code should look like this after converting the components into animated components.
<Animated.View style={[styles.card, {
marginLeft: index == 0 ? SIDECARD_LENGTH : SPACING,
marginRight: index == 2 ? SIDECARD_LENGTH: SPACING,
}]}>
<Image
source={require("./images/img1.jpg")}
style={{width: "100%", height: "100%"}}
/>
</Animated.View>
<Animated.View>
<AnimatedFlatList
data={DATA}
horizontal={true}
renderItem={({item, index})=>{
return(
<Item index={index} scrollX={scrollX} />
)
}}
keyExtractor={(item) => item.id}
onScroll={(event)=>{
setScrollX(event.nativeEvent.contentOffset.x);
}}
/>
</Animated.View>
Step 4: Mathematics (I know :( but stay with me)
Our next mini-goal is to change the size of the <Item />
as the scrollX
increases/decreases (ie. as we scroll forward or backward).
For that, we need a variable that stores the size of the <Item />
. We will use a hook from the reanimated library for that -
const size = useSharedValue(0.8);
We use this instead of useState
because we will animate this value in the future, and we use useSharedValue
for that.
Here comes the math part -
We will use something called interpolation
. Imagine this as a function that takes in an InputRange
and returns an outputRange
based on that.
Here is the interpolate
function from reanimated library that we will add to our Item
component -
const inputRange = [
(index -1) * CARD_LENGTH,
index * CARD_LENGTH,
(index + 1) * CARD_LENGTH
]
size.value = interpolate(
scrollX,
inputRange,
[0.8, 1, 0.8],
Extrapolate.CLAMP,
)
I know what you are thinking, but it's rather simple once you understand it.
Assume, the input range is [a,b,c].
and output range in [0.8, 1, 0.8]
So if the value falls into range a then the output will be 0.8. Similarly, if the value is between a and b, then the output will be between 0.8 and 1. That's how the interpolation function works.
In our case, we have given value as scrollX
. The first element of inputRange
is for the left sidecard, the middle one is for the main card and the last one is for the right sidecard. Therefore, if the card falls in the first or the last case, the output will be 0.8 and size of the card will appear small. And for the main card the output will be 1 and it will be of normal size.
Similarly, we do this for the opacity of the cards -
const opacity = useSharedValue(1);
const opacityInputRange = [
(index - 1) * CARD_LENGTH,
index * CARD_LENGTH,
(index + 1) * CARD_LENGTH,
];
opacity.value = interpolate(
scrollX,
opacityInputRange,
[0.5, 1, 0.5],
Extrapolate.CLAMP
);
Lastly, we add these styles to out Item
component. Since these are not normal styles, we can't add them using StyleSheet
. Instead, we will add them like this -
const cardStyle = useAnimatedStyle(()=>{
return{
transform: [{scaleY: size.value}],
opacity: opacity.value,
}
})
and then we add this cardStyle
in the style
array of our Item
.
<Animated.View style={[styles.card, cardStyle, {
marginLeft: index == 0 ? SIDECARD_LENGTH : SPACING,
marginRight: index == 2 ? SIDECARD_LENGTH: SPACING,
}]}>
<Image
source={require("./images/img1.jpg")}
style={{width: "100%", height: "100%"}}
/>
</Animated.View>
That's it!!. Just run the app and you have your custom, responsive, animated Carousel ready.
Checkout the full code here - github.com/Sardar1208/YT-animated-carousel/..
Conclusion
This is one of many methods that you can use to create a Carousel. I have animated the size and opacity of the cards, but you can add whatever effect you like in the same way. Read react-native-reanimated
docs to learn more cool stuff you can do with it.
I also have a video on this if you want a more step-by-step approach. You can check it out here.
Thank you for reading this.
#WeMakeDevs