import 'dart:ui'; import 'package:flutter/material.dart'; /// 全局 iOS 风格顶部浮动 Toast /// 用法: AppToast.show(context, '提示内容'); /// AppToast.show(context, '出错了', isError: true); class AppToast { static OverlayEntry? _currentEntry; /// 显示 toast,自动消失 static void show( BuildContext context, String message, { bool isError = false, }) { // 如果已有 toast,先移除 _currentEntry?.remove(); _currentEntry = null; final overlay = Overlay.of(context); late OverlayEntry entry; entry = OverlayEntry( builder: (context) => _IOSToastWidget( message: message, isError: isError, onDismiss: () { entry.remove(); if (_currentEntry == entry) _currentEntry = null; }, ), ); _currentEntry = entry; overlay.insert(entry); } } class _IOSToastWidget extends StatefulWidget { final String message; final bool isError; final VoidCallback onDismiss; const _IOSToastWidget({ required this.message, required this.isError, required this.onDismiss, }); @override State<_IOSToastWidget> createState() => _IOSToastWidgetState(); } class _IOSToastWidgetState extends State<_IOSToastWidget> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _slideAnimation; late Animation _fadeAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 350), reverseDuration: const Duration(milliseconds: 250), vsync: this, ); _slideAnimation = Tween(begin: -1.0, end: 0.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic), ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeOut), ); _controller.forward(); // Auto dismiss after 2.5s Future.delayed(const Duration(milliseconds: 2500), () { if (mounted) { _controller.reverse().then((_) { if (mounted) widget.onDismiss(); }); } }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final topPadding = MediaQuery.of(context).padding.top; return Positioned( top: topPadding + 12, left: 24, right: 24, child: AnimatedBuilder( animation: _controller, builder: (context, child) { return Transform.translate( offset: Offset(0, _slideAnimation.value * 60), child: Opacity( opacity: _fadeAnimation.value, child: child, ), ); }, child: Center( child: ClipRRect( borderRadius: BorderRadius.circular(16), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), decoration: BoxDecoration( color: widget.isError ? const Color(0xFFFEF2F2).withOpacity(0.85) : Colors.white.withOpacity(0.85), borderRadius: BorderRadius.circular(16), border: Border.all( color: widget.isError ? const Color(0xFFFCA5A5).withOpacity(0.4) : Colors.white.withOpacity(0.6), ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), offset: const Offset(0, 4), blurRadius: 20, ), ], ), child: DefaultTextStyle( style: const TextStyle(decoration: TextDecoration.none), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( widget.isError ? Icons.info_outline_rounded : Icons.check_circle_outline_rounded, size: 20, color: widget.isError ? const Color(0xFFEF4444) : const Color(0xFF22C55E), ), const SizedBox(width: 10), Flexible( child: Text( widget.message, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: widget.isError ? const Color(0xFFDC2626) : const Color(0xFF374151), ), ), ), ], ), ), ), ), ), ), ), ); } }