当前位置:   article > 正文

[Flutter]自定义等待转圈和Toast提示

[Flutter]自定义等待转圈和Toast提示

1.自定义样式

2.自定义LoadingView

  1. import 'package:flutter/material.dart';
  2. enum LoadingStyle {
  3. onlyIndicator, // 仅一个转圈等待
  4. roundedRectangle, // 添加一个圆角矩形当背景
  5. maskingOperation, // 添加一个背景蒙层, 阻止用户操作
  6. }
  7. class LoadingView {
  8. static final LoadingView _singleton = LoadingView._internal();
  9. factory LoadingView() {
  10. return _singleton;
  11. }
  12. LoadingView._internal();
  13. OverlayEntry? _overlayEntry;
  14. void show(BuildContext context, {LoadingStyle type = LoadingStyle.onlyIndicator}) {
  15. if (_overlayEntry == null) {
  16. _overlayEntry = _createOverlayEntry(type);
  17. Overlay.of(context).insert(_overlayEntry!);
  18. }
  19. }
  20. void hide() {
  21. _overlayEntry?.remove();
  22. _overlayEntry = null;
  23. }
  24. OverlayEntry _createOverlayEntry(LoadingStyle type) => OverlayEntry(
  25. builder: (BuildContext context) {
  26. List<Widget> stackChildren = [];
  27. if (type == LoadingStyle.roundedRectangle) {
  28. stackChildren.add(
  29. Center(
  30. child: Container(
  31. width: 100,
  32. height: 100,
  33. padding: const EdgeInsets.all(20.0),
  34. decoration: BoxDecoration(
  35. color: Colors.white,
  36. borderRadius: BorderRadius.circular(8.0),
  37. ),
  38. child: const Align(
  39. alignment: Alignment.center,
  40. child: SizedBox(
  41. width: 45,
  42. height: 45,
  43. child: CircularProgressIndicator(
  44. color: Colors.black,
  45. ),
  46. ),
  47. ),
  48. ),
  49. ),
  50. );
  51. } else if (type == LoadingStyle.maskingOperation) {
  52. stackChildren.addAll([
  53. const Opacity(
  54. opacity: 0.5,
  55. child: ModalBarrier(dismissible: false, color: Colors.black),
  56. ),
  57. const Center(child: CircularProgressIndicator()),
  58. ]);
  59. } else {
  60. stackChildren.add(
  61. const Center(child: CircularProgressIndicator()),
  62. );
  63. }
  64. return Stack(children: stackChildren);
  65. },
  66. );
  67. }

3.自定义ToastView

  1. import 'package:flutter/material.dart';
  2. enum ToastPosition { center, bottom }
  3. class ToastView {
  4. static OverlayEntry? _overlayEntry;
  5. static void showToast(
  6. BuildContext context,
  7. String message, {
  8. ToastPosition position = ToastPosition.center,
  9. int second = 2,
  10. Color backgroundColor = Colors.white,
  11. Color textColor = Colors.black,
  12. double horizontalMargin = 16,
  13. EdgeInsetsGeometry padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
  14. }) {
  15. _overlayEntry?.remove();
  16. _overlayEntry = OverlayEntry(
  17. builder: (context) => ToastWidget(
  18. message: message,
  19. position: position,
  20. backgroundColor: backgroundColor,
  21. textColor: textColor,
  22. horizontalMargin: horizontalMargin,
  23. padding: padding,
  24. ),
  25. );
  26. Overlay.of(context)?.insert(_overlayEntry!);
  27. Future.delayed(Duration(seconds: second), () {
  28. _overlayEntry?.remove();
  29. _overlayEntry = null;
  30. });
  31. }
  32. }
  33. class ToastWidget extends StatelessWidget {
  34. final String message;
  35. final ToastPosition position;
  36. final Color backgroundColor;
  37. final Color textColor;
  38. final double horizontalMargin;
  39. final EdgeInsetsGeometry padding;
  40. const ToastWidget({
  41. Key? key,
  42. required this.message,
  43. required this.position,
  44. required this.backgroundColor,
  45. required this.textColor,
  46. required this.horizontalMargin,
  47. required this.padding,
  48. }) : super(key: key);
  49. @override
  50. Widget build(BuildContext context) {
  51. return Positioned(
  52. top: position == ToastPosition.center ? MediaQuery.of(context).size.height / 2 : null,
  53. bottom: position == ToastPosition.bottom ? 50.0 : null,
  54. left: horizontalMargin,
  55. right: horizontalMargin,
  56. child: Material(
  57. color: Colors.transparent,
  58. child: Align(
  59. alignment: position == ToastPosition.center ? Alignment.center : Alignment.bottomCenter,
  60. child: FittedBox(
  61. fit: BoxFit.scaleDown,
  62. child: Container(
  63. padding: padding,
  64. decoration: BoxDecoration(
  65. color: backgroundColor,
  66. borderRadius: BorderRadius.circular(8),
  67. ),
  68. child: Text(
  69. message,
  70. textAlign: TextAlign.center,
  71. style: TextStyle(
  72. fontSize: 15,
  73. color: textColor,
  74. ),
  75. ),
  76. ),
  77. ),
  78. ),
  79. ),
  80. );
  81. }
  82. }

