一、SnackBar

SnackBar 是一个从底部弹起的一个提示框, 类似Toast, 会自动隐藏. 它还可以添加操作按钮, 等等. SnackBar 是通过 Scaffold的showSnackBar 方法来显示的. 所以要显示一个SnackBar, 要先拿到Scaffold. 使用方式如下:

  1. Scaffold.of(context).showSnackBar(
  2. new SnackBar(
  3. duration: Duration(seconds: 1),
  4. content: Text("ok")
  5. )
  6. );

效果:
003.gif

添加操作按钮

可以通过其 action 属性添加操作按钮:

  1. final snackBar = new SnackBar(
  2. content: new Text('删除信息'),
  3. action: new SnackBarAction(
  4. label: '撤消',
  5. onPressed: () {
  6. // do something to undo
  7. }
  8. ),
  9. );
  10. Scaffold.of(context).showSnackBar(snackBar);

效果:
002.png

获取上下文

BuildContext 在 Scaffold 之前时,调用 Scaffold.of(context) 会报错:

  1. Scaffold.of() called with a context that does not contain a Scaffold.

这时可以通过 Builder Widget 来解决,典型的结构如下:

  1. class PageState extends State<Page> {
  2. @override
  3. Widget build(BuildContext context) {
  4. return Scaffold(
  5. appBar: AppBar(title: Text("标题")),
  6. body: new Builder(builder: (BuildContext context) {
  7. return Column(
  8. children: <Widget>[
  9. FlatButton(
  10. child: Text('click me'),
  11. onPressed: () {
  12. Scaffold.of(context).showSnackBar(new SnackBar(duration: Duration(seconds: 1), content: Text("分享")));
  13. },
  14. )
  15. ],
  16. );
  17. }));
  18. }
  19. }

还有一种办法就是为 Scaffold 绑定一个 GlobalKey, 调用的时候使用 _scaffoldkey.currentState.showSnackBar(snackBar), 具体方法看下面的 封装SnackBar

参考:Scaffold.of() called with a context that does not contain a Scaffold

封装SnackBar

为了使 SnackBar 更易于使用, 我们可以对其进行封装:

  1. void showSnackBar (key, context, content, {
  2. String label: '',
  3. VoidCallback onPressed
  4. }) {
  5. final snackBar = new SnackBar(
  6. content: new Text(content),
  7. action: new SnackBarAction(
  8. label: label,
  9. onPressed: () {
  10. // do something to undo
  11. if (onPressed != null) {
  12. onPressed();
  13. }
  14. }
  15. ),
  16. );
  17. key.currentState.showSnackBar(snackBar);
  18. // Scaffold.of(context).showSnackBar(snackBar); // 这句报错: Scaffold.of() called with a context that does not contain a Scaffold
  19. }

使用:

  1. class TestPage extends StatefulWidget {
  2. @override
  3. State<StatefulWidget> createState() {
  4. return _TestPageState();
  5. }
  6. }
  7. class _TestPageState extends State<TestPage> {
  8. var _scaffoldkey = new GlobalKey<ScaffoldState>(); // 声明一个 key
  9. Widget build(BuildContext context) {
  10. return Scaffold(
  11. key: _scaffoldkey, // 绑定key
  12. appBar: new AppBar(
  13. title: new Text('test'),
  14. leading: IconButton(icon: Icon(Icons.dashboard), onPressed: () {}),
  15. actions: <Widget>[
  16. // 调用 showSnackBar
  17. IconButton(icon: Icon(Icons.share), onPressed: () => showSnackBar(_scaffoldkey, context, 'Share')), // 使用
  18. ],
  19. ),
  20. body: SingleChildScrollView());
  21. }
  22. }

二、Toast

官方没有实现吐司, 但是有一个很好用的第三方组件 fluttertoast

引入:

  1. dependencies:
  2. fluttertoast: ^3.1.3

使用:

  1. import 'package:fluttertoast/fluttertoast.dart';
  2. Fluttertoast.showToast(
  3. msg: "This is Center Short Toast",
  4. toastLength: Toast.LENGTH_SHORT,
  5. gravity: ToastGravity.CENTER,
  6. timeInSecForIos: 1,
  7. backgroundColor: Colors.red,
  8. textColor: Colors.white,
  9. fontSize: 16.0
  10. );

