flutter 弹窗之系列一

自定义不受Navigator影响的弹窗

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
  void _dialogController1() {
    DialogController alert = DialogController.alert(
      title: "Title",
      subTitle: "SubTitle",
      onCancel: () {
        debugPrint("alert cancel");
      },
      run: () async {
        // 一些耗时操作
        return true;
      },
    );
    alert.show(context);
  }
  void _dialogController2(){
    DialogController loadingAlert = DialogController.loadingAlert();
    loadingAlert.showWithTimeout(context, timeout: 10);
    // await 其他耗时操作
    loadingAlert.close();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
          child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          GestureDetector(
            onTap: () {
              _dialogController1();
            },
            child: const Text(
              '\n点击显示弹窗一\n',
            ),
          ),
          GestureDetector(
            onTap: () {
              _dialogController2();
            },
            child: const Text(
              '\n点击显示弹窗二\n',
            ),
          ),
        ],
      )),
    );
  }
}
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: [
            // 背景
            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 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 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);
  }
}

系统Dialog的使用

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
  void _showDialog() {
    showDialog(
      context: context,
      barrierColor: Colors.transparent, //设置透明底色,自定义也可能会用到
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text("测试标题"),
          content: const Text("测试内容"),
          actions: [
            TextButton(
              onPressed: () {},
              child: const Text('确认'),
            ),
            TextButton(
              onPressed: () {},
              child: const Text('取消'),
            ),
          ],
          shape:
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
        );
      },
    );
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: GestureDetector(
          onTap: () {
            _showDialog();
          },
          child: const Text(
            '\n点击显示弹窗一\n',
          ),
        ),
      ),
    );
  }
}

定制Dialog

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
  void _showDialog() {
    showDialog(
      context: context,
      barrierColor: Colors.transparent, //设置透明底色
      builder: (BuildContext context) {
        return const DialogView(
          title: "测试标题",
          message: "测试内容",
        );
      },
    );
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: GestureDetector(
          onTap: () {
            _showDialog();
          },
          child: const Text(
            '\n点击显示弹窗一\n',
          ),
        ),
      ),
    );
  }
}
//这个弹层一般是通过 showDialog 弹出,实际上相当于跳转了一个新界面,因此返回需通过 Navigator pop回去
class DialogView extends Dialog {
  final String title;
  final String message;
  const DialogView({Key? key, required this.title, required this.message})
      : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.transparent,
      body: Center(
        child: Container(
          width: 100,
          height: 150,
          color: Colors.black.withOpacity(0.7),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Text(title,
                  style: const TextStyle(color: Colors.white, fontSize: 14.0)),
              Text(message,
                  style: const TextStyle(color: Colors.white, fontSize: 14.0)),
              TextButton(
                onPressed: () {
                  //showDialog相当于push,因此自己返回需要pop
                  Navigator.pop(context);
                },
                child: const Text('返回'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

获取组件偏移量

//组件渲染完成之后,可以通过组价你的context参数,间接获取组件的偏移量
    RenderBox box = context.findRenderObject() as RenderBox;
    final local = box.localToGlobal(Offset.zero);
    debugPrint("组件偏移量:$local");

DropdownButton

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
  String? selectValue;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: DropdownButton(
          hint: const Text("请选择您要的号码:"),
          items: getItems(),
          value: selectValue,
          onChanged: (value) {
            debugPrint(value);
            setState(() {
              selectValue = value;
            });
          },
        )
      ),
    );
  }
  List> getItems() {
    List> items = [];
    items.add(const DropdownMenuItem(child: Text("AA"), value: "11"));
    items.add(const DropdownMenuItem(child: Text("BB"), value: "22",));
    items.add(const DropdownMenuItem(child: Text("CC"), value: "33",));
    items.add(const DropdownMenuItem(child: Text("DD"), value: "44",));
    items.add(const DropdownMenuItem(child: Text("EE"), value: "55",));
    return items;
  }
}

底部弹窗BottomSheet

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
  void _showDialog(){
    showModalBottomSheet(
        context: context,
        builder: (BuildContext context) {
          return Column(
            mainAxisSize: MainAxisSize.min, // 设置最小的弹出
            children: [
              ListTile(
                leading: const Icon(Icons.photo_camera),
                title: const Text("Camera"),
                onTap: () async {
                },
              ),
              ListTile(
                leading: const Icon(Icons.photo_library),
                title: const Text("Gallery"),
                onTap: () async {
                },
              ),
            ],
          );
        }
    );
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: GestureDetector(
          onTap: () {
            _showDialog();
          },
          child: const Text(
            '\n点击显示弹窗一\n',
          ),
        ),
      ),
    );
  }
}

