Flutter:二维码保存相册,请求权限。

发布于:2025-02-11 ⋅ 阅读:(121) ⋅ 点赞:(0)

记录下,把页面红色区域内的内容,转成图片后保存到相册的功能
在这里插入图片描述

依赖

# 生成二维码
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>


网站公告

今日签到

点亮在社区的每一天
去签到