rtc_prd/airhub_app/lib/widgets/ios_toast.dart
seaislee1209 f9666d4aa3 feat: UI规范化 + 故事吸入动画 + 音乐页面优化
- 全局字体统一(Outfit/DM Sans), 头部/按钮/Toast规范化
- 故事详情页: Genie Suck吸入动画(标题+卡片一起缩小模糊消失)
- 书架页: bookPop弹出+粒子效果(三段式动画完整链路)
- 音乐页面: 心情卡片emoji换Material图标+彩色圆块横排布局
- 音乐页面: 进度条胶囊宽度对齐, 播放按钮位置修复, 间距均匀化
- 音乐播放: 接入just_audio, 支持播放暂停进度拖拽自动切歌
- 新增: iOS风格毛玻璃Toast, 渐变背景组件, 通知页面
- 阶段总结文档更新

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 19:34:53 +08:00

177 lines
5.1 KiB
Dart
Raw Permalink 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 '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<double> _slideAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 350),
reverseDuration: const Duration(milliseconds: 250),
vsync: this,
);
_slideAnimation = Tween<double>(begin: -1.0, end: 0.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
);
_fadeAnimation = Tween<double>(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),
),
),
),
],
),
),
),
),
),
),
),
);
}
}