在应用开发中,或多或少都会遇到需要弹框的问题, 比如:需要用户确认,需要输入一些信息等等的问题,这就要用到 dialog 相关的概念了
而在 flutter 中,所有可以看见的都是 Widget,dialog 也不例外
不过和 android 或 iOS 中不同的一点是,Flutter 中 dialog 不是一个单独的类,而是一个可以由你自定义的 Widget
写在前面
首先为了方便,我定义了一个简单的方法用于构建按钮
Widget buildButton(
String text,
Function onPressed, {
Color color = Colors.white,
}) {
return FlatButton(
color: color,
child: Text(text),
onPressed: onPressed,
);
}
showDialog
dialog 的方法签名是这样的
其中 context 和 builder 是必传项
builder 需要返回一个 Widget,这个 Widget 会被作为 dialog 展示在页面上
比如我简单的写了一个这个方法
showDialog(
context: context,
builder: (ctx) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
buildButton("返回1", () {}),
buildButton("返回2", () {}),
],
),
);
},
);
当我调用这个方法时,会得到这样的样式
这个就是最简单的方法,然后点击外部,dialog 会消失
添加关闭时的返回值
接着我给按钮添加具体的事件
修改代码为以下的样子
_showDialog() async {
var result = await showDialog(
context: context,
builder: (ctx) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
buildButton("返回1", () => Navigator.of(context).pop(1)),
buildButton("返回2", () => Navigator.pop(context, 2)),
],
),
);
},
);
print("result = $result");
}
然后分别点击 1 2 和外部让 dialog 消失
会得到以下的结果
不过这个只能让 dialog 显示固定的内容,如果你的 dialog 有内容变化,则使用这个方式就不行了,哪怕是调用 setState 也不会发生变化,这个是因为外部 State 的状态变化不会影响到 dialog 的内容,因为 dialog 是附着至 app 根部的,而不是附着于页面
结合 StatefulWidget 使用
所以我们 dialog 中也可以使用 StatefulWidget,如同一个页面一样,只是这个页面可能不是全屏的
我定义了一个简单的 CounterWidget
class CounterWidget extends StatefulWidget {
@override
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
var _counter = 0;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Material(
child: Container(
width: 100,
height: 100,
child: Text(
_counter.toString(),
style: TextStyle(fontSize: 40),
),
alignment: Alignment.center,
),
color: Colors.white,
),
buildButton("+1", () => setState(() => _counter++)),
buildButton("-1", () => setState(() => _counter--)),
],
),
);
}
}
并且调用
showDialog(context: context, builder: (ctx) => CounterWidget());
这里可以看到,一个带状态的控件也是可以被展示在 dialog 中的
结合 StatefulBuilder
在 flutter 中有一个类,叫 StatefulBuilder
这个类的 builder 构造中会给一个 state,这个 state 是一个方法,返回 void,传入参数是一个方法,听起来很绕
大概是这样用
var statefulBuilder = StatefulBuilder(
builder: (ctx, state) {
state(() {});
return Container();
},
);
看起来和 setState 很像
这里我模拟一个 progress 的变化,不过这个进度是由外部传入的
_showDialogWithStatefulBuilder() {
var progress = 0.0;
StateSetter ss;
Timer.periodic(Duration(milliseconds: 300), (timer) {
progress += 0.1;
if (ss != null) {
ss(() {});
}
if (progress >= 1) {
timer.cancel();
ss = null;
}
});
var sb = StatefulBuilder(
builder: (ctx, state) {
ss = state;
return Center(
child: Container(
height: 40,
child: LinearProgressIndicator(
backgroundColor: Colors.white,
value: progress,
),
),
);
},
);
showDialog(context: context, builder: (ctx) => sb);
}
这里只是简单的演示一个用法,实际应用中,进度条应该是可以多处复用的,应该使用 StatefulWidget 进行复用,而不是简易的使用 StatefulBuilder 来做这件事情,并且,应该在构建时传入 stream 并且监听 stream 为宜,而不应该使用这种 Timer 的形式
StatefulBuilder 应该用于弹出布局很特殊不太可能复用于其他地方的情况
使用 iOS 风格
有的同学可能要问了,你这演示都是 MD 风格的,我需要的是苹果风格的, 怎么办?
在 flutter 中,如果你需要 iOS 风格的,只需要使用 Cupertino 组件即可
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);
}
带输入框的 dialog
showHasInputDialog() {
var widget = Center(
child: Container(
height: 40,
width: double.infinity,
child: Material(
child: TextField(),
),
),
);
showDialog(context: context, builder: (_) => widget);
}
根据软键盘自动变化位置
之前的输入框有一些问题,如果你的弹窗在底部,则弹出的输入框可能会被挡住
这里需要另一个方法来实现
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
class InputDialog extends StatefulWidget {
@override
_InputDialogState createState() => _InputDialogState();
}
class _InputDialogState extends State<InputDialog> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
super.didChangeMetrics();
if (this.mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
var mediaQueryData = MediaQueryData.fromWindow(ui.window);
return AnimatedContainer(
color: Colors.transparent,
duration: const Duration(milliseconds: 300),
padding: EdgeInsets.only(bottom: mediaQueryData.viewInsets.bottom),
child: Material(child: TextField()),
alignment: Alignment.center,
);
}
}
定义一个 dialog 类,然后监听窗口的变化
然后在变化的时候动态的修改 padding,以达到输入框永远在界面中心的目的
后记
完整代码 github
第一篇主要讲了 showDialog 方法的一些使用方法和建议
以上