Cours React Native · Module 6: Photos, appareil photo et son

Projet : Galerie d'Atlantis

3 min de lecture ·

Combinons toutes les techniques apprises dans une galerie photo !

Galerie complète

import { useState, useEffect } from 'react';
import { View, FlatList, Pressable, StyleSheet, Alert } from 'react-native';
import { Image } from 'expo-image';
import * as ImagePicker from 'expo-image-picker';
import * as MediaLibrary from 'expo-media-library';
import * as Sharing from 'expo-sharing';
import { Ionicons } from '@expo/vector-icons';

interface Photo {
  id: string;
  uri: string;
  createdAt: Date;
}

export function PhotoGallery() {
  const [photos, setPhotos] = useState<Photo[]>([]);
  const [selectedPhoto, setSelectedPhoto] = useState<Photo | null>(null);

  // Ajouter une photo depuis la caméra
  const takePhoto = async () => {
    const { status } = await ImagePicker.requestCameraPermissionsAsync();
    if (status !== 'granted') return;

    const result = await ImagePicker.launchCameraAsync({
      quality: 0.8,
      allowsEditing: true,
    });

    if (!result.canceled) {
      const newPhoto: Photo = {
        id: Date.now().toString(),
        uri: result.assets[0].uri,
        createdAt: new Date(),
      };
      setPhotos((prev) => [newPhoto, ...prev]);
    }
  };

  // Ajouter depuis la galerie
  const pickFromGallery = async () => {
    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ['images'],
      allowsMultipleSelection: true,
      quality: 0.8,
    });

    if (!result.canceled) {
      const newPhotos = result.assets.map((asset) => ({
        id: Date.now().toString() + Math.random(),
        uri: asset.uri,
        createdAt: new Date(),
      }));
      setPhotos((prev) => [...newPhotos, ...prev]);
    }
  };

  // Partager
  const sharePhoto = async (photo: Photo) => {
    const isAvailable = await Sharing.isAvailableAsync();
    if (!isAvailable) return;

    await Sharing.shareAsync(photo.uri);
  };

  // Enregistrer dans la galerie du système
  const saveToDevice = async (photo: Photo) => {
    const { status } = await MediaLibrary.requestPermissionsAsync();
    if (status !== 'granted') return;

    await MediaLibrary.createAssetAsync(photo.uri);
    Alert.alert('Succès', 'Enregistré dans la galerie !');
  };

  // Supprimer
  const deletePhoto = (photoId: string) => {
    Alert.alert('Supprimer la photo', 'Es-tu sûr ?', [
      { text: 'Annuler', style: 'cancel' },
      {
        text: 'Supprimer',
        style: 'destructive',
        onPress: () => {
          setPhotos((prev) => prev.filter((p) => p.id !== photoId));
          setSelectedPhoto(null);
        },
      },
    ]);
  };

  // Vue détaillée
  if (selectedPhoto) {
    return (
      <View style={styles.detailContainer}>
        <Image
          source={{ uri: selectedPhoto.uri }}
          style={styles.fullImage}
          contentFit="contain"
        />
        <View style={styles.detailActions}>
          <Pressable onPress={() => sharePhoto(selectedPhoto)}>
            <Ionicons name="share-outline" size={28} color="#fff" />
          </Pressable>
          <Pressable onPress={() => saveToDevice(selectedPhoto)}>
            <Ionicons name="download-outline" size={28} color="#fff" />
          </Pressable>
          <Pressable onPress={() => deletePhoto(selectedPhoto.id)}>
            <Ionicons name="trash-outline" size={28} color="#ff6b6b" />
          </Pressable>
          <Pressable onPress={() => setSelectedPhoto(null)}>
            <Ionicons name="close" size={28} color="#fff" />
          </Pressable>
        </View>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      {/* En-tête avec actions */}
      <View style={styles.header}>
        <Pressable style={styles.addButton} onPress={takePhoto}>
          <Ionicons name="camera" size={24} color="#fff" />
        </Pressable>
        <Pressable style={styles.addButton} onPress={pickFromGallery}>
          <Ionicons name="images" size={24} color="#fff" />
        </Pressable>
      </View>

      {/* Grille de photos */}
      <FlatList
        data={photos}
        numColumns={3}
        keyExtractor={(item) => item.id}
        contentContainerStyle={styles.grid}
        renderItem={({ item }) => (
          <Pressable
            style={styles.photoItem}
            onPress={() => setSelectedPhoto(item)}
          >
            <Image
              source={{ uri: item.uri }}
              style={styles.thumbnail}
              contentFit="cover"
              transition={200}
            />
          </Pressable>
        )}
        ListEmptyComponent={
          <View style={styles.empty}>
            <Ionicons name="images-outline" size={64} color="#ccc" />
            <Text style={styles.emptyText}>Aucune photo</Text>
          </View>
        }
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0a2e4a' },
  header: { flexDirection: 'row', padding: 16, gap: 12 },
  addButton: {
    backgroundColor: '#1a5276',
    padding: 12,
    borderRadius: 8,
  },
  grid: { padding: 2 },
  photoItem: { flex: 1/3, aspectRatio: 1, padding: 2 },
  thumbnail: { flex: 1, borderRadius: 4 },
  detailContainer: { flex: 1, backgroundColor: '#000' },
  fullImage: { flex: 1 },
  detailActions: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    padding: 20,
    backgroundColor: 'rgba(0,0,0,0.8)',
  },
  empty: { alignItems: 'center', marginTop: 100 },
  emptyText: { color: '#ccc', marginTop: 16 },
});

