保存图片
This commit is contained in:
parent
a0f361a9ff
commit
1ca3c84102
|
@ -15,8 +15,11 @@ class MainApplication : Application(), ReactApplication {
|
||||||
|
|
||||||
override val reactNativeHost: ReactNativeHost =
|
override val reactNativeHost: ReactNativeHost =
|
||||||
object : DefaultReactNativeHost(this) {
|
object : DefaultReactNativeHost(this) {
|
||||||
override fun getPackages(): List<ReactPackage> =
|
override fun getPackages(): List<ReactPackage> {
|
||||||
PackageList(this).packages
|
val packages = PackageList(this).packages.toMutableList()
|
||||||
|
packages.add(MediaScannerPackage()) // 使用 Kotlin 语法添加包
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
override fun getJSMainModuleName(): String = "index"
|
override fun getJSMainModuleName(): String = "index"
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
package com.yourapp;
|
package com.bikeapp_demo;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.ContentResolver;
|
||||||
|
import android.content.ContentValues;
|
||||||
import android.media.MediaScannerConnection;
|
import android.media.MediaScannerConnection;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.util.Log;
|
||||||
import com.facebook.react.bridge.ReactApplicationContext;
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
import com.facebook.react.bridge.ReactMethod;
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
import com.facebook.react.bridge.Callback;
|
import com.facebook.react.bridge.Callback;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
public class MediaScannerModule extends ReactContextBaseJavaModule {
|
public class MediaScannerModule extends ReactContextBaseJavaModule {
|
||||||
|
|
||||||
private final ReactApplicationContext reactContext;
|
private final ReactApplicationContext reactContext;
|
||||||
|
private static final String TAG = "MediaScannerModule";
|
||||||
|
|
||||||
public MediaScannerModule(ReactApplicationContext reactContext) {
|
public MediaScannerModule(ReactApplicationContext reactContext) {
|
||||||
super(reactContext);
|
super(reactContext);
|
||||||
|
@ -23,17 +32,62 @@ public class MediaScannerModule extends ReactContextBaseJavaModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void scanFile(String path, String mimeType, Callback callback) {
|
public void scanFile(String sourcePath, String mimeType, final Callback callback) {
|
||||||
MediaScannerConnection.scanFile(reactContext, new String[]{path}, new String[]{mimeType},
|
try {
|
||||||
new MediaScannerConnection.OnScanCompletedListener() {
|
File sourceFile = new File(sourcePath);
|
||||||
@Override
|
if (!sourceFile.exists()) {
|
||||||
public void onScanCompleted(String path, Uri uri) {
|
Log.e(TAG, "Source file does not exist: " + sourcePath);
|
||||||
if (uri != null) {
|
callback.invoke("Source file does not exist: " + sourcePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentResolver resolver = reactContext.getContentResolver();
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(MediaStore.Images.Media.DISPLAY_NAME, sourceFile.getName());
|
||||||
|
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
|
||||||
|
values.put(MediaStore.Images.Media.IS_PENDING, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
|
||||||
|
if (imageUri == null) {
|
||||||
|
Log.e(TAG, "Failed to create media store entry");
|
||||||
|
callback.invoke("Failed to create media store entry");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (OutputStream os = resolver.openOutputStream(imageUri);
|
||||||
|
FileInputStream fis = new FileInputStream(sourceFile)) {
|
||||||
|
if (os == null) {
|
||||||
|
Log.e(TAG, "Failed to open output stream");
|
||||||
|
callback.invoke("Failed to open output stream");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||||
|
os.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
os.flush();
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
values.clear();
|
||||||
|
values.put(MediaStore.Images.Media.IS_PENDING, 0);
|
||||||
|
resolver.update(imageUri, values, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
callback.invoke(null);
|
callback.invoke(null);
|
||||||
} else {
|
} catch (IOException e) {
|
||||||
callback.invoke("Error scanning file");
|
Log.e(TAG, "IO Error: " + e.getMessage());
|
||||||
|
resolver.delete(imageUri, null, null);
|
||||||
|
callback.invoke("IO Error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error: " + e.getMessage());
|
||||||
|
callback.invoke("Error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.bikeapp_demo;
|
||||||
|
|
||||||
|
import com.facebook.react.ReactPackage;
|
||||||
|
import com.facebook.react.bridge.NativeModule;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.uimanager.ViewManager;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MediaScannerPackage implements ReactPackage {
|
||||||
|
@Override
|
||||||
|
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||||
|
List<NativeModule> modules = new ArrayList<>();
|
||||||
|
modules.add(new MediaScannerModule(reactContext));
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
}
|
17169
package-lock.json
generated
Normal file
17169
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -12,6 +12,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eva-design/eva": "^2.2.0",
|
"@eva-design/eva": "^2.2.0",
|
||||||
"@react-native-async-storage/async-storage": "^2.1.0",
|
"@react-native-async-storage/async-storage": "^2.1.0",
|
||||||
|
"@react-native-camera-roll/camera-roll": "^7.9.0",
|
||||||
"@react-native-community/geolocation": "^3.4.0",
|
"@react-native-community/geolocation": "^3.4.0",
|
||||||
"@react-native-community/slider": "^4.5.5",
|
"@react-native-community/slider": "^4.5.5",
|
||||||
"@react-native-picker/picker": "^2.9.0",
|
"@react-native-picker/picker": "^2.9.0",
|
||||||
|
|
49
src/utils/ImageUtils.ts
Normal file
49
src/utils/ImageUtils.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
import { CameraRoll } from "@react-native-camera-roll/camera-roll";
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
|
||||||
|
const ImageUtil = {
|
||||||
|
saveImg: async (img: string) => {
|
||||||
|
try {
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
const result = await CameraRoll.save(img, {
|
||||||
|
type: 'photo'
|
||||||
|
});
|
||||||
|
console.log('保存成功!地址:', result);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
// Android 处理
|
||||||
|
const storeLocation = `${RNFS.CachesDirectoryPath}`;
|
||||||
|
const pathName = `${new Date().getTime()}_${Math.floor(Math.random() * 9999)}.jpg`;
|
||||||
|
const downloadPath = `${storeLocation}/${pathName}`;
|
||||||
|
|
||||||
|
// 下载或复制文件
|
||||||
|
const res = await RNFS.downloadFile({
|
||||||
|
fromUrl: img,
|
||||||
|
toFile: downloadPath,
|
||||||
|
}).promise;
|
||||||
|
|
||||||
|
if (res && res.statusCode === 200) {
|
||||||
|
const result = await CameraRoll.save(`file://${downloadPath}`, {
|
||||||
|
type: 'photo'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清理临时文件
|
||||||
|
try {
|
||||||
|
await RNFS.unlink(downloadPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('清理临时文件失败:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('保存成功!地址:', result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存失败!', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImageUtil;
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { View, StyleSheet, Image, ImageBackground, TouchableOpacity, Text, Alert, Platform, NativeModules } from 'react-native';
|
import { View, StyleSheet, Image, ImageBackground, TouchableOpacity, Text, Platform, NativeModules } from 'react-native';
|
||||||
import { rpx } from '../../utils/rpx';
|
import { rpx } from '../../utils/rpx';
|
||||||
import { RouteProp, useRoute } from '@react-navigation/native';
|
import { RouteProp, useRoute } from '@react-navigation/native';
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
|
@ -7,12 +7,14 @@ import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
|
||||||
import { Spinner, Layout } from '@ui-kitten/components';
|
import { Spinner, Layout } from '@ui-kitten/components';
|
||||||
import QRCode from 'react-native-qrcode-svg';
|
import QRCode from 'react-native-qrcode-svg';
|
||||||
import { captureRef } from 'react-native-view-shot';
|
import { captureRef } from 'react-native-view-shot';
|
||||||
|
import Toast from 'react-native-toast-message';
|
||||||
|
|
||||||
const ShareQrcode = () => {
|
const ShareQrcode = () => {
|
||||||
const route = useRoute<RouteProp<RootStackParamList, 'ShareQrcode'>>();
|
const route = useRoute<RouteProp<RootStackParamList, 'ShareQrcode'>>();
|
||||||
const { QrId } = route.params;
|
const { QrId } = route.params;
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const qrCodeUrl = `https://testlu.chuangtewl.com/prod-api?keyId=${QrId}`;
|
const qrCodeUrl = `https://testlu.chuangtewl.com/prod-api?keyId=${QrId}`;
|
||||||
|
const logoUrl = 'https://lxnapi.ccttiot.com/bike/img/static/u3giTY4VkWYpnGWRuFHF';
|
||||||
const qrRef = useRef();
|
const qrRef = useRef();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -23,9 +25,18 @@ const ShareQrcode = () => {
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const showToast = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||||||
|
Toast.show({
|
||||||
|
type,
|
||||||
|
text1: message,
|
||||||
|
position: 'top',
|
||||||
|
topOffset: Platform.OS === 'ios' ? 60 : 20,
|
||||||
|
visibilityTime: 2000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const saveToGallery = async () => {
|
const saveToGallery = async () => {
|
||||||
try {
|
try {
|
||||||
// 修正权限请求
|
|
||||||
const writePermission = await request(
|
const writePermission = await request(
|
||||||
Platform.select({
|
Platform.select({
|
||||||
android: PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE,
|
android: PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE,
|
||||||
|
@ -33,65 +44,49 @@ const ShareQrcode = () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const readPermission = await request(
|
if (writePermission !== RESULTS.GRANTED) {
|
||||||
Platform.select({
|
showToast('请在设置中授予存储权限', 'error');
|
||||||
android: PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE,
|
|
||||||
default: PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (writePermission !== RESULTS.GRANTED || readPermission !== RESULTS.GRANTED) {
|
|
||||||
Alert.alert(
|
|
||||||
'权限不足',
|
|
||||||
'请在设置中授予存储权限',
|
|
||||||
[
|
|
||||||
{ text: '取消' },
|
|
||||||
{
|
|
||||||
text: '去设置',
|
|
||||||
onPress: () => {
|
|
||||||
if (Platform.OS === 'android') {
|
|
||||||
Linking.openSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用应用专用目录
|
|
||||||
const fileName = `qrcode_${Date.now()}.jpg`;
|
const fileName = `qrcode_${Date.now()}.jpg`;
|
||||||
const downloadDest = `${RNFS.ExternalDirectoryPath}/${fileName}`;
|
const tempPath = `${RNFS.CachesDirectoryPath}/${fileName}`;
|
||||||
|
|
||||||
// 截取QR码视图
|
|
||||||
const uri = await captureRef(qrRef, {
|
const uri = await captureRef(qrRef, {
|
||||||
format: 'jpg',
|
format: 'jpg',
|
||||||
quality: 1,
|
quality: 1,
|
||||||
|
result: 'base64',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 复制文件到目标位置
|
await RNFS.writeFile(tempPath, uri, 'base64');
|
||||||
await RNFS.copyFile(uri, downloadDest);
|
|
||||||
|
|
||||||
// 使用 MediaScanner 使图片在相册中可见
|
await Promise.race([
|
||||||
NativeModules.MediaScanner.scanFile(downloadDest, 'image/jpeg', (err) => {
|
new Promise((resolve, reject) => {
|
||||||
if (err) {
|
NativeModules.MediaScanner.scanFile(
|
||||||
console.error('MediaScanner error:', err);
|
tempPath,
|
||||||
Alert.alert('错误', '保存失败,请重试');
|
'image/jpeg',
|
||||||
|
(error) => {
|
||||||
|
if (error) {
|
||||||
|
console.warn('MediaScanner warning:', error);
|
||||||
|
resolve(true);
|
||||||
} else {
|
} else {
|
||||||
Alert.alert('成功', '二维码已保存到相册');
|
resolve(true);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
new Promise((resolve) => setTimeout(resolve, 3000))
|
||||||
|
]);
|
||||||
|
|
||||||
// 清理临时文件
|
showToast('二维码已保存到相册', 'success');
|
||||||
try {
|
|
||||||
await RNFS.unlink(uri);
|
RNFS.unlink(tempPath).catch(e => {
|
||||||
} catch (e) {
|
console.warn('清理临时文件失败:', e);
|
||||||
console.log('清理临时文件失败:', e);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Save error:', error);
|
console.error('Save error:', error);
|
||||||
Alert.alert('错误', '保存失败,请重试');
|
showToast('文件可能已保存到相册,请检查', 'info');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -120,6 +115,10 @@ const ShareQrcode = () => {
|
||||||
size={rpx(438)}
|
size={rpx(438)}
|
||||||
color="black"
|
color="black"
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
|
logo={{ uri: logoUrl }}
|
||||||
|
logoSize={rpx(88)}
|
||||||
|
logoBackgroundColor='white'
|
||||||
|
logoBorderRadius={rpx(10)}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
</ImageBackground>
|
</ImageBackground>
|
||||||
|
@ -127,6 +126,7 @@ const ShareQrcode = () => {
|
||||||
<TouchableOpacity style={styles.btn} onPress={saveToGallery}>
|
<TouchableOpacity style={styles.btn} onPress={saveToGallery}>
|
||||||
<Text style={styles.btnText}>保存至图片</Text>
|
<Text style={styles.btnText}>保存至图片</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<Toast />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user