4.创建一个全局的回调管理AlertCallbackManager

经过上面自定义视图,我们注意到,视图的展示都需要BuildContext context。若是这样的话,就强行将弹窗视图的逻辑绑定到了具体的某个组件上,导致组件销毁时弹窗也必须销毁。否则,context都消失了,你又如何去处理插入其中的视图?

我们往往需要,让等待转圈在离开页面后还继续展示,让Toast在关闭页面时也不被影响到其提示的时长。

所以,这里我们用了一个全局回调管理。

  1. enum AlertCallbackType { none, showLoading, hideLoading, showToast }
  2. class AlertCallbackManager {
  3. // 私有构造函数
  4. AlertCallbackManager._privateConstructor();
  5. // 单例实例
  6. static final AlertCallbackManager _instance = AlertCallbackManager._privateConstructor();
  7. // 获取单例实例的方法
  8. static AlertCallbackManager get instance => _instance;
  9. // 定义闭包类型的回调函数
  10. Function(AlertCallbackType type, String message)? callback;
  11. }

5.创建一个根组件,将等待加载和Toast提示当作公共逻辑来处理。

有了全局回调管理,我们还需要有一个不会被轻易销毁的根组件,来提供BuildContext context。

注意:全局提示回调, 要放在MaterialApp包装之后,因为这里的LoadingView实现方式需要放在MaterialApp之下。

  1. void main() async {
  2. WidgetsFlutterBinding.ensureInitialized();
  3. runApp(const MyApp());
  4. }
  5. // MARK: 用来包装MaterialApp
  6. class MyApp extends StatelessWidget {
  7. const MyApp({super.key});
  8. @override
  9. Widget build(BuildContext context) {
  10. return const MaterialApp(
  11. title: 'Flutter Demo',
  12. debugShowCheckedModeBanner: false, // 禁用调试标签
  13. home: BaseWidget(),
  14. );
  15. }
  16. }
  17. // MARK: 根组件 用来处理公用逻辑
  18. class BaseWidget extends StatefulWidget {
  19. const BaseWidget({super.key});
  20. @override
  21. State<BaseWidget> createState() => _BaseWidgetState();
  22. }
  23. class _BaseWidgetState extends State<BaseWidget> {
  24. @override
  25. void initState() {
  26. super.initState();
  27. // 提示回调, 要放在MaterialApp包装之后
  28. AlertCallbackManager.instance.callback = (type, message) async {
  29. if (mounted) { // 检查当前State是否仍然被挂载(即没有被dispose)
  30. if (type == AlertCallbackType.showLoading) {
  31. LoadingView().show(context);
  32. } else if (type == AlertCallbackType.hideLoading) {
  33. LoadingView().hide();
  34. } else if (type == AlertCallbackType.showToast) {
  35. ToastView.showToast(context, message);
  36. }
  37. }
  38. };
  39. }
  40. @override
  41. void dispose() {
  42. LoadingView().hide();
  43. super.dispose();
  44. }
  45. @override
  46. Widget build(BuildContext context) {
  47. return const HomePage();
  48. }
  49. }

然后在需要展示的地方,用如下方式调用,最好再进一步将方法封装得短一些。

  1. AlertCallbackManager.instance.callback?.call(AlertCallbackType.showLoading, "");
  2. AlertCallbackManager.instance.callback?.call(AlertCallbackType.hideLoading, "");
  3. AlertCallbackManager.instance.callback?.call(AlertCallbackType.showToast, "message");

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/227306
推荐阅读
相关标签
  

闽ICP备14008679号