📸 Stunning Image Preview in React Native – Swipe, Rotate, Pagination & Text indicator! 🚀
Amit Kumar

Amit Kumar @amitkumar13

About: Software Engineer

Location:
New Delhi, India
Joined:
Apr 19, 2024

📸 Stunning Image Preview in React Native – Swipe, Rotate, Pagination & Text indicator! 🚀

Publish Date: Feb 22
7 0

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! 🔄🔥

Image description

🚀 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,
  },
});


Enter fullscreen mode Exit fullscreen mode

🔹 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,
  },
});

Enter fullscreen mode Exit fullscreen mode

🔹 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,
  },
});

Enter fullscreen mode Exit fullscreen mode

🔹 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! 🚀

Comments 0 total

    Add comment