Cours React Native · Module 6: Photos, appareil photo et son
Projet : Galerie d'Atlantis
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.