leiwuhen-67's blog leiwuhen-67's blog
首页
    • 《Vue》笔记
    • 《React》笔记
    • 《NodeJs》笔记
    • 《CSS》笔记
    • 《Redis》笔记
    • 基础入门
    • 《Mock》笔记
    • 《MySQL》笔记
    • 《Git》相关
影音视听
收藏
关于
GitHub (opens new window)

我的公众号

首页
    • 《Vue》笔记
    • 《React》笔记
    • 《NodeJs》笔记
    • 《CSS》笔记
    • 《Redis》笔记
    • 基础入门
    • 《Mock》笔记
    • 《MySQL》笔记
    • 《Git》相关
影音视听
收藏
关于
GitHub (opens new window)
  • React

  • React Native

    • React Native之打包安卓apk优化
    • React Native之安卓apk架构区别解析
    • React Native之打包安卓修改Apk文件名
    • React Native之项目创建以及路由配置
    • React Native之h5唤起APP配置
    • React Native之安卓应用保存图片到相册
    • 《React》笔记
    • React Native
    心欲无痕
    2025-05-28
    目录

    React Native之安卓应用保存图片到相册

    最近在研究 React Native,想着能不能像安卓那样也封装方法便于随时调用。

    在保存图片的时候又分了好几种情况,比如保存本地图片到相册、保存网络图片到相册、保存 base64 格式的图片到相册。

    保存本地图片相对简单,直接使用 @react-native-camera-roll/camera-roll 这个插件就行。保存网络图片和 base64 格式的图片需要先下载图片,然后再进行保存操作。

    最初使用 react-native-fs 这个插件,然而在经过一天多的折腾之后,选择放弃了,不知道是版本不兼容还是哪里的问题,也查询过很多资料,都试过之后还是莫名报错,哪怕我只是引入该插件,然后打印一下结果也会报错,TypeError: Cannot read property 'RNFSFileTypeRegular' of null 错误。

    一番折腾无果后,最后选择了 react-native-blob-util 这个插件,特此记录下踩坑的过程。

    不管是哪种方式的保存,都需要先获取权限才行。因此,这里简单封装一下获取权限的方法:

    在 src/utils 下建立 imageUtils.js 文件,代码如下:

    import { Alert, Linking, PermissionsAndroid } from 'react-native';
    
    async function requestStoragePermission() {
      try {
        const result = await PermissionsAndroid.request(
          PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
          {
            title: '存储权限请求',
            message: '应用需要访问相册以保存图片',
            buttonNeutral: '稍后询问',
            buttonNegative: '取消',
            buttonPositive: '确定',
          }
        );
    
        return {
          granted: result === PermissionsAndroid.RESULTS.GRANTED,
          neverAskAgain: result === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN,
        };
      } catch (err) {
        console.warn('权限请求异常:', err);
        return { granted: false, neverAskAgain: false };
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    获取权限时还得考虑用户拒绝的情况,因此,在拒绝后,二次点击时需要引导用户去设置页开启权限,因此,上面的文件中还得加一个引导弹窗的方法:

    // 显示前往设置的引导弹窗
    function showPermissionGuideAlert() {
      Alert.alert(
        '需要存储权限',
        '请在设置中开启存储权限以继续使用该功能(也可能是媒体和文件访问权限)',
        [
          {
            text: '取消',
            style: 'cancel',
          },
          {
            text: '前往设置',
            onPress: () => {
              // 打开应用设置页面
              Linking.openSettings().catch(() => {
                Alert.alert('无法打开设置页面,请手动前往系统设置');
              });
            },
          },
        ]
      );
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    准备工作做完了,现在开始分三种情况来考虑:

    # 1、保存本地图片到相册(类似:file://...)

    import { CameraRoll } from '@react-native-camera-roll/camera-roll';
    
    export async function saveLocalImage(localFilePath) {
      try {
        const { granted, neverAskAgain } = await requestStoragePermission();
    
        if (!granted) {
          if (neverAskAgain) {
            showPermissionGuideAlert();
          } else {
            Alert.alert('权限被拒绝', '需要存储权限来保存图片');
          }
          return;
        }
    
        await CameraRoll.saveAsset(localFilePath, { type: 'photo' });
        Alert.alert('保存成功', '图片已保存到相册');
      } catch (error) {
        console.error('保存本地图片失败:', error);
        Alert.alert('保存失败', error.message || '未知错误');
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    # 2、保存网络图片到相册

    const RNBlobUtil = require('react-native-blob-util').default;
    
    export async function saveNetworkImage ( imageUrl ) {
      try {
        const { granted, neverAskAgain } = await requestStoragePermission();
    
        if (!granted) {
          if (neverAskAgain) {
            showPermissionGuideAlert();
          } else {
            Alert.alert('权限被拒绝', '需要存储权限来保存图片');
          }
          return;
        }
    
        // 创建临时文件路径
        const fileName = imageUrl.split('/').pop() || `image_${Date.now()}.jpg`;
        const { fs } = RNBlobUtil;
        const cacheDir = fs.dirs.CacheDir;
        const filePath = `${ cacheDir }/${ fileName }`;
    
        // 下载图片
        const response = await RNBlobUtil.config({
          fileCache: true,
          path: filePath,
        } ).fetch( 'GET', imageUrl );
    
        // 检查下载状态
        if (response.info().status !== 200) {
          throw new Error(`下载失败,状态码: ${response.info().status}`);
        }
        await CameraRoll.saveAsset( `file://${ filePath }`, { type: 'photo' } );
        // 清理临时文件
        await fs.unlink(filePath);
        Alert.alert('保存成功', '图片已保存到相册');
      } catch (error) {
        console.error('保存网络图片失败:', error);
        Alert.alert('保存失败', error.message || '未知错误');
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40

    # 3、保存 base64 格式的图片到相册

    export async function saveBase64Image ( base64String, fileName ) {
      try {
        const { granted, neverAskAgain } = await requestStoragePermission();
    
        if (!granted) {
          if (neverAskAgain) {
            showPermissionGuideAlert();
          } else {
            Alert.alert('权限被拒绝', '需要存储权限来保存图片');
          }
          return;
        }
    
        let processedBase64 = base64String;
        if (base64String.startsWith('data:image')) {
          // 如果是完整的Data URL格式,提取实际的Base64部分
          processedBase64 = base64String.split(',')[1];
        }
    
        // 创建临时文件路径
        const actualFilename = fileName || `base64_image_${Date.now()}.jpg`;
        const cacheDir = RNBlobUtil.fs.dirs.CacheDir;
        const filePath = `${cacheDir}/${actualFilename}`;
    
        // 将Base64数据写入文件
        await RNBlobUtil.fs.writeFile(filePath, processedBase64, 'base64');
    
        // 保存到相册
        await CameraRoll.saveAsset(`file://${filePath}`, { type: 'photo' });
    
        // 清理临时文件
        await RNBlobUtil.fs.unlink(filePath);
    
        Alert.alert('保存成功', '图片已保存到相册');
      } catch (error) {
        console.error('保存Base64图片失败:', error);
        Alert.alert('保存失败', error.message || '未知错误');
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39

    然后在其他地方就可以使用了。

    import { StyleSheet, Text, TouchableOpacity, View, Alert } from 'react-native';
    import { saveNetworkImage, saveBase64Image } from '../utils/imageUtils';
    
    export default function DemoScreen () {
    	const handleSaveNetworkImage = async () => {
    	  const imageUrl = 'https://qcloud.dpfile.com/pc/TrdZpLN1zkXDV4oN2FH98LdVnvHj694NKQu0_KA3ul4eYxZWRPQ7CJuw-PqyZBS4.jpg';
    	  const result = await saveNetworkImage( imageUrl );
    
    	  Alert.alert(
    	    result.success ? '成功' : '失败',
    	    result.message
    	  );
    	};
    
    	return (
    		<TouchableOpacity style={[styles.btnWrapper]} onPress={handleSaveNetworkImage}>
    		  <Text style={[styles.btnText]}>保存到相册</Text>
    		</TouchableOpacity>
    	)
    }
    
    const styles = StyleSheet.create( {
      btnWrapper: {
        marginTop: 20,
        padding: 10,
        borderRadius: 5,
        backgroundColor: '#007bff',
      },
      btnText: {
        color: '#fff',
      },
    } );
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

    src/utils/imageUtils.js 文件完整代码:

    import { Alert, Linking, PermissionsAndroid, Platform } from 'react-native';
    import { CameraRoll } from '@react-native-camera-roll/camera-roll';
    const RNBlobUtil = require('react-native-blob-util').default;
    
    async function requestStoragePermission() {
      try {
        const result = await PermissionsAndroid.request(
          PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
          {
            title: '存储权限请求',
            message: '应用需要访问相册以保存图片',
            buttonNeutral: '稍后询问',
            buttonNegative: '取消',
            buttonPositive: '确定',
          }
        );
    
        return {
          granted: result === PermissionsAndroid.RESULTS.GRANTED,
          neverAskAgain: result === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN,
        };
      } catch (err) {
        console.warn('权限请求异常:', err);
        return { granted: false, neverAskAgain: false };
      }
    }
    
    // 显示前往设置的引导弹窗
    function showPermissionGuideAlert() {
      Alert.alert(
        '需要存储权限',
        '请在设置中开启存储权限以继续使用该功能\n\n' +
        '操作步骤:\n' +
        '1. 点击"前往设置"\n' +
        '2. 找到"权限"或"应用权限"\n' +
        '3. 开启"存储"或"文件和媒体"权限',
        [
          {
            text: '取消',
            style: 'cancel',
          },
          {
            text: '前往设置',
            onPress: () => {
              // 打开应用设置页面
              Linking.openSettings().catch(() => {
                Alert.alert('无法打开设置页面,请手动前往系统设置');
              });
            },
          },
        ]
      );
    }
    
    // 保存本地图片到相册
    export async function saveLocalImage(localFilePath) {
      try {
        const { granted, neverAskAgain } = await requestStoragePermission();
    
        if (!granted) {
          if (neverAskAgain) {
            showPermissionGuideAlert();
          } else {
            Alert.alert('权限被拒绝', '需要存储权限来保存图片');
          }
          return;
        }
    
        await CameraRoll.saveAsset(localFilePath, { type: 'photo' });
        Alert.alert('保存成功', '图片已保存到相册');
      } catch (error) {
        console.error('保存本地图片失败:', error);
        Alert.alert('保存失败', error.message || '未知错误');
      }
    }
    
    // 保存网络图片到相册
    export async function saveNetworkImage ( imageUrl ) {
      try {
        const { granted, neverAskAgain } = await requestStoragePermission();
    
        if (!granted) {
          if (neverAskAgain) {
            showPermissionGuideAlert();
          } else {
            Alert.alert('权限被拒绝', '需要存储权限来保存图片');
          }
          return;
        }
    
        // 创建临时文件路径
        const fileName = imageUrl.split('/').pop() || `image_${Date.now()}.jpg`;
        const { fs } = RNBlobUtil;
        const cacheDir = fs.dirs.CacheDir;
        const filePath = `${ cacheDir }/${ fileName }`;
    
        // 下载图片
        const response = await RNBlobUtil.config({
          fileCache: true,
          path: filePath,
        } ).fetch( 'GET', imageUrl );
    
        // 检查下载状态
        if (response.info().status !== 200) {
          throw new Error(`下载失败,状态码: ${response.info().status}`);
        }
        await CameraRoll.saveAsset( `file://${ filePath }`, { type: 'photo' } );
        // 清理临时文件
        await fs.unlink(filePath);
        Alert.alert('保存成功', '图片已保存到相册');
      } catch (error) {
        console.error('保存网络图片失败:', error);
        Alert.alert('保存失败', error.message || '未知错误');
      }
    }
    
    // 保存Base64格式图片到相册
    export async function saveBase64Image ( base64String, fileName ) {
      try {
        const { granted, neverAskAgain } = await requestStoragePermission();
    
        if (!granted) {
          if (neverAskAgain) {
            showPermissionGuideAlert();
          } else {
            Alert.alert('权限被拒绝', '需要存储权限来保存图片');
          }
          return;
        }
    
        // 确保base64数据格式正确
        let processedBase64 = base64String;
        if (base64String.startsWith('data:image')) {
          // 如果是完整的Data URL格式,提取实际的Base64部分
          processedBase64 = base64String.split(',')[1];
        }
    
        // 创建临时文件路径
        const actualFilename = fileName || `base64_image_${Date.now()}.jpg`;
        const cacheDir = RNBlobUtil.fs.dirs.CacheDir;
        const filePath = `${cacheDir}/${actualFilename}`;
    
        // 将Base64数据写入文件
        await RNBlobUtil.fs.writeFile(filePath, processedBase64, 'base64');
    
        // 保存到相册
        await CameraRoll.saveAsset(`file://${filePath}`, { type: 'photo' });
    
        // 清理临时文件
        await RNBlobUtil.fs.unlink(filePath);
    
        Alert.alert('保存成功', '图片已保存到相册');
      } catch (error) {
        console.error('保存Base64图片失败:', error);
        Alert.alert('保存失败', error.message || '未知错误');
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    编辑 (opens new window)
    React Native之h5唤起APP配置

    ← React Native之h5唤起APP配置

    Theme by Vdoing
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式