
In React Native, there are many different ways to go about displaying a collection of images in a gallery view, and a carousel is one of the most popular methods to achieve this. Using an open-source library like react-native-swiper, or something a bit more advanced like react-native-snap-carousel, provides us with the tools we need to create it, but what if you want to create a custom gallery view with additional functionality?
In this tutorial, we're going to create a custom gallery of images using react-native-snap-carousel
and FlatList
from React Native, and the open-source library is going to display each image in a carousel view. The FlatList
is what we will use to display the thumbnail view for each image below the carousel. The construction of the syncing part between the two is to add a functionality such that when an image in the carousel is scrolled either left or right, the thumb in the FlatList
is also going to be scrolled along with it. Of course, to achieve this synchronization between the two, we will use React Hooks such that you will be able to implement such a pattern in your own React Native apps.
To get the most out of this tutorial, you'll want to familiarize yourself with JavaScript/ES6 and meet the following requirements in your local dev environment:
To begin this tutorial, we want to set up a new React Native project and install all of the dependencies that are required to implement our customized carousel image gallery. Open up a terminal window and run each command as mentioned in the following order:
npx react-native init rnPreviewImageGallery
cd rnPreviewImageGallery
yarn add react-native-snap-carousel
I prefer to use react-native-snap-carousel
because it does not require the configuration of any additional steps in order to be used on native devices. Plus, it offers different layouts to configure the carousel view, right out of the box.
After installing the dependencies, let's bring in the image assets we'd like to use. For this example, I'm using some free, high-res images of Amsterdam pulled from Unsplash. If you'd like to follow along using the same images, you can find them stored here in the GitHub repo for this tutorial.
After setting up the source images we're going to use, open up the App.js
file and initiate it with a title of the screen you'd like to display. Import the following statements, then create an IMAGES
object by importing each image using Common JS require statements.
Using the useState
React hook, create an array of images called images
with each image having a unique id
in order to help you differentiate between each object in the array.
const [images, setImages] = useState([]);
You'll notice that the useState
hook returns two values in an array. The first value is the current value of the state object, and the second value in the array is the function to update the state value of the first. This why the second value starts with a conventional prefix of a set. You can technically name it anything, but following conventions that are commonly used in the React world is a good practice to follow.
Additionally, we should define some constants that will be used throughout this tutorial so that the overall spacing between each thumbnail and the width and height of each thumbnail can be represented in the FlatList
.
To set up the carousel view of an image for different screen sizes, we're going to use the Dimensions
API from React Native.
Add the following code snippet to App.js
and make sure to define state variables at the top of the App
function. Hooks are always called at the top level of a functional component in React. When defining a state, they should be the first thing in the function, especially before returning a JSX.
import React, { useState, useRef } from 'react';
import {
TouchableOpacity,
View,
Text,
Image,
FlatList,
Dimensions
} from 'react-native';
const { width } = Dimensions.get('window');
const SPACING = 10;
const THUMB_SIZE = 80;
const IMAGES = {
image1: require('./assets/images/1.jpeg'),
image2: require('./assets/images/2.jpeg'),
image3: require('./assets/images/3.jpeg'),
image4: require('./assets/images/4.jpeg'),
image5: require('./assets/images/5.jpeg'),
image6: require('./assets/images/6.jpeg'),
image7: require('./assets/images/7.jpeg')
};
const App = () => {
const [images, setImages] = useState([
{ id: '1', image: IMAGES.image1 },
{ id: '2', image: IMAGES.image2 },
{ id: '3', image: IMAGES.image3 },
{ id: '4', image: IMAGES.image4 },
{ id: '5', image: IMAGES.image5 },
{ id: '6', image: IMAGES.image6 },
{ id: '7', image: IMAGES.image7 }
]);
return (
<View style={{ flex: 1, backgroundColor: 'black', alignItems: 'center' }}>
<Text
style={{
color: 'white',
fontSize: 32,
marginTop: 50,
marginBottom: 25
}}
>
Custom Gallery
</Text>
{/* Carousel View */}
{/* Thumbnail component using FlatList */}
</View>
);
};
export default App;
To initiate the development server for iOS, execute the command npx react-native run-ios
from a terminal window. Similarly, the build command for Android is npx react-native run-android
.
Here is the app running after this step on an iOS simulator:
The component library, react-native-snap-carousel
, has a vast API containing properties and different layout patterns that are plug-n-use, and it also allows you to implement custom interpolations and animations. You can find more information on how to customize it in the official documentation, here.
For our purposes though, let's stick to the default layout pattern to display a carousel. To create a carousel view, import the component from react-native-snap-carousel
by adding the following import statement in the App.js
file. Let's also import the Pagination
component offered separately by this library to display the dot indicator.
// after other import statements
import Carousel, { Pagination } from 'react-native-snap-carousel';
Next, add a View
component after the title in the App.js
file. It is going to wrap the Carousel
component, which takes a set of required props to work:
data
, the array of images
or items to loop.layout
to define the way images are rendered and animated. For this example, we will use the default
value.sliderWidth
to define the width in pixels for the carousel container.itemWidth
to define the width in pixels for each item rendered inside the carousel.renderItem
takes an image item from the data
array and renders it as a list. To render the image, the Image
component from React Native is used.Then, add the following code snippet in App.js
to see the carousel in action:
return (
<View style={{ flex: 1, backgroundColor: 'black', alignItems: 'center' }}>
{/* Title JSX Remains same */}
{/* Carousel View */}
<View style={{ flex: 1 / 2, marginTop: 20 }}>
<Carousel
layout='default'
data={images}
sliderWidth={width}
itemWidth={width}
renderItem={({ item, index }) => (
<Image
key={index}
style={{ width: '100%', height: '100%' }}
resizeMode='contain'
source={item.image}
/>
)}
/>
</View>
</View>
);
In the simulator, you should see the following result:
The Pagination
component from the react-native-snap-carousel
is what we'll be using to display a dot indicator for our carousel, and it requires the following props:
activeDotIndex
to represent the current image shown in the carousel.dotsLength
to calculate how many dots to display based on the number of items or images in the carousel.inactiveDotColor
to display the dot indicator color when it is inactive.dotColor
to display the dot indicator color when it is active.inactiveDotScale
is used to set the value to scale the dot indicator when it's inactive.animatedDuration
is used to control the length of dot animation in milliseconds. The default value for it is 250
. If you'd like to change the default value to something else, this is the prop you'll want to use.Add the following code snippet after the Carousel
component in App.js
file:
<View>
{/* Carousel Component code remains same */}
<Pagination
inactiveDotColor='gray'
dotColor={'orange'}
activeDotIndex={indexSelected}
dotsLength={images.length}
animatedDuration={150}
inactiveDotScale={1}
/>
</View>
The value of activeDotIndex
is calculated based on the current index of the image item. Let's add a state variable called indexSelected
in the App
component with a value of zero. It is going to update when the index value of the current image changes. The initial value of this state variable is going to be 0
. Next, create a handler method called onSelect()
—this will update the value of the current index.
Then, add the following code snippet before rendering the JSX in App
component:
const App = () => {
// code remains same
const [indexSelected, setIndexSelected] = useState(0);
const onSelect = indexSelected => {
setIndexSelected(indexSelected);
};
};
Now, add a prop to the Carousel
component called onSnapToItem
. It accepts a callback as a value, and this callback is fired every time the index of the image item changes. In other words, every time the user swipes to the next image. The only argument passed to this callback is the current index
of the item, which is updated with the help of the onSelect()
handler method.
<Carousel
// rest remains same
onSnapToItem={index => onSelect(index)}
/>
If we head back to the simulator, you should see that the dot indicator syncs with the Carousel item now.
Let's add another view component below the View
that wraps the carousel to display the total number of images and the current image index number.
// Carousel View
<View
style={{
marginTop: 20,
paddingHorizontal: 32,
alignSelf: 'flex-end'
}}
>
<Text
style={{
color: 'white',
fontSize: 22
}}
>
{indexSelected + 1}/{images.length}
</Text>
</View>
Here is the result after implementing this step:
Awesome! The configuration for the Carousel component is officially complete. Now, let's dive into how to sync it with a custom FlatList component in the next section.
Let's display a list of thumbnails using FlatList
from React Native using the same array of images
from the state variable. This list is going to be displayed at the bottom of the device's screen and is a horizontal list. To achieve this, let's use the position: absolute
style property with a bottom
of value 80
.
Each thumbnail is composed of an Image
component, and they have the width and height of the THUMB_SIZE
variable we declared earlier. To show the selected thumbnail or the current thumbnail using a ternary operator, let's manipulate the style properties borderWidth
and borderColor
on this Image
component.
It is going to be wrapped by a TouchableOpacity
component because its onPress
prop is going to fire a handler method, we have yet to create, that allows a user to change the selected image with a tap.
To achieve this, add the following code snippet after Carousel's View:
<FlatList
horizontal={true}
data={images}
style={{ position: 'absolute', bottom: 80 }}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
paddingHorizontal: SPACING
}}
keyExtractor={item => item.id}
renderItem={({ item, index }) => (
<TouchableOpacity activeOpacity={0.9}>
<Image
style={{
width: THUMB_SIZE,
height: THUMB_SIZE,
marginRight: SPACING,
borderRadius: 16,
borderWidth: index === indexSelected ? 4 : 0.75,
borderColor: index === indexSelected ? 'orange' : 'white'
}}
source={item.image}
/>
</TouchableOpacity>
)}
/>
The list of thumbnails renders as shown below:
In this mockup image, you will see that the first image is selected. You cannot change the currently selected image yet in the FlatList.
FlatList
The basic element that is going to allow us to sync the image change between both the Carousel view and the thumbnail is a React hook called useRef
.
useRef
is a function that returns a mutable ref object whose current
property can be initialized to keep track of the current index value for each image, and the index value here is the image selected. Initially, it will be the first thumbnail and the first image shown in the carousel.
First, let's create a ref to serve as the reference of the current image from Carousel
component and add it to the App.js
file:
const App = () => {
const carouselRef = useRef();
// ...
};
Since the Carousel
component keeps track of the change of the current index of the image component by triggering a callback called snapToItem()
, we can use it to sync with the FlatList
.
Start by adding a handler method called onTouchThumbnail()
after defining the ref. It accepts one argument called touched
which is the index value of the current image selected from the TouchableOpacity
component or Carousel. If the value of the argument touched
and indexSelected
is the same, do nothing. Otherwise, when the value of the touched
or indexSelected
updates, change the current image in the Carousel
and the FlatList
at the same time.
const onTouchThumbnail = touched => {
if (touched === indexSelected) return;
carouselRef?.current?.snapToItem(touched);
};
Add the ref
prop on Carousel
component:
<Carousel
ref={carouselRef}
//...
/>
Next, add an onPress
prop on the TouchableOpacity
component:
<TouchableOpacity
onPress={() => onTouchThumbnail(index)}
activeOpacity={0.9}
>
Here is the output after this step:
The selection sync itself works, but do you notice the problem with the FlatList
component? Right now, it doesn't scroll on its own when an image from the Carousel is selected that is not in the current view on the screen, but we can fix this!
scrollToOffset
To start, create a new ref called flatListRef
in App.js
and add the ref prop to FlatList
component:
const App = () => {
// ...
const flatListRef = useRef();
return (
// ...
<FlatList
ref={flatListRef}
// rest remains same
/>
);
};
The scrollToOffset method available on FlatList
can be used to scroll the thumbnails to a certain offset, and it accepts two arguments. The first is called offset
which accepts a number as a value. The second argument is the animated
property which determines whether the scroll should be animated or not.
The value for the offset
is going to be indexSelected
of the thumbnail multiplied by the size of the thumbnail. Let's also set the value of animated
to true.
Since the FlatList
has to scroll on every selection, let's add mutate to the ref inside the handler method, onSelect()
.
const onSelect = indexSelected => {
setIndexSelected(indexSelected);
flatListRef?.current?.scrollToOffset({
offset: indexSelected * THUMB_SIZE,
animated: true
});
};
Here is the output after this step:
In this tutorial, we discussed only one scenario for creating a custom image gallery with FlatList, but there are so many different customization options to look into further. The main objective here is to get familiar with the use of react-native-snap-carousel, useRef hook, and the scrollToOffset
method in FlatList.
If you're looking to build a scalable cross-platform app, Crowdbotics is here to help! Our expert PMs and developers can convert your website into a native app or vice-versa or build a brand new universal app from scratch. Get in touch with us today for a detailed quote and timeline!
Originally published:
May 12, 2021