Files
tuiche/lib/component/tool/TopSlideNotification.dart
2025-11-13 09:56:02 +08:00

201 lines
5.2 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';
import 'package:flutter/scheduler.dart';
import 'package:get/get.dart';
import 'package:vbvs_app/common/color/AppGlobal.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/controller/theme_controller/ThemeController.dart';
class TopSlideNotification extends StatefulWidget {
BuildContext? context;
final String text;
double? fontSize = 26.rpx;
Color? textColor;
double? slideOffset = 200;
final Duration duration;
TopSlideNotification({
super.key,
this.text = '操作成功!',
this.fontSize,
this.textColor,
this.slideOffset,
this.duration = const Duration(seconds: 2),
});
static OverlayEntry? _currentEntry; // 单例 OverlayEntry
static bool _isShowing = false;
static void show(
BuildContext context, {
String text = '操作成功!',
double fontSize = 16,
Color? textColor,
double slideOffset = 300.0,
Duration duration = const Duration(seconds: 2),
}) {
_showInternal(
context: context,
text: text,
fontSize: fontSize,
textColor: textColor,
slideOffset: slideOffset,
duration: duration,
);
}
// Internal implementation used by both methods
static void _showInternal({
BuildContext? context,
required String text,
required double fontSize,
Color? textColor,
required double slideOffset,
required Duration duration,
}) {
// 如果已有弹窗,先移除
_removeCurrentEntry();
// 获取有效的context优先使用传入的context没有则使用navigatorKey的context
final effectiveContext = context ?? AppGlobal.navigatorKey.currentContext;
if (effectiveContext == null) {
debugPrint('TopSlideNotification: No valid context available');
return;
}
final overlay = Overlay.of(effectiveContext);
if (overlay == null) {
debugPrint('TopSlideNotification: Could not find Overlay');
return;
}
final entry = OverlayEntry(
builder: (_) => TopSlideNotification(
text: text,
fontSize: fontSize,
textColor: textColor,
slideOffset: slideOffset,
duration: duration,
),
);
_currentEntry = entry;
_isShowing = true;
overlay.insert(entry);
// 自动移除
Future.delayed(duration + const Duration(milliseconds: 500), () {
_removeCurrentEntry();
});
}
static void _removeCurrentEntry() {
if (_currentEntry != null && _isShowing) {
_currentEntry!.remove();
_currentEntry = null;
_isShowing = false;
}
}
@override
State<TopSlideNotification> createState() => _TopSlideNotificationState();
}
class _TopSlideNotificationState extends State<TopSlideNotification>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _animation;
bool _isAnimating = false; // 标志位,控制是否正在动画中
@override
void initState() {
super.initState();
// 初始化 AnimationController
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
// 动画初始化完成后调用
SchedulerBinding.instance.addPostFrameCallback((_) {
_startAnimation(); // 调用动画启动方法
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 获取屏幕高度,用于计算动画的偏移值
final screenHeight = MediaQuery.of(context).size.height;
final offsetValue = widget.slideOffset! / screenHeight;
// 设置动画
_animation =
Tween<Offset>(begin: const Offset(0, -1), end: Offset(0, offsetValue))
.animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
));
}
@override
void dispose() {
// 确保动画控制器在组件销毁时被释放
_controller.dispose();
super.dispose();
}
Color get _textColor {
return widget.textColor ?? Get.find<ThemeController>().currentColor.sc2;
}
@override
Widget build(BuildContext context) {
return Positioned(
top: 140.rpx,
left: 0,
right: 0,
child: SlideTransition(
position: _animation,
child: Material(
color: stringToColor("#000000").withOpacity(0.8),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 20.rpx),
child: Container(
child: Text(
widget.text,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: widget.fontSize,
color: _textColor,
),
),
),
),
),
),
);
}
// 执行动画
Future<void> _startAnimation() async {
if (_isAnimating) return; // 如果正在动画中,则不执行新的动画
_isAnimating = true; // 标记动画开始
try {
await _controller.forward();
await Future.delayed(widget.duration);
// 只有在组件仍然挂载时才执行 reverse 动作
if (mounted) {
await _controller.reverse();
}
} finally {
_isAnimating = false; // 动画完成后,标记动画结束
}
}
}