import 'dart:io'; import 'dart:ui' show ImageByteFormat; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:gal/gal.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; /// ============================================================ /// 【ShareService】分享服务(系统分享面板统一封装) /// /// 一、为什么包一层: /// 业务页不用关心 share_plus 的 API 细节,统一调 shareText / /// shareImage / shareFromWidget,返回统一的 ShareResult。 /// /// 二、能力: /// 1. shareText → 分享纯文本 / 链接 /// 2. shareImage → 分享单张图片(本地文件) /// 3. shareImages → 分享多张图片 /// 4. shareFromWidget → 把任意 Widget 截图成图片 → 分享(海报核心能力) /// 5. saveToGallery → 保存图片到系统相册(Android/iOS) /// /// 三、平台策略: /// Android → ACTION_SEND Intent(系统分享面板,用户自选微信/QQ/微博等) /// iOS → UIActivityViewController(系统分享面板) /// Web → Web Share API(浏览器原生,不支持则降级下载) /// /// 四、后续扩展: /// 如需精确分享到微信好友/朋友圈,加 fluwx(微信 OpenSDK), /// 在 ShareService 内部加分支判断,业务页不用改。 /// /// 五、高频坑: /// 1. Android 分享图片必须用 FileProvider,share_plus 已内置处理, /// 但传入的必须是真实文件路径(XFile),不能是 Uint8List 直接传; /// 2. shareFromWidget 的截图依赖 RepaintBoundary, /// Widget 必须在渲染树中(不能是离屏未 build 的); /// 3. Web 端 shareXFiles 不支持 Linux,Windows 走 mailto。 /// ============================================================ class ShareService { ShareService._(); static final ShareService instance = ShareService._(); // ==================== 文本分享 ==================== /// 分享文本 / 链接 /// /// [text] 要分享的内容(纯文本或 URL) /// [subject] 邮件分享时的主题(可选) Future shareText({ required String text, String? subject, }) async { return SharePlus.instance.share( ShareParams( text: text, subject: subject, ), ); } // ==================== 图片分享 ==================== /// 分享单张图片(本地文件路径) /// /// [filePath] 本地图片文件路径 /// [text] 附带的文字说明(可选,部分 App 会显示) Future shareImage({ required String filePath, String? text, }) async { return shareImages(filePaths: [filePath], text: text); } /// 分享多张图片 /// /// [filePaths] 本地图片文件路径列表 /// [text] 附带的文字说明(可选) Future shareImages({ required List filePaths, String? text, }) async { final files = filePaths.map((p) => XFile(p)).toList(); return SharePlus.instance.share( ShareParams( files: files, text: text, ), ); } /// 分享内存字节(Uint8List)为图片 /// /// 适用场景:网络图片下载后直接分享,不先写磁盘。 /// 内部自动写入临时目录再分享。 Future shareImageBytes({ required Uint8List bytes, String fileName = 'share_image.jpg', String? text, }) async { final dir = await getTemporaryDirectory(); final file = File('${dir.path}/$fileName'); await file.writeAsBytes(bytes); return shareImage(filePath: file.path, text: text); } // ==================== Widget 截图分享(海报核心) ==================== /// 把任意 Widget 截图成图片 → 分享 /// /// 这是"攻克海报分享"的核心能力: /// 1. 用 RepaintBoundary 包裹目标 Widget; /// 2. 截图转 Uint8List; /// 3. 写入临时文件 → 调系统分享。 /// /// [builder] 构建要截图的 Widget(通常是海报布局) /// [text] 附带的文字说明 /// [pixelRatio] 截图清晰度(默认 3.0,越高越清晰但越慢) Future shareFromWidget({ required WidgetBuilder builder, required BuildContext context, String? text, double pixelRatio = 3.0, }) async { final bytes = await captureWidget( builder: builder, context: context, pixelRatio: pixelRatio, ); if (bytes == null) { return ShareResult.unavailable; } return shareImageBytes(bytes: bytes, text: text); } /// 把任意 Widget 截图为 Uint8List(PNG) /// /// 独立方法:截图后可以做任何事(保存、上传、分享), /// 不局限于分享场景。 static Future captureWidget({ required WidgetBuilder builder, required BuildContext context, double pixelRatio = 3.0, }) async { // 用 OverlayEntry 把 Widget 临时放到渲染树中 late OverlayEntry entry; final boundaryKey = GlobalKey(); entry = OverlayEntry( builder: (_) => RepaintBoundary( key: boundaryKey, child: builder(context), ), ); // 插入 Overlay(Widget 开始 build) Overlay.of(context).insert(entry); // 等一帧让 Widget 完成渲染 await Future.delayed(const Duration(milliseconds: 100)); try { final boundary = boundaryKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; if (boundary == null) return null; final image = await boundary.toImage(pixelRatio: pixelRatio); final bytes = await image.toByteData(format: ImageByteFormat.png); return bytes?.buffer.asUint8List(); } finally { // 截图完成,移除临时 Overlay entry.remove(); } } // ==================== 保存到相册 ==================== /// 保存图片到系统相册(Android/iOS/macOS/Windows) /// /// 底层用 gal 插件: /// Android → MediaStore(API 30+)或 WRITE_EXTERNAL_STORAGE(API ≤ 29) /// iOS → PHPhotoLibrary /// /// [filePath] 本地图片文件路径 /// [album] 保存到指定相册名(可选,Android 上不一定生效,取决于系统实现) /// /// 返回 true 表示保存成功,false 表示失败。 /// 权限不足时会抛 GalException,调用方可 catch 处理。 static Future saveToGallery({ required String filePath, String? album, }) async { if (kIsWeb) { debugPrint('⚠ Web 端不支持保存到相册'); return false; } try { await Gal.putImage(filePath, album: album); return true; } on GalException catch (e) { debugPrint('⚠ 保存到相册失败:${e.type.message}'); return false; } catch (e) { debugPrint('⚠ 保存到相册失败:$e'); return false; } } /// 保存内存字节到系统相册 /// /// 适用场景:Widget 截图后直接保存,不先写磁盘。 /// 内部自动写入临时文件再调 [saveToGallery]。 static Future saveToGalleryFromBytes({ required Uint8List bytes, String fileName = 'save_image.jpg', String? album, }) async { final dir = await getTemporaryDirectory(); final file = File('${dir.path}/$fileName'); await file.writeAsBytes(bytes); return saveToGallery(filePath: file.path, album: album); } }