[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");
  
相关推荐
Lanren的编程日记15 分钟前
Flutter鸿蒙应用开发:基础UI组件库设计与实现实战
flutter·ui·harmonyos
西西学代码24 分钟前
Flutter---波形动画
flutter
于慨4 小时前
flutter基础组件用法
开发语言·javascript·flutter
恋猫de小郭6 小时前
Android CLI ,谷歌为 Android 开发者专研的 AI Agent,提速三倍
android·前端·flutter
火柴就是我7 小时前
flutter pushAndRemoveUntil 的一次小疑惑
flutter
于慨7 小时前
flutter doctor问题解决
flutter
唔667 小时前
flutter 图片加载类 图片的安全使用
安全·flutter
Nathan202406169 小时前
Flutter - InheritedWidget
flutter·dart
恋猫de小郭9 小时前
JetBrains Amper 0.10 ,期待它未来替代 Gradle
android·前端·flutter
Lanren的编程日记10 小时前
Flutter鸿蒙应用开发:实时聊天功能集成实战
flutter·华为·harmonyos