保存图片

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 { 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"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
} }
override val reactHost: ReactHost override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost) get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
SoLoader.init(this, false) SoLoader.init(this, false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app. // If you opted-in for the New Architecture, we load the native entry point for this app.
load() 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.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);
callback.invoke(null); return;
} else { }
callback.invoke("Error scanning file");
} 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": { "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
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 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',
} else { (error) => {
Alert.alert('成功', '二维码已保存到相册'); 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) { } 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>
); );
} }

705
yarn.lock

File diff suppressed because it is too large Load Diff