记录下,把页面红色区域内的内容,转成图片后保存到相册的功能
依赖
# 生成二维码
qr_flutter: ^4.1.0
# 保存图片
image_gallery_saver_plus: ^3.0.5
# 权限处理
permission_handler: ^11.3.1
# 设备信息
device_info_plus: ^9.1.2
view
import 'package:demo/common/index.dart';
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import 'index.dart';
class SharePage extends GetView<ShareController> {
const SharePage({super.key});
// 主视图
Widget _buildView() {
return RepaintBoundary(
key: controller.qrKey,
child: <Widget>[
TDImage(
assetUrl: 'assets/img/user.png',
width: 100.w,
height: 100.w,
),
TextWidget.body('邀请码:10086', size: 40.sp),
QrImageView(
data: '10086',
version: QrVersions.auto,
size: 400.w,
gapless: false,
embeddedImage: const AssetImage('assets/img/user.png'),
embeddedImageStyle: QrEmbeddedImageStyle(
size: Size(100.w, 100.w),
),
),
].toColumn(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
).card(color: Colors.white).tight(width: 750.w, height: 750.w),
);
}
@override
Widget build(BuildContext context) {
return GetBuilder<ShareController>(
init: ShareController(),
id: "share",
builder: (_) {
return Scaffold(
backgroundColor: const Color(0xffF5F6FA),
appBar: TDNavBar(
height: 45,
title: '邀请码',
titleFontWeight: FontWeight.w600,
backgroundColor: Colors.white,
screenAdaptation: true,
useDefaultBack: true,
rightBarItems: [
TDNavBarItem(
iconWidget: Text('保存'),
action: controller.saveQrCode,
),
],
),
body: SafeArea(
child: _buildView(),
),
);
},
);
}
}
controller
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'dart:ui' as ui;
class InviteFriendController extends GetxController {
InviteFriendController();
final GlobalKey qrKey = GlobalKey();
// 检查并请求权限
Future<bool> _checkAndRequestPermission() async {
if (!GetPlatform.isAndroid) return true;
try {
final androidInfo = await DeviceInfoPlugin().androidInfo;
final sdkInt = androidInfo.version.sdkInt;
PermissionStatus status;
if (sdkInt >= 30) { // Android 11 (API 30) 及以上
status = await Permission.manageExternalStorage.request();
} else { // Android 11 以下
status = await Permission.storage.request();
}
if (!status.isGranted) {
Get.snackbar('提示', '需要存储权限才能保存图片', backgroundColor: Colors.white);
return false;
}
return true;
} catch (e) {
Get.snackbar('提示', '权限检查失败', backgroundColor: Colors.white);
return false;
}
}
Future<void> saveQrCode() async {
try {
// 先检查权限
if (!await _checkAndRequestPermission()) {
return;
}
// 获取 RenderRepaintBoundary
final boundary = qrKey.currentContext!
.findRenderObject() as RenderRepaintBoundary;
// 等待一下确保UI已经完全渲染
await Future.delayed(const Duration(milliseconds: 20));
// 将 Widget 转换成图片
final image = await boundary.toImage(pixelRatio: 3.0);
// 将图片转换成字节数据
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
if (byteData != null) {
// 使用 image_gallery_saver_plus 保存到相册
final result = await ImageGallerySaverPlus.saveImage(
byteData.buffer.asUint8List(),
quality: 100,
name: "qr_code_${DateTime.now().millisecondsSinceEpoch}"
);
if (result != null && result['isSuccess']) {
Get.snackbar('提示', '保存成功',backgroundColor: Colors.white);
} else {
Get.snackbar('提示', '保存失败,请确保已授予存储权限',backgroundColor: Colors.white);
}
}
} catch (e) {
Get.snackbar('提示', '保存失败,请确保已授予存储权限',backgroundColor: Colors.white);
}
}
_initData() {
update(["invite_friend"]);
}
void onTap() {}
// @override
// void onInit() {
// super.onInit();
// }
@override
void onReady() {
super.onReady();
_initData();
}
// @override
// void onClose() {
// super.onClose();
// }
}
AndroidManifest.xml 权限配置
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 允许应用访问网络,用于下载APK -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- 允许应用请求安装APK包 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<!-- 允许应用写入外部存储 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 允许应用读取外部存储 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- Android 10及以上需要 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- 相机 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 允许应用读取媒体图像 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<application
android:label="辰****选"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true">
<!-- FileProvider配置 -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<!-- FileProvider是Android 7.0后推出的文件访问机制,用于安全地分享文件 -->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" /> <!-- 指定可访问路径的配置文件 -->
</provider>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>