效果:
003.png

取消所有吐司:

  1. Fluttertoast.cancel();

三、对话框

在 Flutter 里有大大小小的弹出框,例如:AlertDialog、SimpleDialog 等。

对于弹出框这些都不会直接使用它的组件,而是使用对应的调用函数 showDialog

AlertDialog

AlertDialog 对话框是一个警报对话框,会通知用户需要确认的情况。

示例:

  1. showDialog<Null>(
  2. context: context,
  3. barrierDismissible: false,
  4. builder: (BuildContext context) {
  5. return new AlertDialog(
  6. title: new Text('标题'),
  7. content: new SingleChildScrollView(
  8. child: new ListBody(
  9. children: <Widget>[
  10. new Text('内容'),
  11. ],
  12. ),
  13. ),
  14. actions: <Widget>[
  15. new FlatButton(
  16. child: new Text('确定'),
  17. onPressed: () {
  18. Navigator.of(context).pop();
  19. },
  20. ),
  21. ],
  22. );
  23. },
  24. )

效果:
011.png

SimpleDialog

SimpleDialog 简单的对话框为用户提供了多个选项之间的选择。

示例:

  1. showDialog<String>(
  2. context: context,
  3. barrierDismissible: false,
  4. builder: (BuildContext context) {
  5. return new SimpleDialog(
  6. title: const Text('Select assignment'),
  7. children: <Widget>[
  8. SimpleDialogOption(
  9. onPressed: () { Navigator.pop(context, '1'); },
  10. child: const Text('Treasury department'),
  11. ),
  12. SimpleDialogOption(
  13. onPressed: () { Navigator.of(context).pop('2'); },
  14. child: const Text('State department'),
  15. ),
  16. ],
  17. );
  18. },
  19. ).then((val) {
  20. print(val);
  21. })

效果:
010.png

选中对应的选项, 将打印出 pop 传递的参数

弹出路由时传值

使用 showDialog 后会弹出一个新页面, 我们通过操作路由的方式操作弹出层, 通过 pop 方法隐藏弹出层, 同时可以向外层页面传递参数, 通过 then 接收传出的参数:

  1. FlatButton(
  2. child: Text('click me'),
  3. onPressed: () {
  4. showDialog<String>( // 传出的参数为 String 类型
  5. context: context,
  6. barrierDismissible: false,
  7. builder: (BuildContext context) {
  8. return new AlertDialog(
  9. title: new Text('标题'),
  10. content: new FlatButton(
  11. child: new Text('确定'),
  12. onPressed: () {
  13. Navigator.of(context).pop('1'); // 返回参数 '1'
  14. },
  15. ),
  16. );
  17. },
  18. ).then((val) {
  19. print(val); // 接收参数 -> '1'
  20. });
  21. },
  22. )

更新 showDialog 中的状态

使用 showDialog 后,通过 setState() 无法更新当前dialog。其实原因很简单,因为dialog其实是另一个页面,准确地来说是另一个路由,因为dialog的关闭也是通过navigator来pop的,所以它的地位跟你当前主页面一样。这个概念一定要明确,因为无论在Android或iOS中,daliog都是依附于当前主页面的一个控件,但是在Flutter中不同,它是一个新的路由。所以使用当前主页面的setState()来更新,当然没法达到你要的效果。

showDialog方法的Api中也明确说明了这一点,dialog所持有的context已经变了:

This widget does not share a context with the location that showDialog is originally called from. Use a [StatefulBuilder] or a custom [StatefulWidget] if the dialog needs to update dynamically.

所以, 解决方案也很简单, 可以在弹出层外部套一个 StatefulBuilder:

  1. showDialog(
  2. context: context,
  3. builder: (context) {
  4. return StatefulBuilder(
  5. builder: (context, state) {
  6. print('label = $label');
  7. return GestureDetector(
  8. child: Text(label), // 渲染为底层页面的 label
  9. onTap: () {
  10. // 注意不是调用底层页面的setState, 而是要调用StatefulBuilder中的state
  11. state(() {
  12. label = "test"; // 对底层页面的 label 重新赋值, 同时会更新弹出层
  13. });
  14. },
  15. );
  16. },
  17. );
  18. });

