Building a Gallery Carousel in React Native using Reanimated — I
Well, hello everyone!
In this blog, we are going to build a simple Carousel Component in React Native.
Inspired by one of the Tweets, my thought went like, “Can it be done in React Native?”. Well, I think many other developers would have done it. This blog is just my take on how I build it.
Take a look at the Interaction we will be building.
Now, seen that, let's get coding.
React Native Project Setup
You can use React Native’s built-in command line interface to generate a new project.
npx react-native@latest init AwesomeProject
The packages needed to get the Interaction done are only React Native Reanimated and Gesture Handler libraries.
For styling, I will use the twrnc
package, an API for Tailwind CSS with React Native.
npm install --save twrnc
As we will be using SVG in our project, we need to install them as well
yarn add react-native-svg
// And Linking
cd ios && pod install
Furthermore, install react-native-reanimated
and react-native-gesture-handler
libraries.
yarn add react-native-reanimated react-native-gesture-handler
Get detailed instructions for reanimated here and gesture handler here.
That’s it for dependencies.
Let us run our React Native app now.
// For iOS Simulator
npx react-native run-ios
// For Android, make sure you have an Android Emulator running
npx react-native run-android
That’s all for setting up. 🎉
Setting up with required Assets, Constants, and Data
import { ArrowLeft, ArrowRight } from "../icons";
const CARD_WIDTH = 300;
const SCREEN_WIDTH = Dimensions.get("screen").width;
const carouselItems = [
{
image:
"https://images.unsplash.com/photo-1678436748951-ef6d381e7a25?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHx0b3BpYy1mZWVkfDN8YWV1NnJMLWo2ZXd8fGVufDB8fHx8fA%3D%3D",
ar: 0.7,
},
{
image:
"https://images.unsplash.com/photo-1680813977591-5518e78445a0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
ar: 0.67,
},
{
image:
"https://images.unsplash.com/photo-1679508056887-5c76269dad54?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
ar: 0.8,
},
{
image:
"https://images.unsplash.com/photo-1681243303374-72d01f749dfa?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHx0b3BpYy1mZWVkfDczfGFldTZyTC1qNmV3fHxlbnwwfHx8fHw%3D",
ar: 0.68,
},
{
image:
"https://images.unsplash.com/photo-1675185741953-18b60234cb79?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
ar: 0.67,
},
{
image:
"https://images.unsplash.com/photo-1685725083464-26cab8f2da1e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
ar: 0.67,
},
];
We would set the Width of a single card to 300px, you can change this based on your requirements. And create a variable SCREEN_WIDTH
set it to the device width.
We need a set of images with its Aspect Ratio to place it properly inside our card and is stretched or shrunk without getting a deformed image.
The icons are SVG icons taken from https://heroicons.com/
import Svg, { Path } from "react-native-svg";
type ArrowLeftProps = {
stroke?: string;
};
export const ArrowLeft = ({ stroke = "black" }: ArrowLeftProps) => {
return (
<Svg
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke={stroke}
height={24}
width={24}
>
<Path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75"
/>
</Svg>
);
};
export const ArrowRight = ({ stroke = "black" }: ArrowRightProps) => {
return (
<Svg
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke={stroke}
height={24}
width={24}
>
<Path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75"
/>
</Svg>
);
};
Creating a Simple ScrollView
with Images
Before creating the ScrollView
, let us define some variables which will be useful to manage behavior and state for our Interaction.
const scrollXOffset = useSharedValue(0);
const [isFirstCard, setIsFirstCard] = useState(scrollXOffset.value === 0);
const [isLastCard, setIsLastCard] = useState(false);
const [scrollViewWidth, setScrollViewWidth] = useState(0);
const scrollRef = useRef<Animated.ScrollView>(null);
The first is the scrollXOffset
SharedValue
variable, from the Reanimated library, created using useSharedValuethe
hook. It is used to track the current horizontal scroll position.
The next two are the React useState
variables, isFirstCard
and isLastCard
used to check if the scroll position is at first card or has reached the end of the carousel.
The isFirstCard
is initialized based on the initial value of scrollXOffset.value
, which when is 0 is set to true.
Next is the scrollViewWidth
, which is used to store the width of the entire ScrollView
component. This is used to calculate the final position of the Carousel.
Finally, scrollRef
is a variable to hold the reference to the Animated.ScrollView
component. It is used to scroll the view programmatically.
Coding the Component Render
Our component render will look something like this:
return (
<SafeAreaView style={tailwind.style("flex-1 justify-center bg-white")}>
<Animated.View style={tailwind.style("overflow-visible")}>
<Animated.ScrollView
ref={scrollRef}
horizontal
onScroll={scrollHandler}
showsHorizontalScrollIndicator={false}
snapToInterval={CARD_WIDTH}
scrollEventThrottle={16}
decelerationRate={0}
style={tailwind.style("overflow-visible")}
contentContainerStyle={tailwind.style("overflow-visible pl-4 pr-4")}
onContentSizeChange={handleContentSizeChange}
>
{carouselItems.map((item, index) => (
<GalleryCarouselItem
key={index}
{...{
item,
index,
scrollXOffset,
handleCarouselItemPress,
scrollViewWidth,
}}
/>
))}
</Animated.ScrollView>
<Animated.View style={tailwind.style("px-4 flex flex-row pt-5")}>
<Pressable
disabled={isFirstCard}
onPress={goToPrevious}
style={({ pressed }) =>
tailwind.style(
pressed ? "bg-gray-100 rounded-xl" : "",
"mr-2 p-2",
)
}
>
<Animated.View style={tailwind.style("")}>
<ArrowLeft
stroke={isFirstCard ? tailwind.color("bg-gray-400") : "black"}
/>
</Animated.View>
</Pressable>
<Pressable
disabled={isLastCard}
onPress={goToNext}
style={({ pressed }) =>
tailwind.style(
pressed ? "bg-gray-100 rounded-xl" : "",
"mr-2 p-2",
)
}
>
<Animated.View style={tailwind.style("")}>
<ArrowRight
stroke={isLastCard ? tailwind.color("bg-gray-400") : "black"}
/>
</Animated.View>
</Pressable>
</Animated.View>
</Animated.View>
</SafeAreaView>
);
The above code implements a simple Horizontal Carousel with Navigation controls, the ArrowLeft
(previous) and ArrowRight
(next).
We wrap our whole component inside <SafeAreaView />
which ensures that the content displayed are withing safe areas of the screen, accounting for notches.
Next comes an outermost <Animated.View>
which wrap the entire carousel(ScrollView
) with the navigation controls.
The Animated.ScrollView
is the core component of our horizontal carousel. First we use the ref prop, set it to scrollRef
for making manual operations on the View.
The ScrollView
renders a set of carousel items, using the map function. Each array item is rendered using the <GalleryCarouselItem />
, and we pass required props to the component.
Next comes a onScroll
which is used to handle scroll events, we create a scrollHandler
using useAnimatedScrollHandler()
from the Reanimated library.
To set the size of the full scroll view, we use the onContentSizeChange
prop, create a function and set the value.
const handleContentSizeChange = (width: number, _height: number) => {
setScrollViewWidth(width);
};
We will have to set the snap intervals for the Carousel, which is set to the CARD_WIDTH
value, set the scrollEventThrottle
to 16, and decelerationRate
to 0.
Next is the pair of Navigation Controls, including the ArrowLeft
and ArrowRight
icons, which are used for navigating the carousel.
The two buttons are implemented using the Pressable
component from React Native, and are conditionally disabled based on the two variables we have declared before, isFirstCard
and isLastCard
variables.
When interacted, there are a couple of function definitions which are called, the goToPrevious
and goToNext
functions.
With all this in place, we would have something like this:
You can see the navigation controls gets disabled based on the position of the carousel.
That is it for this blog, in the next blog we will see how to tie the scroll to the Card Component, animate the style based on the scrollXOffset
and the definition of Navigation Control functions.