PopupMenuButton

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
  var items = ["AA", "BB", "CC", "DD", "FF"];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        backgroundColor: Colors.greenAccent,
        actions: [
          PopupMenuButton(
            itemBuilder: (BuildContext context) {
              return _getItemBuilder2();
            },
            icon: const Icon(Icons.access_alarm),
            onSelected: (value) {
              debugPrint(value);
            },
            onCanceled: () {},
            offset: const Offset(200, 100),
          )
        ],
      ),
      body: const Center(),
    );
  }
  List> _getItemBuilder() {
    return items
        .map((item) => PopupMenuItem(
              value: item,
              child: Text(item),
            ))
        .toList();
  }
  List> _getItemBuilder2() {
    return >[
      const PopupMenuItem(
        value: "1",
        child: ListTile(
          leading: Icon(Icons.share),
          title: Text('分享'),
        ),
      ),
      const PopupMenuDivider(), //分割线
      const PopupMenuItem(
        value: "2",
        child: ListTile(
          leading: Icon(Icons.settings),
          title: Text('设置'),
        ),
      ),
    ];
  }
}
明确 Flutter 中 dialog 的基本特性
  • Flutter 中 dialog 实际上是一个由 route 直接切换显示的页面,所以使用 Navigator.of(context) 的 push、pop(xx) 方法进行显示、关闭、返回数据
  • Flutter 中有两种风格的 dialog
    • showDialog() 启动的是 material 风格的对话框
    • showCupertinoDialog() 启动的是 ios 风格的对话框
  • Flutter 中有两种样式的 dialog
    • SimpleDialog 使用多个 SimpleDialogOption 为用户提供了几个选项
    • AlertDialog 一个可选标题 title 和一个可选列表的 actions 选项

 showDialog 方法讲解

Future showDialog({
  @required BuildContext context,
  bool barrierDismissible = true,
  @Deprecated(
    'Instead of using the "child" argument, return the child from a closure '
    'provided to the "builder" argument. This will ensure that the BuildContext '
    'is appropriate for widgets built in the dialog.'
  ) Widget child,
  WidgetBuilder builder,
}) {
    .......
}
  • context 上下文对象
  • barrierDismissible 点外面是不是可以关闭,默认是 true 可以关闭的
  • builder 是 widget 构造器
  • FlatButton 标准 AlertDialog 中的按钮必须使用这个类型
  • Navigator.of(context).pop(); 对话框内关闭对话框

 AlertDialog

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
  void _showDialog() {
    // 定义对话框
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) {
        return AlertDialog(
          title: const Text("这里是测试标题"),
          actions: [
            GestureDetector(
              child: const Text("删除"),
              onTap: () {
                debugPrint("删除");
                Navigator.of(context).pop();
              },
            ),
            GestureDetector(
              child: const Text("取消"),
              onTap: () {
                debugPrint("取消");
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: GestureDetector(
          onTap: () {
            _showDialog();
          },
          child: const Text(
            '\n点击显示弹窗一\n',
          ),
        ),
      ),
    );
  }
}
自定义对话框
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
  var num = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: GestureDetector(
      onTap: () {
        showDialog(
            context: context,
            builder: (context) {
              return TestDialog();
            });
      },
      child: const Text(
        '\n点击显示弹窗一\n',
      ),
    )));
  }
}
class TestDialog extends StatefulWidget {
  @override
  State createState() {
    return TestDialogState();
  }
}
class TestDialogState extends State {
  var num = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.transparent,
        body: Center(child: Container(
          color: Colors.greenAccent,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Text(
                num.toString(),
                style: const TextStyle(decoration: TextDecoration.none),
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  GestureDetector(
                    child: Text("+"),
                    onTap: () {
                      setState(() {
                        num++;
                      });
                    },
                  ),
                  GestureDetector(
                    child: Text("-"),
                    onTap: () {
                      setState(() {
                        num--;
                      });
                    },
                  ),
                ],
              ),
            ],
          ),
          width: 100,
          height: 200,
        ),));
  }
}

SimpleDialog

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
  void _showDialog() {
    // 定义对话框
    showDialog(
        context: context,
        builder: (context) {
          return SimpleDialog(
            title: new Text("SimpleDialog"),
            children: [
              new SimpleDialogOption(
                child: new Text("SimpleDialogOption One"),
                onPressed: () {
                  Navigator.of(context).pop("SimpleDialogOption One");
                },
              ),
              new SimpleDialogOption(
                child: new Text("SimpleDialogOption Two"),
                onPressed: () {
                  Navigator.of(context).pop("SimpleDialogOption Two");
                },
              ),
              new SimpleDialogOption(
                child: new Text("SimpleDialogOption Three"),
                onPressed: () {
                  Navigator.of(context).pop("SimpleDialogOption Three");
                },
              ),
            ],
          );
        });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: GestureDetector(
          onTap: () {
            _showDialog();
          },
          child: const Text(
            '\n点击显示弹窗一\n',
          ),
        ),
      ),
    );
  }
}

自定义ios风格对话框

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
  void showCupertinoDialog() {
    var dialog = CupertinoAlertDialog(
      content: Text(
        "你好,我是你苹果爸爸的界面",
        style: TextStyle(fontSize: 20),
      ),
      actions: [
        CupertinoButton(
          child: Text("取消"),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
        CupertinoButton(
          child: Text("确定"),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ],
    );
    showDialog(context: context, builder: (_) => dialog);
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: GestureDetector(
          onTap: () {
            showCupertinoDialog();
          },
          child: const Text(
            '\n点击显示弹窗一\n',
          ),
        ),
      ),
    );
  }
}

自定义对话框注意事项

  • 自定义的 dialog 要是太长了超过屏幕长度了,请在外面加一个可以滚动的 SingleChildScrollView
  • 自定义的 dialog 要是有 ListView 的话,必须在最外面加上一个确定宽度和高度的 Container,要不会报错,道理和上面的那条一样的

案例 切换到分支 flutter_custom_widget