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

1.自定义样式

2.自定义LoadingView

Dart 复制代码
import 'package:flutter/material.dart';

enum LoadingStyle {
  onlyIndicator, // 仅一个转圈等待
  roundedRectangle, // 添加一个圆角矩形当背景
  maskingOperation, // 添加一个背景蒙层, 阻止用户操作
}

class LoadingView {
  static final LoadingView _singleton = LoadingView._internal();

  factory LoadingView() {
    return _singleton;
  }

  LoadingView._internal();

  OverlayEntry? _overlayEntry;  

  void show(BuildContext context, {LoadingStyle type = LoadingStyle.onlyIndicator}) {
    if (_overlayEntry == null) {
      _overlayEntry = _createOverlayEntry(type);
      Overlay.of(context).insert(_overlayEntry!);  
    }
  }

  void hide() {
    _overlayEntry?.remove();
    _overlayEntry = null;
  }

  OverlayEntry _createOverlayEntry(LoadingStyle type) => OverlayEntry(
        builder: (BuildContext context) {
          List<Widget> stackChildren = [];
          if (type == LoadingStyle.roundedRectangle) {
            stackChildren.add(
              Center(
                child: Container(
                  width: 100,
                  height: 100,
                  padding: const EdgeInsets.all(20.0),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(8.0),
                  ),
                  child: const Align(
                    alignment: Alignment.center,
                    child: SizedBox(
                      width: 45,
                      height: 45,
                      child: CircularProgressIndicator(
                        color: Colors.black,
                      ),
                    ),
                  ),
                ),
              ),
            );
          } else if (type == LoadingStyle.maskingOperation) {
            stackChildren.addAll([
              const Opacity(
                opacity: 0.5,
                child: ModalBarrier(dismissible: false, color: Colors.black),
              ),
              const Center(child: CircularProgressIndicator()),
            ]);
          } else {
            stackChildren.add(
              const Center(child: CircularProgressIndicator()),
            );
          }

          return Stack(children: stackChildren);
        },
      );
}

3.自定义ToastView

Dart 复制代码
import 'package:flutter/material.dart';

enum ToastPosition { center, bottom }

class ToastView {
  static OverlayEntry? _overlayEntry;

  static void showToast(
    BuildContext context,
    String message, {
    ToastPosition position = ToastPosition.center,
    int second = 2,
    Color backgroundColor = Colors.white,
    Color textColor = Colors.black,
    double horizontalMargin = 16,
    EdgeInsetsGeometry padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
  }) {
    _overlayEntry?.remove();

    _overlayEntry = OverlayEntry(
      builder: (context) => ToastWidget(
        message: message,
        position: position,
        backgroundColor: backgroundColor,
        textColor: textColor,
        horizontalMargin: horizontalMargin,
        padding: padding,
      ),
    );

    Overlay.of(context)?.insert(_overlayEntry!);

    Future.delayed(Duration(seconds: second), () {
      _overlayEntry?.remove();
      _overlayEntry = null;
    });
  }
}

class ToastWidget extends StatelessWidget {
  final String message;
  final ToastPosition position;
  final Color backgroundColor;
  final Color textColor;
  final double horizontalMargin;
  final EdgeInsetsGeometry padding;

  const ToastWidget({
    Key? key,
    required this.message,
    required this.position,
    required this.backgroundColor,
    required this.textColor,
    required this.horizontalMargin,
    required this.padding,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Positioned(
      top: position == ToastPosition.center ? MediaQuery.of(context).size.height / 2 : null,
      bottom: position == ToastPosition.bottom ? 50.0 : null,
      left: horizontalMargin,
      right: horizontalMargin,
      child: Material(
        color: Colors.transparent,
        child: Align(
          alignment: position == ToastPosition.center ? Alignment.center : Alignment.bottomCenter,
          child: FittedBox(
            fit: BoxFit.scaleDown,
            child: Container(
              padding: padding,
              decoration: BoxDecoration(
                color: backgroundColor,
                borderRadius: BorderRadius.circular(8),
              ),
              child: Text(
                message,
                textAlign: TextAlign.center,
                style: TextStyle(
                  fontSize: 15,
                  color: textColor,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

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

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

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

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

Dart 复制代码
enum AlertCallbackType { none, showLoading, hideLoading, showToast }

class AlertCallbackManager {
  // 私有构造函数
  AlertCallbackManager._privateConstructor();

  // 单例实例
  static final AlertCallbackManager _instance = AlertCallbackManager._privateConstructor();

  // 获取单例实例的方法
  static AlertCallbackManager get instance => _instance;

  // 定义闭包类型的回调函数
  Function(AlertCallbackType type, String message)? callback;
}

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

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

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

Dart 复制代码
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

// MARK: 用来包装MaterialApp
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false, // 禁用调试标签
      home: BaseWidget(),
    );
  }
}

// MARK: 根组件 用来处理公用逻辑
class BaseWidget extends StatefulWidget {
  const BaseWidget({super.key});

  @override
  State<BaseWidget> createState() => _BaseWidgetState();
}

class _BaseWidgetState extends State<BaseWidget> {
  @override
  void initState() {
    super.initState();
    // 提示回调, 要放在MaterialApp包装之后
    AlertCallbackManager.instance.callback = (type, message) async {
      if (mounted) { // 检查当前State是否仍然被挂载(即没有被dispose)
        if (type == AlertCallbackType.showLoading) {
          LoadingView().show(context);
        } else if (type == AlertCallbackType.hideLoading) {
          LoadingView().hide();
        } else if (type == AlertCallbackType.showToast) {
          ToastView.showToast(context, message);
        }
      }
    };
  }

  @override
  void dispose() {
    LoadingView().hide();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return const HomePage();
  }
}

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

Dart 复制代码
AlertCallbackManager.instance.callback?.call(AlertCallbackType.showLoading, "");

AlertCallbackManager.instance.callback?.call(AlertCallbackType.hideLoading, "");
 
AlertCallbackManager.instance.callback?.call(AlertCallbackType.showToast, "message");
  
相关推荐
比格丽巴格丽抱1 小时前
flutter项目苹果编译运行打包上线
flutter·ios
AiFlutter5 小时前
Flutter通过 Coap发送组播
flutter
嘟嘟叽1 天前
初学 flutter 环境变量配置
flutter
iFlyCai1 天前
深入理解Flutter生命周期函数之StatefulWidget(一)
flutter·生命周期·dart·statefulwidget
sunly_1 天前
Flutter:photo_view图片预览功能
android·javascript·flutter
Summer不秃2 天前
Flutter中sqflite的使用案例
flutter
sunly_2 天前
Flutter:TweenAnimationBuilder自定义隐式动画
flutter
AiFlutter2 天前
Flutter-Web首次加载时添加动画
前端·flutter
Allen Su2 天前
【Flutter 问题系列第 84 篇】如何清除指定网络图片的缓存
flutter·缓存·如何清除指定网络图片的缓存·网络图片缓存
sunly_2 天前
Flutter:key的作用原理(LocalKey ,GlobalKey)
开发语言·javascript·flutter