[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");
  
相关推荐
许进进16 小时前
FlutterWeb渲染模式及提速
android·flutter·web
喵个咪17 小时前
Flutter 使用 RxDart & Streams 实现 BLoC模式
前端·flutter·reactivex
iFlyCai1 天前
Flutter本地数据持久化的几种方式
flutter
snwrking2 天前
各个版本Android上的Location与Notification权限的问题
android·flutter
头好晕呀2 天前
Flutter+WebRTC开发点对点加密即时通讯APP--好友列表界面实现
android·前端·flutter
coder_pig3 天前
跟🤡杰哥一起学Flutter (十九、Flutter混编杂谈[Android]😫)
android·flutter·harmonyos
鳄鱼不怕_牙医不怕4 天前
Flutter 源码梳理系列(十):BuildOwner class
flutter·源码阅读
甜瓜看代码4 天前
让你的切换丝滑起来,Hero animations的介绍
flutter
啊猪是的读来过倒4 天前
Vue 全局状态管理新宠:Pinia实战指南
前端·vue.js·flutter·pinia·全局状态管理
偷野的程咬金5 天前
了解flutter中SingleTickerProviderStateMixin的使用
flutter