参考:

封装对话框

实际上调用起 showDialog 还是挺麻烦的, 要传递一堆的参数, 于是, 为了简化操作, 我封装了一个更简单的方法方便调用对话框:

  1. void showAlertDialog (context, {
  2. String title = '提示',
  3. String content = '',
  4. String confirmText = '确定',
  5. String cancelText = '取消',
  6. bool showConfirm = true,
  7. bool showCancel = true,
  8. bool barrierDismissible: false,
  9. VoidCallback onConfirm,
  10. VoidCallback onCancel,
  11. }) {
  12. showDialog(
  13. barrierDismissible: barrierDismissible,
  14. context: context,
  15. builder: (_) => AlertDialog(
  16. title: Text(title),
  17. content: Text(content),
  18. actions:<Widget>[
  19. showCancel ? FlatButton(child: Text(cancelText), onPressed: (){
  20. Navigator.of(context).pop();
  21. if (onCancel != null) {
  22. onCancel();
  23. }
  24. },) : null,
  25. showConfirm ? FlatButton(child: Text(confirmText), onPressed: (){
  26. Navigator.of(context).pop();
  27. if (onConfirm != null) {
  28. onConfirm();
  29. }
  30. },) : null
  31. ]
  32. ));
  33. }

使用:

  1. showAlertDialog(context,
  2. content: '确定退出?',
  3. onConfirm: () {
  4. Navigator.pushReplacementNamed(context, 'LoginPage');
  5. },
  6. );

三、底部滑出

BottomSheet 是一个从屏幕底部滑起的列表(以显示更多的内容)

可以调用 showBottomSheet()showModalBottomSheet() 弹出

  1. showModalBottomSheet(
  2. context: context,
  3. builder: (BuildContext context) {
  4. return new Container(
  5. height: 100.0,
  6. child: Text('Hello'),
  7. );
  8. },
  9. ).then((val) {
  10. print(val);
  11. });

效果:
001.gif

四、可伸缩面板

使用 ExpansionPanel 可以创建一个可伸缩面板。

  1. import 'package:flutter/material.dart';
  2. class HomePage extends StatefulWidget {
  3. @override
  4. createState() => new _HomePageState();
  5. }
  6. class _HomePageState extends State<HomePage> {
  7. var _isExpanded = false;
  8. @override
  9. Widget build(BuildContext context) {
  10. return Scaffold(
  11. appBar: new AppBar(title: Text('首页')),
  12. body: new Builder(builder: (BuildContext context) {
  13. return
  14. Container(
  15. alignment: Alignment.center,
  16. child: Column(
  17. children: <Widget>[
  18. ExpansionPanelList(
  19. children : <ExpansionPanel>[
  20. ExpansionPanel(
  21. headerBuilder:(context, isExpanded){
  22. return ListTile(
  23. title: Text(_isExpanded ? '收拢我' : '展开我'),
  24. );
  25. },
  26. body: Padding(
  27. padding: EdgeInsets.fromLTRB(15, 0, 15, 15),
  28. child: ListBody(
  29. children: [1,2,3,4,5].map((item) {
  30. return Card(
  31. margin:EdgeInsets.fromLTRB(0, 0, 0, 10),
  32. child: Padding(padding: EdgeInsets.all(8),child: Text('我是内容'),),
  33. );
  34. }).toList()
  35. ),
  36. ),
  37. isExpanded: _isExpanded,
  38. canTapOnHeader: true,
  39. ),
  40. ],
  41. expansionCallback:(panelIndex, isExpanded){
  42. setState(() {
  43. _isExpanded = !isExpanded;
  44. });
  45. },
  46. animationDuration : kThemeAnimationDuration,
  47. ),
  48. ],
  49. ),
  50. );
  51. })
  52. );
  53. }
  54. }

效果:
012.gif

