Files
flutter-template/lib/core/widgets/app_toast.dart
T
SkyJourney 61017f1c39 chore: 初始化脚手架 — sunny_mochi 企业级 Flutter 模板
Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库
7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系
5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter

通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
2026-05-14 12:51:05 +08:00

174 lines
5.0 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
enum ToastType { info, success, error, warning }
abstract final class AppToast {
static OverlayEntry? _current;
static void show(
BuildContext context,
String message, {
ToastType type = ToastType.info,
Duration duration = const Duration(seconds: 4),
}) {
_current?.remove();
_current = null;
final overlay = Overlay.of(context, rootOverlay: true);
late OverlayEntry entry;
entry = OverlayEntry(
builder: (_) => _ToastBanner(
message: message,
type: type,
duration: duration,
onDismiss: () {
entry.remove();
if (_current == entry) _current = null;
},
),
);
_current = entry;
overlay.insert(entry);
}
}
class _ToastBanner extends StatefulWidget {
const _ToastBanner({
required this.message,
required this.type,
required this.duration,
required this.onDismiss,
});
final String message;
final ToastType type;
final Duration duration;
final VoidCallback onDismiss;
@override
State<_ToastBanner> createState() => _ToastBannerState();
}
class _ToastBannerState extends State<_ToastBanner>
with SingleTickerProviderStateMixin {
late final AnimationController _ctrl;
late final Animation<Offset> _slide;
late final Animation<double> _fade;
Timer? _timer;
@override
void initState() {
super.initState();
_ctrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 280),
);
_slide = Tween<Offset>(
begin: const Offset(0, -1.2),
end: Offset.zero,
).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOutCubic));
_fade = CurvedAnimation(parent: _ctrl, curve: Curves.easeOut);
unawaited(_ctrl.forward());
_timer = Timer(widget.duration, _dismiss);
}
void _dismiss() {
_timer?.cancel();
unawaited(_ctrl.reverse().then((_) => widget.onDismiss()));
}
@override
void dispose() {
_timer?.cancel();
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final (accentColor, icon) = switch (widget.type) {
ToastType.success =>
(const Color(0xFF34C759), Icons.check_circle_outline_rounded),
ToastType.error =>
(const Color(0xFFFF3B30), Icons.error_outline_rounded),
ToastType.warning =>
(const Color(0xFFFF9F0A), Icons.warning_amber_rounded),
ToastType.info =>
(const Color(0xFF6B63D9), Icons.info_outline_rounded),
};
final topPadding = MediaQuery.of(context).padding.top;
return Positioned(
top: topPadding + 10.h,
left: 16.w,
right: 16.w,
child: SlideTransition(
position: _slide,
child: FadeTransition(
opacity: _fade,
child: Material(
elevation: 12,
shadowColor: Colors.black26,
borderRadius: BorderRadius.circular(14.r),
color: Colors.white,
child: ClipRRect(
borderRadius: BorderRadius.circular(14.r),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(width: 4.w, color: accentColor),
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 14.w,
vertical: 13.h,
),
child: Row(
children: [
Icon(icon, color: accentColor, size: 20.sp),
SizedBox(width: 10.w),
Expanded(
child: Text(
widget.message,
style: TextStyle(
color: const Color(0xFF1C1C1E),
fontSize: 14.sp,
fontWeight: FontWeight.w500,
height: 1.4,
),
),
),
SizedBox(width: 8.w),
GestureDetector(
onTap: _dismiss,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.close_rounded,
color: const Color(0xFFAEAEB2),
size: 16.sp,
),
),
),
],
),
),
),
],
),
),
),
),
),
),
);
}
}