In today's digital world, images play a crucial role in enhancing user experience. Imagine an elegant image carousel where users can not only swipe through images but also preview, rotate, and navigate seamlessly with pagination and text indicators! π
In this guide, weβll build an interactive image viewer in React Native with the ability to open images in full-screen preview, swipe through them horizontally, track their position with pagination, and even rotate them! ππ₯
π Features at a Glance
β
Horizontal scrolling gallery
β
Full-screen image preview
β
Smooth swipe navigation with pagination
β
Image rotation support
β
Text indicator for image position (e.g., 1/5, 2/5, etc.)
Letβs dive in and bring this magic to life! β¨
π Setting Up the Image Gallery
First, letβs create our main ImageView component, which displays a list of images in a horizontal scrollable FlatList. When a user taps an image, it opens in full-screen preview. πΌοΈ
π ImageView.js
import {
FlatList,
Image,
Modal,
StyleSheet,
Text,
TouchableOpacity,
useWindowDimensions,
View,
} from 'react-native';
import React, {useCallback, useState, useRef} from 'react';
import {CloseIcon, RotateIcon} from '../../assets';
import PaginationView from './PaginationView';
const ImagePreview = ({flatListRef, data, selectedIndex, closePreview}) => {
const {width, height} = useWindowDimensions();
const listRef = useRef(null);
const [currentIndex, setCurrentIndex] = useState(selectedIndex || 0);
const [rotationMap, setRotationMap] = useState({});
const rotateImage = () => {
setRotationMap(prev => ({
...prev,
[currentIndex]: prev[currentIndex] === 90 ? 0 : 90,
}));
};
const renderItem = useCallback(
({item, index}) => {
const rotation = rotationMap[index] || 0;
return (
<View key={index} style={styles.imageContainer(width, height)}>
<Image
source={{uri: item}}
style={styles.image(width, height, rotation)}
/>
</View>
);
},
[rotationMap],
);
const onMomentumScrollEnd = useCallback(
event => {
const newIndex = Math.round(event.nativeEvent.contentOffset.x / width);
setCurrentIndex(newIndex);
},
[width],
);
const getItemLayout = useCallback(
(_, index) => ({
length: width,
offset: width * index,
index,
}),
[width],
);
const handlePress = newIndex => {
setCurrentIndex(newIndex);
listRef.current?.scrollToIndex({ index: newIndex, animated: true });
};
return (
<View style={styles.modalContainer}>
<TouchableOpacity style={styles.closeButton} onPress={closePreview}>
<CloseIcon />
</TouchableOpacity>
<TouchableOpacity style={styles.rotateButton} onPress={rotateImage}>
<RotateIcon />
</TouchableOpacity>
<Text style={styles.indicatorText}>
{`${currentIndex + 1} / ${data.length}`}
</Text>
<FlatList
ref={listRef}
data={data}
horizontal
pagingEnabled
scrollEnabled={data.length > 0}
keyExtractor={(_, index) => index.toString()}
initialScrollIndex={selectedIndex}
getItemLayout={getItemLayout}
onMomentumScrollEnd={onMomentumScrollEnd}
showsHorizontalScrollIndicator={false}
renderItem={renderItem}
/>
<PaginationView
data={data}
currentIndex={currentIndex}
setCurrentIndex={setCurrentIndex}
onPress={handlePress}
/>
</View>
);
};
export default ImagePreview;
const styles = StyleSheet.create({
imageContainer: (width, height) => ({
width: width - 40,
height,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 20,
}),
image: (width, height, rotation) => ({
width: rotation % 180 === 0 ? width - 40 : height - 340,
height: rotation % 180 === 0 ? height - 340 : width - 40,
resizeMode: rotation % 180 === 0 ? 'cover' : 'contain',
transform: [{rotate: `${rotation}deg`}],
borderRadius: 14,
}),
closeButton: {
position: 'absolute',
top: 70,
right: 20,
zIndex: 10,
},
rotateButton: {
position: 'absolute',
top: 70,
left: 20,
zIndex: 10,
},
modalContainer: {
fflex: 1,
backgroundColor: '#000000D0',
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
},
indicatorText: {
position: 'absolute',
top: 80,
color: '#fff',
fontWeight: '600',
fontSize: 16,
},
});
πΉ Here, FlatList is used to create a horizontal scrollable list of images.
πΉ TouchableOpacity allows tapping on images to trigger the preview.
πΉ useCallback & useMemo optimize rendering performance.
π Creating the Full-Screen Image Preview
Now, let's create the ImagePreview component, which will show the selected image in full-screen mode with the ability to rotate! π
π ImagePreview.js
import {
FlatList,
Image,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native';
import React, { useCallback, useState, useRef, useMemo } from 'react';
import { metaData } from '../../screens/CarouselBackgroundAnimation/data';
import ImagePreview from './ImagePreview';
const ImageView = () => {
const [selectedIndex, setSelectedIndex] = useState(null);
const flatListRef = useRef(null);
const handlePreview = (index = null) => setSelectedIndex(index);
const renderItem = useCallback(
({ item, index }) => (
<TouchableOpacity
style={styles.imageContainer}
onPress={() => handlePreview(index)}
activeOpacity={0.8}
>
<Image source={{ uri: item }} style={styles.imageStyle} />
</TouchableOpacity>
),
[]
);
const keyExtractor = useCallback((_, index) => index.toString(), []);
const memoizedFlatList = useMemo(
() => (
<FlatList
horizontal
data={metaData}
renderItem={renderItem}
keyExtractor={keyExtractor}
contentContainerStyle={styles.contentContainerStyle}
showsHorizontalScrollIndicator={false}
/>
),
[renderItem]
);
return (
<View style={styles.container}>
{memoizedFlatList}
{selectedIndex !== null && (
<ImagePreview
data={metaData}
flatListRef={flatListRef}
selectedIndex={selectedIndex}
closePreview={() => handlePreview(null)}
/>
)}
</View>
);
};
export default ImageView;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
imageContainer: {
alignSelf: 'center',
borderRadius: 14,
overflow: 'hidden',
},
imageStyle: {
width: 250,
height: 400,
borderRadius: 14,
},
contentContainerStyle: {
gap: 24,
paddingHorizontal: 24,
},
});
πΉ The FlatList inside the Modal allows horizontal swiping.
πΉ TouchableOpacity buttons let users close the modal or rotate the image.
πΉ Rotation state ensures each image remembers its rotation! π
π Pagination & Image Indicator
Now, let's add pagination to our image viewer, along with a text indicator to show the current image position! πΈπ
π PaginationView.js
import {FlatList, StyleSheet, TouchableOpacity} from 'react-native';
import React, {useCallback} from 'react';
const PaginationView = ({data = [], currentIndex, onPress}) => {
const renderItem = useCallback(
({_, index}) => {
return (
<TouchableOpacity
hitSlop={styles.hitSlop}
onPress={() => onPress(index)}
style={styles.container(index, currentIndex)}
/>
);
},
[currentIndex, data, onPress],
);
const keyExtractor = useCallback((_, index) => index.toString(), []);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
horizontal
contentContainerStyle={styles.contentContainerStyle}
showsHorizontalScrollIndicator={false}
scrollEnabled={false}
style={{marginBottom: 100}}
/>
);
};
export default PaginationView;
const styles = StyleSheet.create({
container: (index, currentIndex) => ({
width: index === currentIndex ? 12 : 8,
height: index === currentIndex ? 12 : 8,
backgroundColor: index === currentIndex ? '#fff' : '#888',
borderRadius: index === currentIndex ? 6 : 4,
alignSelf: 'center',
}),
contentContainerStyle: {
gap: 14,
paddingHorizontal: 10,
marginBottom: 20,
marginTop: 20,
},
hitSlop: {
top: 5,
left: 5,
right: 5,
bottom: 5,
},
});
πΉ Key Enhancements
β FlatList inside the modal allows smooth horizontal swiping.
β TouchableOpacity buttons let users navigate, close the modal, or rotate the image.
β Rotation state ensures each image remembers its rotation! π
π Wrapping Up!
We just built a fully interactive image viewer that allows users to preview, swipe, and rotate images seamlessly! π Whether youβre building an e-commerce app, a gallery, or a storytelling app, this feature will add wow factor to your user experience! π―
πΉ Next Step? Try adding pinch-to-zoom for an even richer experience! π
π₯ What do you think? Would you add this to your project? Letβs discuss in the comments! π
Top comments (0)