Flutter--不受Navigator影响的自定义弹窗

解决了什么问题:

  1. 原生的showDialog基于Navigator弹出和关闭,在具有一定复杂度的业务中无法判断你在pop谁。我这个方案每个弹窗都有一个对应的实例可以控制,且不会被Navigatorpop方法影响。
  2. 优化了对于Loading业务的弹窗使用,原生弹窗很难在非线性流程下控制关闭弹窗。

样式展示

显示一般类型的Alert

Dart 复制代码
DialogController alert = DialogController.alert(
  title: "Title",
  subTitle: "SubTitle",
  onCancel: () {
    print("alert cancle");
  },
  run: () async {
    // 一些耗时操作
    return true;
  },
);
alert.show(context);

显示加载类型的Alert

Dart 复制代码
DialogController loadingAlert = DialogController.loadingAlert();
loadingAlert.showWithTimeout(context, timeout: 10);
// await 其他耗时操作 
loadingAlert.close();

实现代码,这里只是提供一个思路,更多样式按照这个规范拓展即可

Dart 复制代码
import 'dart:async';

import 'package:flutter/material.dart';

class BaseDialog {
  BaseDialog(this._barrierDismissible, this._alignment);

  /// 点击背景是否关闭弹窗
  final bool _barrierDismissible;
  final AlignmentGeometry _alignment;

  /// 页面状态,用来做动画判断
  bool _isCloseState = true;

  /// 动画时长
  final _milliseconds = 240;

  /// 初始化dialog的内容
  /// [isClose]用来标识动画的状态
  /// [milliseconds]用来标识动画时长
  initContentView(Widget Function(BuildContext context, bool isClose, int milliseconds) builder) {
    _overlayEntry = OverlayEntry(
      builder: (context) {
        return Stack(
          alignment: _alignment,
          children: <Widget>[
            // 背景
            Positioned.fill(
              child: GestureDetector(
                onTap: () {
                  // 点击背景关闭页面
                  if (_barrierDismissible) close();
                },
                child: AnimatedOpacity(
                  opacity: _isCloseState ? 0.0 : 1,
                  duration: Duration(milliseconds: _milliseconds),
                  child: Container(
                    color: Colors.black.withOpacity(0.5),
                  ),
                ),
              ),
            ),
            builder(context, _isCloseState, _milliseconds),
          ],
        );
      },
    );
  }

  late OverlayEntry _overlayEntry;
  bool _isPop = true;

  /// 显示弹窗
  /// 小等于0不设置超时
  void show(BuildContext context, int timeout) async {
    //显示弹窗
    Overlay.of(context).insert(_overlayEntry);
    // 稍微延迟一下,不然动画不动
    await Future.delayed(const Duration(milliseconds: 10));
    _isCloseState = false;
    // 重新build启动动画
    _overlayEntry.markNeedsBuild();
    _isPop = true;
    // 启动计时器,timeout秒后执行关闭操作
    if (timeout > 0) {
      Future.delayed(Duration(seconds: timeout), () => close());
    }
  }

  /// 关闭弹窗
  Future<void> close() async {
    if (_isPop) {
      _isPop = false;
      _isCloseState = true;
      // 重新build启动动画
      _overlayEntry.markNeedsBuild();
      // 等待动画结束后再移除涂层
      await Future.delayed(Duration(milliseconds: _milliseconds));
      _overlayEntry.remove();
      onClose();
    }
  }

  void Function() onClose = (){};
}

class DialogController {
  DialogController(this._baseDialog);

  final BaseDialog _baseDialog;

  /// 关闭弹窗
  close() {
    _baseDialog.close();
  }

  /// 显示弹窗
  show(BuildContext context) {
    _baseDialog.show(context, 0);
  }

  /// 显示一个默认带超时的弹窗
  /// 小等于0不设置超时
  void showWithTimeout(BuildContext context, {int timeout = 20}) {
    _baseDialog.show(context, timeout);
  }

  /// 创造一个普通样式的alert弹窗
  /// 它显示在屏幕中央,具有一个标题和内容描述文本,
  /// [onBarrierTap]当点击背景时触发
  factory DialogController.alert({
    required String title,
    required String subTitle,
    bool barrierDismissible = true,
    Future<bool> Function()? run,
    void Function()? onCancel,
  }) {
    final dialog = BaseDialog(
      barrierDismissible,
      AlignmentDirectional.center,
    );
    if (onCancel != null) {
      dialog.onClose = onCancel;
    }
    dialog.initContentView((context, isClose, int milliseconds) {
      return AnimatedOpacity(
        opacity: isClose ? 0.0 : 1,
        duration: Duration(milliseconds: milliseconds),
        child: AlertDialog(
          title: Text(title),
          content: Text(subTitle),
          actions:  [
            FilledButton.tonal(
              onPressed: () {
                dialog.close();
              },
              child: const Text("取消"),
            ),
            FilledButton(
              onPressed: () async {
                if (run != null) {
                  final r = await run();
                  if (r) dialog.close();
                } else {
                  dialog.close();
                }
              },
              child: const Text("确认"),
            )
          ],
        ),
      );
    });
    return DialogController(dialog);
  }

  factory DialogController.loadingAlert({
    String? title,
    String? subTitle,
  }) {
    final dialog = BaseDialog(
      false,
      AlignmentDirectional.center,
    );
    dialog.initContentView((context, isClose, int milliseconds) {
      return AnimatedOpacity(
        opacity: isClose ? 0.0 : 1,
        duration: Duration(milliseconds: milliseconds),
        child: AlertDialog(
          title: Text(title ?? "正在加载"),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const SizedBox(height: 16),
              const SizedBox(
                width: 24,
                height: 24,
                child: CircularProgressIndicator(strokeWidth: 3,),
              ), // 添加一个加载指示器
              const SizedBox(height: 16),
              Text(subTitle ?? '请等待...'), // 提示用户等待
            ],
          ),
        ),
      );
    });
    return DialogController(dialog);
  }
}
相关推荐
火柴就是我1 天前
flutter 之真手势冲突处理
android·flutter
Speed1231 天前
`mockito` 的核心“打桩”规则
flutter·dart
法的空间1 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
恋猫de小郭1 天前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter
玲珑Felone1 天前
从flutter源码看其渲染机制
android·flutter
ALLIN2 天前
Flutter 三种方式实现页面切换后保持原页面状态
flutter
Dabei2 天前
Flutter 国际化
flutter
Dabei2 天前
Flutter MQTT 通信文档
flutter
Dabei2 天前
Flutter 中实现 TCP 通信
flutter
孤鸿玉2 天前
ios flutter_echarts 不在当前屏幕 白屏修复
flutter