五、提示框

Tooltip 是继承于StatefulWidget的一个Widget,它并不需要调出方法,当用户长按被Tooltip包裹的Widget时,会自动弹出相应的操作提示。

  1. Tooltip(
  2. message: 'Hello',
  3. child: Text('Press me'),
  4. )

效果:
013.gif

六、日期时间选择器

Flutter 提供两个函数:showDatePickershowTimePicker, 用于选择日期和时间。

日期选择器的定义:

  1. Future<DateTime> showDatePicker ({
  2. @required BuildContext context, // 上下文
  3. @required DateTime initialDate, // 初始日期
  4. @required DateTime firstDate, // 日期范围,开始
  5. @required DateTime lastDate, // 日期范围,结尾
  6. SelectableDayPredicate selectableDayPredicate,
  7. DatePickerMode initialDatePickerMode: DatePickerMode.day,
  8. Locale locale, // 国际化
  9. TextDirection textDirection,
  10. });

时间选择器的定义:

  1. Future<TimeOfDay> showTimePicker({
  2. @required BuildContext context,
  3. @required TimeOfDay initialTime
  4. });

使用示例:

  1. showDatePicker(
  2. context: context,
  3. initialDate: new DateTime.now(),
  4. firstDate: new DateTime.now().subtract(new Duration(days: 30)), // 减 30 天
  5. lastDate: new DateTime.now().add(new Duration(days: 30)), // 加 30 天
  6. ).then((DateTime val) {
  7. if (val != null) {
  8. print(val); // 2019-01-01 00:00:00.000
  9. }
  10. }).catchError((err) {
  11. print(err);
  12. });

效果:
002.webp

  1. showTimePicker(
  2. context: context,
  3. initialTime: new TimeOfDay.now(),
  4. ).then((TimeOfDay val) {
  5. print(val.format(context)); // 8:20 AM
  6. }).catchError((err) {
  7. print(err);
  8. });

效果:
003.webp

国际化

默认的日期时间选择器都是英文版的, 即使系统设置了语言为中文, 选择器仍然会以英文呈现, 因此需要手动实现国际化

首先引入依赖:

  1. dependencies:
  2. flutter:
  3. sdk: flutter
  4. flutter_localizations:
  5. sdk: flutter
  6. flutter_cupertino_localizations: ^1.0.1

在入口文件加入:

  1. import 'package:flutter_localizations/flutter_localizations.dart';
  2. ...
  3. MaterialApp(
  4. localizationsDelegates: [
  5. GlobalMaterialLocalizations.delegate,
  6. GlobalWidgetsLocalizations.delegate,
  7. GlobalCupertinoLocalizations.delegate,
  8. ],
  9. supportedLocales: [ // 数组长度为1, 为了 showTimePicker 调用也是中文
  10. const Locale.fromSubtags(languageCode: 'zh'), // 注册中文
  11. ],
  12. home: Scaffold(
  13. appBar: AppBar(
  14. title: Text('日期时间选择'),
  15. backgroundColor: Colors.pink,
  16. ),
  17. body: HomeContent(),
  18. ),
  19. );

在使用时间日期选择器的地方:

  1. showDatePicker(
  2. context: context,
  3. initialDate: new DateTime.now(),
  4. firstDate: new DateTime.now().subtract(new Duration(days: 365*2)),
  5. lastDate: new DateTime.now(),
  6. locale: Locale.fromSubtags(languageCode: 'zh'), // 需要在 supportedLocales 中注册
  7. ).then((DateTime val) {
  8. if (val != null) {
  9. print(val);
  10. }
  11. }).catchError((err) {
  12. print(err);
  13. });

而 showTimePicker 不需要传入 locale 属性, 而是在配置 supportedLocales 数组的时候, 一定只能有唯一的一个语言, 调用的时候即可设置为指定的语言

其实, 若 supportedLocales 设置的只有唯一的一个语言, 调用 showDatePicker 的时候, 也不需要传入 locale 参数

参考:

相关的第三方控件

如果官方的日期时间选择器不能满足需求, 可以看下以下第三方控件:

七、其他弹出层组件