自定义不受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");
DropdownButton
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 的基本特性
Flutter
中dialog
实际上是一个由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