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
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
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
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
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
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
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
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)