[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");
  
相关推荐
我命由我1234517 小时前
Dart - Dart SDK、Hello World 案例、变量声明、常量声明、常量 final、字符串类型
前端·flutter·前端框架·html·web·dart·web app
xmdy586618 小时前
Flutter+开源鸿蒙实战|校园易生活Day7 个人中心完善+我的发布/收藏+退出登录+主题切换+全局UI美化(项目闭环)
flutter·开源·harmonyos
xmdy586620 小时前
Flutter + 开源鸿蒙实战|城市智慧停车管理系统 Day1 项目初始化+架构搭建+全局依赖集成+多端适配基座
flutter·开源·harmonyos
恋猫de小郭21 小时前
AndroidX 将引入有全新 AppState ,用于管理 Compose 状态
android·前端·flutter
Zender Han21 小时前
Flutter 轻量存储方案介绍、区别、对比和使用场景
android·flutter·ios
东坡肘子21 小时前
CocoaPods 正在退场,SwiftPM 才刚到第二章 -- 肘子的 Swift 周报 #135
flutter·swiftui·swift
xmdy58661 天前
Flutter + 开源鸿蒙实战|城市智慧停车管理系统 Day3 车场详情+车位预约+计时计费算法+路线导航+常用车场缓存持久化
flutter·开源·harmonyos
xmdy58661 天前
Flutter+开源鸿蒙实战|城市共享驿站智能存取系统 Day6 全局UI精细化美化+通用组件封装+反馈设置模块+隐私弹窗+鸿蒙打包签名适配+项目整体重构
flutter·开源·harmonyos
WaywardOne2 天前
Flutter面试事件队列,微任务队列以及事件循环相关问题及回答
flutter·面试
明君879972 天前
Flutter 包体积优化实战:从 175MB 到 105MB
flutter