flutter架 构项目基础
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
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<ShareResult> shareText({
|
||||
required String text,
|
||||
String? subject,
|
||||
}) async {
|
||||
return SharePlus.instance.share(
|
||||
ShareParams(
|
||||
text: text,
|
||||
subject: subject,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 图片分享 ====================
|
||||
|
||||
/// 分享单张图片(本地文件路径)
|
||||
///
|
||||
/// [filePath] 本地图片文件路径
|
||||
/// [text] 附带的文字说明(可选,部分 App 会显示)
|
||||
Future<ShareResult> shareImage({
|
||||
required String filePath,
|
||||
String? text,
|
||||
}) async {
|
||||
return shareImages(filePaths: [filePath], text: text);
|
||||
}
|
||||
|
||||
/// 分享多张图片
|
||||
///
|
||||
/// [filePaths] 本地图片文件路径列表
|
||||
/// [text] 附带的文字说明(可选)
|
||||
Future<ShareResult> shareImages({
|
||||
required List<String> filePaths,
|
||||
String? text,
|
||||
}) async {
|
||||
final files = filePaths.map((p) => XFile(p)).toList();
|
||||
return SharePlus.instance.share(
|
||||
ShareParams(
|
||||
files: files,
|
||||
text: text,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 分享内存字节(Uint8List)为图片
|
||||
///
|
||||
/// 适用场景:网络图片下载后直接分享,不先写磁盘。
|
||||
/// 内部自动写入临时目录再分享。
|
||||
Future<ShareResult> 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<ShareResult> 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<Uint8List?> 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<bool> 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<bool> 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user