Files
feature-next-arch/lib/components/capsule_toast.dart
Songzex 7c01229b9e 1:修复远程遥控控制状态显示不正确的问题
2:更换端口映射为测试服的8081-59003,9001-59004
2026-08-26 14:23:20 +08:00

126 lines
3.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}