129 lines
3.3 KiB
Dart
129 lines
3.3 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
const Key _toastKey = Key('centered-toast');
|
|
const Duration _fadeDuration = Duration(milliseconds: 200);
|
|
const Duration _visibleDuration = Duration(seconds: 2);
|
|
const double _toastRadius = 12;
|
|
const double _toastMaxWidth = 320;
|
|
const double _toastHorizontalInset = 20;
|
|
const double _toastVerticalInset = 12;
|
|
const double _toastBackgroundOpacity = 0.78;
|
|
const TextStyle _toastTextStyle = TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 14,
|
|
height: 1.4,
|
|
fontWeight: FontWeight.w500,
|
|
);
|
|
const EdgeInsets _toastPadding = EdgeInsets.symmetric(
|
|
horizontal: _toastHorizontalInset,
|
|
vertical: _toastVerticalInset,
|
|
);
|
|
|
|
OverlayEntry? _activeToast;
|
|
|
|
/// Shows a lightweight centered message and replaces any active toast.
|
|
void showCenteredToast(BuildContext context, String message) {
|
|
_removeActiveToast();
|
|
final overlay = Overlay.of(context, rootOverlay: true);
|
|
late final OverlayEntry entry;
|
|
entry = OverlayEntry(
|
|
builder: (_) => _CenteredToast(
|
|
message: message,
|
|
onDismissed: () {
|
|
if (!identical(_activeToast, entry)) return;
|
|
_activeToast = null;
|
|
if (entry.mounted) entry.remove();
|
|
},
|
|
),
|
|
);
|
|
_activeToast = entry;
|
|
overlay.insert(entry);
|
|
}
|
|
|
|
void _removeActiveToast() {
|
|
final entry = _activeToast;
|
|
_activeToast = null;
|
|
if (entry != null && entry.mounted) entry.remove();
|
|
}
|
|
|
|
class _CenteredToast extends StatefulWidget {
|
|
const _CenteredToast({required this.message, required this.onDismissed});
|
|
|
|
final String message;
|
|
final VoidCallback onDismissed;
|
|
|
|
@override
|
|
State<_CenteredToast> createState() => _CenteredToastState();
|
|
}
|
|
|
|
class _CenteredToastState extends State<_CenteredToast>
|
|
with SingleTickerProviderStateMixin {
|
|
late final AnimationController _opacity = AnimationController(
|
|
vsync: this,
|
|
duration: _fadeDuration,
|
|
);
|
|
Timer? _dismissTimer;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_opacity.forward();
|
|
_dismissTimer = Timer(_visibleDuration, _dismiss);
|
|
}
|
|
|
|
Future<void> _dismiss() async {
|
|
try {
|
|
await _opacity.reverse();
|
|
} on TickerCanceled {
|
|
return;
|
|
}
|
|
if (mounted) widget.onDismissed();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_dismissTimer?.cancel();
|
|
_opacity.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Positioned.fill(
|
|
child: IgnorePointer(
|
|
child: Center(
|
|
child: FadeTransition(
|
|
opacity: CurvedAnimation(
|
|
parent: _opacity,
|
|
curve: Curves.easeOutCubic,
|
|
),
|
|
child: Material(
|
|
type: MaterialType.transparency,
|
|
child: Container(
|
|
key: _toastKey,
|
|
constraints: const BoxConstraints(maxWidth: _toastMaxWidth),
|
|
padding: _toastPadding,
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withValues(
|
|
alpha: _toastBackgroundOpacity,
|
|
),
|
|
borderRadius: BorderRadius.circular(_toastRadius),
|
|
),
|
|
child: Text(
|
|
widget.message,
|
|
textAlign: TextAlign.center,
|
|
style: _toastTextStyle,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|