flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+225
View File
@@ -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);
}
}