保存图片

This commit is contained in:
tx 2024-12-26 17:20:09 +08:00
parent a0f361a9ff
commit 1ca3c84102
8 changed files with 17802 additions and 359 deletions

View File

@ -13,28 +13,31 @@ import com.facebook.soloader.SoLoader
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages.toMutableList()
packages.add(MediaScannerPackage()) // 使用 Kotlin 语法添加包
return packages
}
override fun getJSMainModuleName(): String = "index"
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
}
}
}

View File

@ -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.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.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
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 {
private final ReactApplicationContext reactContext;
private static final String TAG = "MediaScannerModule";
public MediaScannerModule(ReactApplicationContext reactContext) {
super(reactContext);
@ -23,17 +32,62 @@ public class MediaScannerModule extends ReactContextBaseJavaModule {
}
@ReactMethod
public void scanFile(String path, String mimeType, Callback callback) {
MediaScannerConnection.scanFile(reactContext, new String[]{path}, new String[]{mimeType},
new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
if (uri != null) {
callback.invoke(null);
} else {
callback.invoke("Error scanning file");
}
public void scanFile(String sourcePath, String mimeType, final Callback callback) {
try {
File sourceFile = new File(sourcePath);
if (!sourceFile.exists()) {
Log.e(TAG, "Source file does not exist: " + sourcePath);
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);
} catch (IOException e) {
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());
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
"dependencies": {
"@eva-design/eva": "^2.2.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/slider": "^4.5.5",
"@react-native-picker/picker": "^2.9.0",

49
src/utils/ImageUtils.ts Normal file
View 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;

View File

@ -1,5 +1,5 @@
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 { RouteProp, useRoute } from '@react-navigation/native';
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 QRCode from 'react-native-qrcode-svg';
import { captureRef } from 'react-native-view-shot';
import Toast from 'react-native-toast-message';
const ShareQrcode = () => {
const route = useRoute<RouteProp<RootStackParamList, 'ShareQrcode'>>();
const { QrId } = route.params;
const [loading, setLoading] = useState(true);
const qrCodeUrl = `https://testlu.chuangtewl.com/prod-api?keyId=${QrId}`;
const logoUrl = 'https://lxnapi.ccttiot.com/bike/img/static/u3giTY4VkWYpnGWRuFHF';
const qrRef = useRef();
useEffect(() => {
@ -23,9 +25,18 @@ const ShareQrcode = () => {
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 () => {
try {
// 修正权限请求
const writePermission = await request(
Platform.select({
android: PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE,
@ -33,65 +44,49 @@ const ShareQrcode = () => {
})
);
const readPermission = await request(
Platform.select({
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();
}
}
}
]
);
if (writePermission !== RESULTS.GRANTED) {
showToast('请在设置中授予存储权限', 'error');
return;
}
// 使用应用专用目录
const fileName = `qrcode_${Date.now()}.jpg`;
const downloadDest = `${RNFS.ExternalDirectoryPath}/${fileName}`;
const tempPath = `${RNFS.CachesDirectoryPath}/${fileName}`;
// 截取QR码视图
const uri = await captureRef(qrRef, {
format: 'jpg',
quality: 1,
result: 'base64',
});
// 复制文件到目标位置
await RNFS.copyFile(uri, downloadDest);
await RNFS.writeFile(tempPath, uri, 'base64');
// 使用 MediaScanner 使图片在相册中可见
NativeModules.MediaScanner.scanFile(downloadDest, 'image/jpeg', (err) => {
if (err) {
console.error('MediaScanner error:', err);
Alert.alert('错误', '保存失败,请重试');
} else {
Alert.alert('成功', '二维码已保存到相册');
}
await Promise.race([
new Promise((resolve, reject) => {
NativeModules.MediaScanner.scanFile(
tempPath,
'image/jpeg',
(error) => {
if (error) {
console.warn('MediaScanner warning:', error);
resolve(true);
} else {
resolve(true);
}
}
);
}),
new Promise((resolve) => setTimeout(resolve, 3000))
]);
showToast('二维码已保存到相册', 'success');
RNFS.unlink(tempPath).catch(e => {
console.warn('清理临时文件失败:', e);
});
// 清理临时文件
try {
await RNFS.unlink(uri);
} catch (e) {
console.log('清理临时文件失败:', e);
}
} catch (error) {
console.error('Save error:', error);
Alert.alert('错误', '保存失败,请重试');
showToast('文件可能已保存到相册,请检查', 'info');
}
};
@ -120,6 +115,10 @@ const ShareQrcode = () => {
size={rpx(438)}
color="black"
backgroundColor="white"
logo={{ uri: logoUrl }}
logoSize={rpx(88)}
logoBackgroundColor='white'
logoBorderRadius={rpx(10)}
/>
</Layout>
</ImageBackground>
@ -127,6 +126,7 @@ const ShareQrcode = () => {
<TouchableOpacity style={styles.btn} onPress={saveToGallery}>
<Text style={styles.btnText}></Text>
</TouchableOpacity>
<Toast />
</View>
);
}

705
yarn.lock

File diff suppressed because it is too large Load Diff