一、SnackBar
SnackBar 是一个从底部弹起的一个提示框, 类似Toast, 会自动隐藏. 它还可以添加操作按钮, 等等. SnackBar 是通过 Scaffold的showSnackBar 方法来显示的. 所以要显示一个SnackBar, 要先拿到Scaffold. 使用方式如下:
Scaffold.of(context).showSnackBar(new SnackBar(duration: Duration(seconds: 1),content: Text("ok")));
效果:
添加操作按钮
可以通过其 action 属性添加操作按钮:
final snackBar = new SnackBar(content: new Text('删除信息'),action: new SnackBarAction(label: '撤消',onPressed: () {// do something to undo}),);Scaffold.of(context).showSnackBar(snackBar);
效果:
获取上下文
当 BuildContext 在 Scaffold 之前时,调用 Scaffold.of(context) 会报错:
Scaffold.of() called with a context that does not contain a Scaffold.
这时可以通过 Builder Widget 来解决,典型的结构如下:
class PageState extends State<Page> {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("标题")),body: new Builder(builder: (BuildContext context) {return Column(children: <Widget>[FlatButton(child: Text('click me'),onPressed: () {Scaffold.of(context).showSnackBar(new SnackBar(duration: Duration(seconds: 1), content: Text("分享")));},)],);}));}}
还有一种办法就是为 Scaffold 绑定一个 GlobalKey, 调用的时候使用 _scaffoldkey.currentState.showSnackBar(snackBar), 具体方法看下面的 封装SnackBar
参考:Scaffold.of() called with a context that does not contain a Scaffold
封装SnackBar
为了使 SnackBar 更易于使用, 我们可以对其进行封装:
void showSnackBar (key, context, content, {String label: '',VoidCallback onPressed}) {final snackBar = new SnackBar(content: new Text(content),action: new SnackBarAction(label: label,onPressed: () {// do something to undoif (onPressed != null) {onPressed();}}),);key.currentState.showSnackBar(snackBar);// Scaffold.of(context).showSnackBar(snackBar); // 这句报错: Scaffold.of() called with a context that does not contain a Scaffold}
使用:
class TestPage extends StatefulWidget {@overrideState<StatefulWidget> createState() {return _TestPageState();}}class _TestPageState extends State<TestPage> {var _scaffoldkey = new GlobalKey<ScaffoldState>(); // 声明一个 keyWidget build(BuildContext context) {return Scaffold(key: _scaffoldkey, // 绑定keyappBar: new AppBar(title: new Text('test'),leading: IconButton(icon: Icon(Icons.dashboard), onPressed: () {}),actions: <Widget>[// 调用 showSnackBarIconButton(icon: Icon(Icons.share), onPressed: () => showSnackBar(_scaffoldkey, context, 'Share')), // 使用],),body: SingleChildScrollView());}}
二、Toast
官方没有实现吐司, 但是有一个很好用的第三方组件 fluttertoast
引入:
dependencies:fluttertoast: ^3.1.3
使用:
import 'package:fluttertoast/fluttertoast.dart';Fluttertoast.showToast(msg: "This is Center Short Toast",toastLength: Toast.LENGTH_SHORT,gravity: ToastGravity.CENTER,timeInSecForIos: 1,backgroundColor: Colors.red,textColor: Colors.white,fontSize: 16.0);
效果:
取消所有吐司:
Fluttertoast.cancel();
三、对话框
在 Flutter 里有大大小小的弹出框,例如:AlertDialog、SimpleDialog 等。
对于弹出框这些都不会直接使用它的组件,而是使用对应的调用函数 showDialog。
AlertDialog
AlertDialog 对话框是一个警报对话框,会通知用户需要确认的情况。
示例:
showDialog<Null>(context: context,barrierDismissible: false,builder: (BuildContext context) {return new AlertDialog(title: new Text('标题'),content: new SingleChildScrollView(child: new ListBody(children: <Widget>[new Text('内容'),],),),actions: <Widget>[new FlatButton(child: new Text('确定'),onPressed: () {Navigator.of(context).pop();},),],);},)
效果:
SimpleDialog
SimpleDialog 简单的对话框为用户提供了多个选项之间的选择。
示例:
showDialog<String>(context: context,barrierDismissible: false,builder: (BuildContext context) {return new SimpleDialog(title: const Text('Select assignment'),children: <Widget>[SimpleDialogOption(onPressed: () { Navigator.pop(context, '1'); },child: const Text('Treasury department'),),SimpleDialogOption(onPressed: () { Navigator.of(context).pop('2'); },child: const Text('State department'),),],);},).then((val) {print(val);})
效果:
选中对应的选项, 将打印出 pop 传递的参数
弹出路由时传值
使用 showDialog 后会弹出一个新页面, 我们通过操作路由的方式操作弹出层, 通过 pop 方法隐藏弹出层, 同时可以向外层页面传递参数, 通过 then 接收传出的参数:
FlatButton(child: Text('click me'),onPressed: () {showDialog<String>( // 传出的参数为 String 类型context: context,barrierDismissible: false,builder: (BuildContext context) {return new AlertDialog(title: new Text('标题'),content: new FlatButton(child: new Text('确定'),onPressed: () {Navigator.of(context).pop('1'); // 返回参数 '1'},),);},).then((val) {print(val); // 接收参数 -> '1'});},)
更新 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
showDialogis originally called from. Use a [StatefulBuilder] or a custom [StatefulWidget] if the dialog needs to update dynamically.
所以, 解决方案也很简单, 可以在弹出层外部套一个 StatefulBuilder:
showDialog(context: context,builder: (context) {return StatefulBuilder(builder: (context, state) {print('label = $label');return GestureDetector(child: Text(label), // 渲染为底层页面的 labelonTap: () {// 注意不是调用底层页面的setState, 而是要调用StatefulBuilder中的statestate(() {label = "test"; // 对底层页面的 label 重新赋值, 同时会更新弹出层});},);},);});
参考:
封装对话框
实际上调用起 showDialog 还是挺麻烦的, 要传递一堆的参数, 于是, 为了简化操作, 我封装了一个更简单的方法方便调用对话框:
void showAlertDialog (context, {String title = '提示',String content = '',String confirmText = '确定',String cancelText = '取消',bool showConfirm = true,bool showCancel = true,bool barrierDismissible: false,VoidCallback onConfirm,VoidCallback onCancel,}) {showDialog(barrierDismissible: barrierDismissible,context: context,builder: (_) => AlertDialog(title: Text(title),content: Text(content),actions:<Widget>[showCancel ? FlatButton(child: Text(cancelText), onPressed: (){Navigator.of(context).pop();if (onCancel != null) {onCancel();}},) : null,showConfirm ? FlatButton(child: Text(confirmText), onPressed: (){Navigator.of(context).pop();if (onConfirm != null) {onConfirm();}},) : null]));}
使用:
showAlertDialog(context,content: '确定退出?',onConfirm: () {Navigator.pushReplacementNamed(context, 'LoginPage');},);
三、底部滑出
BottomSheet 是一个从屏幕底部滑起的列表(以显示更多的内容)
可以调用 showBottomSheet() 或 showModalBottomSheet() 弹出
showModalBottomSheet(context: context,builder: (BuildContext context) {return new Container(height: 100.0,child: Text('Hello'),);},).then((val) {print(val);});
效果:
四、可伸缩面板
使用 ExpansionPanel 可以创建一个可伸缩面板。
import 'package:flutter/material.dart';class HomePage extends StatefulWidget {@overridecreateState() => new _HomePageState();}class _HomePageState extends State<HomePage> {var _isExpanded = false;@overrideWidget build(BuildContext context) {return Scaffold(appBar: new AppBar(title: Text('首页')),body: new Builder(builder: (BuildContext context) {returnContainer(alignment: Alignment.center,child: Column(children: <Widget>[ExpansionPanelList(children : <ExpansionPanel>[ExpansionPanel(headerBuilder:(context, isExpanded){return ListTile(title: Text(_isExpanded ? '收拢我' : '展开我'),);},body: Padding(padding: EdgeInsets.fromLTRB(15, 0, 15, 15),child: ListBody(children: [1,2,3,4,5].map((item) {return Card(margin:EdgeInsets.fromLTRB(0, 0, 0, 10),child: Padding(padding: EdgeInsets.all(8),child: Text('我是内容'),),);}).toList()),),isExpanded: _isExpanded,canTapOnHeader: true,),],expansionCallback:(panelIndex, isExpanded){setState(() {_isExpanded = !isExpanded;});},animationDuration : kThemeAnimationDuration,),],),);}));}}
效果:
五、提示框
Tooltip 是继承于StatefulWidget的一个Widget,它并不需要调出方法,当用户长按被Tooltip包裹的Widget时,会自动弹出相应的操作提示。
Tooltip(message: 'Hello',child: Text('Press me'),)
效果:
六、日期时间选择器
Flutter 提供两个函数:showDatePicker 和 showTimePicker, 用于选择日期和时间。
日期选择器的定义:
Future<DateTime> showDatePicker ({@required BuildContext context, // 上下文@required DateTime initialDate, // 初始日期@required DateTime firstDate, // 日期范围,开始@required DateTime lastDate, // 日期范围,结尾SelectableDayPredicate selectableDayPredicate,DatePickerMode initialDatePickerMode: DatePickerMode.day,Locale locale, // 国际化TextDirection textDirection,});
时间选择器的定义:
Future<TimeOfDay> showTimePicker({@required BuildContext context,@required TimeOfDay initialTime});
使用示例:
showDatePicker(context: context,initialDate: new DateTime.now(),firstDate: new DateTime.now().subtract(new Duration(days: 30)), // 减 30 天lastDate: new DateTime.now().add(new Duration(days: 30)), // 加 30 天).then((DateTime val) {if (val != null) {print(val); // 2019-01-01 00:00:00.000}}).catchError((err) {print(err);});
效果:
showTimePicker(context: context,initialTime: new TimeOfDay.now(),).then((TimeOfDay val) {print(val.format(context)); // 8:20 AM}).catchError((err) {print(err);});
效果:
国际化
默认的日期时间选择器都是英文版的, 即使系统设置了语言为中文, 选择器仍然会以英文呈现, 因此需要手动实现国际化
首先引入依赖:
dependencies:flutter:sdk: flutterflutter_localizations:sdk: flutterflutter_cupertino_localizations: ^1.0.1
在入口文件加入:
import 'package:flutter_localizations/flutter_localizations.dart';...MaterialApp(localizationsDelegates: [GlobalMaterialLocalizations.delegate,GlobalWidgetsLocalizations.delegate,GlobalCupertinoLocalizations.delegate,],supportedLocales: [ // 数组长度为1, 为了 showTimePicker 调用也是中文const Locale.fromSubtags(languageCode: 'zh'), // 注册中文],home: Scaffold(appBar: AppBar(title: Text('日期时间选择'),backgroundColor: Colors.pink,),body: HomeContent(),),);
在使用时间日期选择器的地方:
showDatePicker(context: context,initialDate: new DateTime.now(),firstDate: new DateTime.now().subtract(new Duration(days: 365*2)),lastDate: new DateTime.now(),locale: Locale.fromSubtags(languageCode: 'zh'), // 需要在 supportedLocales 中注册).then((DateTime val) {if (val != null) {print(val);}}).catchError((err) {print(err);});
而 showTimePicker 不需要传入 locale 属性, 而是在配置 supportedLocales 数组的时候, 一定只能有唯一的一个语言, 调用的时候即可设置为指定的语言
其实, 若 supportedLocales 设置的只有唯一的一个语言, 调用 showDatePicker 的时候, 也不需要传入 locale 参数
参考:
相关的第三方控件
如果官方的日期时间选择器不能满足需求, 可以看下以下第三方控件:
- flutter_rounded_date_picker
- flutter_cupertino_date_picker
- date_range_picker
- date_picker_timeline
- pacnepali_date_picker
- flutter_datetime_picker
- flutter_cupertino_data_picker
- date_picker_number
- datetime_picker_formfield
- horizontal_calendar
七、其他弹出层组件
- CupertinoAlertDialog iOS风格的 alert dialog
- CupertinoDialog iOS风格的对话框
