保存图片
This commit is contained in:
parent
a0f361a9ff
commit
1ca3c84102
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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": {
|
||||
"@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
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 { 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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user