自定义不受Navigator影响的弹窗
class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override StatecreateState() => _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 StatecreateState() => _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 StatecreateState() => _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 StatecreateState() => _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 StatecreateState() => _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 StatecreateState() => _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 方法讲解
FutureshowDialog ({ @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 StatecreateState() => _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 StatecreateState() => _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 StatecreateState() => _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 StatecreateState() => _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