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