flutter 弹窗之系列一

自定义不受Navigator影响的弹窗

bash 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  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: <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);
  }
}

系统Dialog的使用

bash 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  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

bash 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  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('返回'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

获取组件偏移量

bash 复制代码
//组件渲染完成之后,可以通过组价你的context参数,间接获取组件的偏移量
    RenderBox box = context.findRenderObject() as RenderBox;
    final local = box.localToGlobal(Offset.zero);
    debugPrint("组件偏移量:$local");
bash 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  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<DropdownMenuItem<String>> getItems() {
    List<DropdownMenuItem<String>> 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

bash 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  void _showDialog(){
    showModalBottomSheet(
        context: context,
        builder: (BuildContext context) {
          return Column(
            mainAxisSize: MainAxisSize.min, // 设置最小的弹出
            children: <Widget>[
              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

bash 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var items = <String>["AA", "BB", "CC", "DD", "FF"];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        backgroundColor: Colors.greenAccent,
        actions: <Widget>[
          PopupMenuButton<String>(
            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<PopupMenuEntry<String>> _getItemBuilder() {
    return items
        .map((item) => PopupMenuItem<String>(
              value: item,
              child: Text(item),
            ))
        .toList();
  }

  List<PopupMenuEntry<String>> _getItemBuilder2() {
    return <PopupMenuEntry<String>>[
      const PopupMenuItem<String>(
        value: "1",
        child: ListTile(
          leading: Icon(Icons.share),
          title: Text('分享'),
        ),
      ),
      const PopupMenuDivider(), //分割线
      const PopupMenuItem<String>(
        value: "2",
        child: ListTile(
          leading: Icon(Icons.settings),
          title: Text('设置'),
        ),
      ),
    ];
  }
}
明确 Flutter 中 dialog 的基本特性
  • Flutterdialog 实际上是一个由 route 直接切换显示的页面,所以使用 Navigator.of(context) 的 push、pop(xx) 方法进行显示、关闭、返回数据
  • Flutter 中有两种风格的 dialog
    • showDialog() 启动的是 material 风格的对话框
    • showCupertinoDialog() 启动的是 ios 风格的对话框
  • Flutter 中有两种样式的 dialog
    • SimpleDialog 使用多个 SimpleDialogOption 为用户提供了几个选项
    • AlertDialog 一个可选标题 title 和一个可选列表的 actions 选项

showDialog 方法讲解

bash 复制代码
Future<T> showDialog<T>({
  @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

bash 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  void _showDialog() {
    // 定义对话框
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) {
        return AlertDialog(
          title: const Text("这里是测试标题"),
          actions: <Widget>[
            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',
          ),
        ),
      ),
    );
  }
}
自定义对话框
bash 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  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<StatefulWidget> createState() {
    return TestDialogState();
  }
}

class TestDialogState extends State<TestDialog> {
  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: <Widget>[
              Text(
                num.toString(),
                style: const TextStyle(decoration: TextDecoration.none),
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: <Widget>[
                  GestureDetector(
                    child: Text("+"),
                    onTap: () {
                      setState(() {
                        num++;
                      });
                    },
                  ),
                  GestureDetector(
                    child: Text("-"),
                    onTap: () {
                      setState(() {
                        num--;
                      });
                    },
                  ),
                ],
              ),
            ],
          ),
          width: 100,
          height: 200,
        ),));
  }
}

SimpleDialog

bash 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  void _showDialog() {
    // 定义对话框
    showDialog(
        context: context,
        builder: (context) {
          return SimpleDialog(
            title: new Text("SimpleDialog"),
            children: <Widget>[
              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风格对话框

bash 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  void showCupertinoDialog() {
    var dialog = CupertinoAlertDialog(
      content: Text(
        "你好,我是你苹果爸爸的界面",
        style: TextStyle(fontSize: 20),
      ),
      actions: <Widget>[
        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

相关推荐
0wioiw01 小时前
Flutter基础(FFI)
flutter
Georgewu9 天前
【HarmonyOS 5】鸿蒙跨平台开发方案详解(一)
flutter·harmonyos
爱吃鱼的锅包肉9 天前
Flutter开发中记录一个非常好用的图片缓存清理的插件
flutter
张风捷特烈10 天前
每日一题 Flutter#13 | build 回调的 BuildContext 是什么
android·flutter·面试
恋猫de小郭10 天前
Flutter 又双叒叕可以在 iOS 26 的真机上 hotload 运行了,来看看又是什么黑科技
android·前端·flutter
QC七哥10 天前
跨平台开发flutter初体验
android·flutter·安卓·桌面开发
小喷友10 天前
Flutter 从入门到精通(水)
前端·flutter·app
恋猫de小郭11 天前
Flutter 里的像素对齐问题,深入理解为什么界面有时候会出现诡异的细线?
android·前端·flutter
tbit11 天前
dart私有命名构造函数的作用与使用场景
flutter·dart