126 lines
3.9 KiB
Dart
126 lines
3.9 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
/// 胶囊样式全局 Toast:体积小、大圆角、清爽,数秒后自动消失。
|
||
/// 通过根 Overlay 展示,不依赖调用方 BuildContext 和 Navigator,
|
||
/// 适用于页面销毁(dispose)后仍需弹出提示的场景。
|
||
class CapsuleToast {
|
||
static OverlayEntry? _entry;
|
||
static bool _showing = false;
|
||
|
||
/// 显示胶囊提示
|
||
/// [message] 提示文案
|
||
/// [duration] 显示时长,默认 2 秒
|
||
/// [showCheck] 是否显示对勾图标
|
||
static void show(
|
||
String message, {
|
||
Duration duration = const Duration(seconds: 2),
|
||
bool showCheck = true,
|
||
}) {
|
||
// 从根 Element 向下遍历子树,查找第一个 OverlayState
|
||
// (Overlay 是根的子节点,不能用 findAncestorStateOfType 向上找),
|
||
// 无需依赖调用方 context,页面销毁后也能正常弹出。
|
||
final root = WidgetsBinding.instance.rootElement;
|
||
if (root == null) return;
|
||
final overlay = _findOverlayState(root);
|
||
if (overlay == null) {
|
||
debugPrint('💊 [CapsuleToast] 未找到 Overlay,无法弹出: $message');
|
||
return;
|
||
}
|
||
debugPrint('💊 [CapsuleToast] 弹出: $message');
|
||
|
||
// 延到下一帧再插入:页面退出(dispose)瞬间 Overlay 可能正在重建,
|
||
// 直接插入可能随旧页面一起被销毁。
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
_insertEntry(message, overlay, duration: duration, showCheck: showCheck);
|
||
});
|
||
}
|
||
|
||
/// 真正插入 OverlayEntry
|
||
static void _insertEntry(
|
||
String message,
|
||
OverlayState overlay, {
|
||
required Duration duration,
|
||
required bool showCheck,
|
||
}) {
|
||
if (!overlay.mounted) return;
|
||
|
||
// 有新的提示时先移除旧的
|
||
if (_showing) {
|
||
_entry?.remove();
|
||
_showing = false;
|
||
}
|
||
|
||
_entry = OverlayEntry(
|
||
builder: (context) => IgnorePointer(
|
||
child: SafeArea(
|
||
child: Align(
|
||
alignment: Alignment.topCenter,
|
||
child: Container(
|
||
margin: const EdgeInsets.only(top: 64),
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xE62C2C2C),
|
||
borderRadius: BorderRadius.circular(100),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.15),
|
||
blurRadius: 8,
|
||
offset: const Offset(0, 2),
|
||
),
|
||
],
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
if (showCheck) ...[
|
||
const Icon(
|
||
Icons.check_circle,
|
||
size: 14,
|
||
color: Color(0xFF4CD964),
|
||
),
|
||
const SizedBox(width: 6),
|
||
],
|
||
Text(
|
||
message,
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
overlay.insert(_entry!);
|
||
_showing = true;
|
||
|
||
Future.delayed(duration, () {
|
||
if (_showing) {
|
||
_entry?.remove();
|
||
_showing = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
/// 从指定 Element 开始深度优先遍历子树,找到第一个 OverlayState
|
||
static OverlayState? _findOverlayState(Element root) {
|
||
OverlayState? result;
|
||
void visitor(Element element) {
|
||
if (result != null) return;
|
||
if (element is StatefulElement && element.state is OverlayState) {
|
||
result = element.state as OverlayState;
|
||
return;
|
||
}
|
||
element.visitChildElements(visitor);
|
||
}
|
||
|
||
root.visitChildElements(visitor);
|
||
return result;
|
||
}
|
||
}
|