React Native相机与相册 #
概述 #
移动应用中经常需要使用相机拍照或从相册选择图片。本章节介绍如何在 React Native 中实现这些功能。
权限配置 #
iOS 权限 #
在 ios/MyApp/Info.plist 中添加:
xml
<key>NSCameraUsageDescription</key>
<string>需要访问相机以拍摄照片</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册以选择照片</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要访问麦克风以录制视频</string>
Android 权限 #
在 android/app/src/main/AndroidManifest.xml 中添加:
xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
react-native-image-picker #
最常用的图片选择库。
安装 #
bash
npm install react-native-image-picker
cd ios && pod install
基本使用 #
tsx
import React, {useState} from 'react';
import {
View,
Image,
Button,
StyleSheet,
Alert,
} from 'react-native';
import {
launchCamera,
launchImageLibrary,
ImagePickerResponse,
} from 'react-native-image-picker';
const ImagePickerExample = () => {
const [image, setImage] = useState<string | null>(null);
const handleTakePhoto = async () => {
const result: ImagePickerResponse = await launchCamera({
mediaType: 'photo',
quality: 0.8,
saveToPhotos: true,
});
if (result.didCancel) {
console.log('User cancelled camera');
return;
}
if (result.errorCode) {
Alert.alert('Error', result.errorMessage || 'Unknown error');
return;
}
if (result.assets && result.assets[0]) {
setImage(result.assets[0].uri || null);
}
};
const handleChoosePhoto = async () => {
const result: ImagePickerResponse = await launchImageLibrary({
mediaType: 'photo',
quality: 0.8,
selectionLimit: 1,
});
if (result.didCancel) {
console.log('User cancelled picker');
return;
}
if (result.errorCode) {
Alert.alert('Error', result.errorMessage || 'Unknown error');
return;
}
if (result.assets && result.assets[0]) {
setImage(result.assets[0].uri || null);
}
};
return (
<View style={styles.container}>
{image && (
<Image source={{uri: image}} style={styles.image} resizeMode="cover" />
)}
<View style={styles.buttons}>
<Button title="拍照" onPress={handleTakePhoto} />
<Button title="从相册选择" onPress={handleChoosePhoto} />
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 16,
},
image: {
width: 300,
height: 300,
borderRadius: 8,
marginBottom: 20,
},
buttons: {
flexDirection: 'row',
gap: 16,
},
});
export default ImagePickerExample;
配置选项 #
tsx
const options: ImageLibraryOptions = {
mediaType: 'photo',
quality: 0.8,
maxWidth: 1024,
maxHeight: 1024,
selectionLimit: 1,
includeBase64: false,
saveToPhotos: true,
cameraType: 'back',
presentationStyle: 'fullScreen',
};
const result = await launchImageLibrary(options);
多选图片 #
tsx
const handleMultiSelect = async () => {
const result = await launchImageLibrary({
mediaType: 'photo',
selectionLimit: 5,
quality: 0.8,
});
if (result.assets) {
const uris = result.assets.map(asset => asset.uri);
setImages(uris);
}
};
录制视频 #
tsx
const handleRecordVideo = async () => {
const result = await launchCamera({
mediaType: 'video',
videoQuality: 'high',
durationLimit: 60,
saveToPhotos: true,
});
if (result.assets && result.assets[0]) {
const videoUri = result.assets[0].uri;
setVideo(videoUri);
}
};
权限请求 #
使用 react-native-permissions #
bash
npm install react-native-permissions
tsx
import {request, PERMISSIONS, RESULTS} from 'react-native-permissions';
import {Platform} from 'react-native';
const requestCameraPermission = async (): Promise<boolean> => {
const permission = Platform.select({
ios: PERMISSIONS.IOS.CAMERA,
android: PERMISSIONS.ANDROID.CAMERA,
});
if (!permission) return false;
const result = await request(permission);
return result === RESULTS.GRANTED;
};
const requestPhotoLibraryPermission = async (): Promise<boolean> => {
const permission = Platform.select({
ios: PERMISSIONS.IOS.PHOTO_LIBRARY,
android: PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE,
});
if (!permission) return false;
const result = await request(permission);
return result === RESULTS.GRANTED;
};
const handleTakePhotoWithPermission = async () => {
const hasPermission = await requestCameraPermission();
if (!hasPermission) {
Alert.alert('权限被拒绝', '请在设置中开启相机权限');
return;
}
const result = await launchCamera({
mediaType: 'photo',
quality: 0.8,
});
// 处理结果...
};
图片压缩 #
使用 react-native-image-resizer #
bash
npm install react-native-image-resizer
tsx
import ImageResizer from 'react-native-image-resizer';
const compressImage = async (uri: string): Promise<string> => {
try {
const result = await ImageResizer.createResizedImage(
uri,
1024,
1024,
'JPEG',
80,
0,
undefined,
false,
{mode: 'contain', onlyScaleDown: true},
);
return result.uri;
} catch (error) {
console.error('Image compression error:', error);
return uri;
}
};
图片上传 #
上传单张图片 #
tsx
const uploadImage = async (uri: string) => {
const formData = new FormData();
const filename = uri.split('/').pop() || 'image.jpg';
const match = /\.(\w+)$/.exec(filename);
const type = match ? `image/${match[1]}` : 'image/jpeg';
formData.append('image', {
uri: Platform.OS === 'ios' ? uri.replace('file://', '') : uri,
type,
name: filename,
});
try {
const response = await api.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: progressEvent => {
const percent = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total || 1),
);
console.log(`Upload progress: ${percent}%`);
},
});
return response.data;
} catch (error) {
console.error('Upload error:', error);
throw error;
}
};
上传多张图片 #
tsx
const uploadMultipleImages = async (uris: string[]) => {
const formData = new FormData();
uris.forEach((uri, index) => {
const filename = uri.split('/').pop() || `image_${index}.jpg`;
const match = /\.(\w+)$/.exec(filename);
const type = match ? `image/${match[1]}` : 'image/jpeg';
formData.append('images', {
uri: Platform.OS === 'ios' ? uri.replace('file://', '') : uri,
type,
name: filename,
});
});
const response = await api.post('/upload/multiple', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
};
完整示例 #
tsx
import React, {useState} from 'react';
import {
View,
Image,
Button,
StyleSheet,
Alert,
FlatList,
TouchableOpacity,
Text,
ActivityIndicator,
} from 'react-native';
import {
launchCamera,
launchImageLibrary,
ImagePickerResponse,
} from 'react-native-image-picker';
import ImageResizer from 'react-native-image-resizer';
import {request, PERMISSIONS, RESULTS} from 'react-native-permissions';
import {Platform} from 'react-native';
const ImageUploadScreen = () => {
const [images, setImages] = useState<string[]>([]);
const [uploading, setUploading] = useState(false);
const requestPermission = async (type: 'camera' | 'library'): Promise<boolean> => {
const permission = type === 'camera'
? Platform.select({
ios: PERMISSIONS.IOS.CAMERA,
android: PERMISSIONS.ANDROID.CAMERA,
})
: Platform.select({
ios: PERMISSIONS.IOS.PHOTO_LIBRARY,
android: PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE,
});
if (!permission) return false;
const result = await request(permission);
return result === RESULTS.GRANTED;
};
const compressImage = async (uri: string): Promise<string> => {
const result = await ImageResizer.createResizedImage(
uri,
1024,
1024,
'JPEG',
80,
0,
);
return result.uri;
};
const handleTakePhoto = async () => {
const hasPermission = await requestPermission('camera');
if (!hasPermission) {
Alert.alert('权限被拒绝', '请在设置中开启相机权限');
return;
}
const result: ImagePickerResponse = await launchCamera({
mediaType: 'photo',
quality: 0.8,
saveToPhotos: true,
});
if (result.assets && result.assets[0]) {
const compressedUri = await compressImage(result.assets[0].uri);
setImages(prev => [...prev, compressedUri]);
}
};
const handleChoosePhoto = async () => {
const hasPermission = await requestPermission('library');
if (!hasPermission) {
Alert.alert('权限被拒绝', '请在设置中开启相册权限');
return;
}
const result: ImagePickerResponse = await launchImageLibrary({
mediaType: 'photo',
selectionLimit: 5,
quality: 0.8,
});
if (result.assets) {
const compressedUris = await Promise.all(
result.assets.map(asset => compressImage(asset.uri)),
);
setImages(prev => [...prev, ...compressedUris]);
}
};
const handleRemoveImage = (index: number) => {
setImages(prev => prev.filter((_, i) => i !== index));
};
const handleUpload = async () => {
if (images.length === 0) {
Alert.alert('提示', '请先选择图片');
return;
}
setUploading(true);
try {
const formData = new FormData();
images.forEach((uri, index) => {
const filename = uri.split('/').pop() || `image_${index}.jpg`;
formData.append('images', {
uri: Platform.OS === 'ios' ? uri.replace('file://', '') : uri,
type: 'image/jpeg',
name: filename,
});
});
const response = await api.post('/upload', formData, {
headers: {'Content-Type': 'multipart/form-data'},
});
Alert.alert('成功', '图片上传成功');
setImages([]);
} catch (error) {
Alert.alert('错误', '上传失败,请重试');
} finally {
setUploading(false);
}
};
const renderImage = ({item, index}: {item: string; index: number}) => (
<View style={styles.imageContainer}>
<Image source={{uri: item}} style={styles.thumbnail} />
<TouchableOpacity
style={styles.removeButton}
onPress={() => handleRemoveImage(index)}>
<Text style={styles.removeText}>×</Text>
</TouchableOpacity>
</View>
);
return (
<View style={styles.container}>
<FlatList
data={images}
renderItem={renderImage}
keyExtractor={(item, index) => index.toString()}
numColumns={3}
contentContainerStyle={styles.imageList}
/>
<View style={styles.actions}>
<Button title="拍照" onPress={handleTakePhoto} />
<Button title="从相册选择" onPress={handleChoosePhoto} />
</View>
<TouchableOpacity
style={[styles.uploadButton, uploading && styles.disabledButton]}
onPress={handleUpload}
disabled={uploading}>
{uploading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.uploadText}>上传图片</Text>
)}
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
imageList: {
padding: 8,
},
imageContainer: {
margin: 4,
width: 100,
height: 100,
},
thumbnail: {
width: '100%',
height: '100%',
borderRadius: 8,
},
removeButton: {
position: 'absolute',
top: -8,
right: -8,
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: '#FF3B30',
justifyContent: 'center',
alignItems: 'center',
},
removeText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
actions: {
flexDirection: 'row',
justifyContent: 'space-around',
padding: 16,
backgroundColor: '#fff',
},
uploadButton: {
backgroundColor: '#007AFF',
margin: 16,
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
disabledButton: {
backgroundColor: '#ccc',
},
uploadText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});
export default ImageUploadScreen;
总结 #
相机和相册集成要点:
- 权限配置:iOS 和 Android 都需要配置权限
- react-native-image-picker:最常用的图片选择库
- 图片压缩:上传前压缩图片节省带宽
- 权限请求:运行时请求权限
继续学习 定位服务,了解如何获取设备位置。
最后更新:2026-03-28