Ce projet combine expo-image, expo-image-picker, expo-media-library et expo-sharing !

Code de cette leçon : App.tsx
import React, { useState } from 'react';
import { View, Text, FlatList, Pressable, StyleSheet } from 'react-native';
import { Image } from 'expo-image';
import * as ImagePicker from 'expo-image-picker';
import * as Sharing from 'expo-sharing';

interface Photo {
  id: string;
  uri: string;
}

export default function App() {
  const [photos, setPhotos] = useState<Photo[]>([]);

  const pickFromGallery = async () => {
    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images,
      allowsMultipleSelection: true,
      quality: 0.8,
    });

    if (!result.canceled) {
      // TODO : transforme result.assets en un tableau d'objets Photo
      //   { id: Date.now().toString() + Math.random(), uri: asset.uri }
      //   et ajoute-les à l'état : setPhotos((prev) => [...newPhotos, ...prev])

    }
  };

  const sharePhoto = async (uri: string) => {
    const isAvailable = await Sharing.isAvailableAsync();
    if (isAvailable) {
      await Sharing.shareAsync(uri);
    }
  };

  return (
    <View style={styles.container}>
      <Pressable style={styles.addButton} onPress={pickFromGallery}>
        <Text style={styles.buttonText}>Ajouter des photos</Text>
      </Pressable>

      <FlatList
        data={photos}
        numColumns={3}
        keyExtractor={(item) => item.id}
        contentContainerStyle={styles.grid}
        renderItem={({ item }) => (
          <Pressable style={styles.item} onPress={() => sharePhoto(item.uri)}>
            <Image source={{ uri: item.uri }} style={styles.thumb} contentFit="cover" transition={200} />
          </Pressable>
        )}
        ListEmptyComponent={<Text style={styles.empty}>Aucune photo</Text>}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0a2e4a' },
  addButton: { backgroundColor: '#1a5276', padding: 14, margin: 12, borderRadius: 8, alignItems: 'center' },
  buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
  grid: { padding: 2 },
  item: { flex: 1 / 3, aspectRatio: 1, padding: 2 },
  thumb: { flex: 1, borderRadius: 4 },
  empty: { color: '#ccc', textAlign: 'center', marginTop: 60 },
});

Entrées de la documentation utilisées dans cette leçon

Tu as repéré une erreur dans cette leçon ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Complète le code de la galerie en combinant toutes les techniques.

Articles utiles