一、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> {
@override
Widget 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 undo
if (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 {
@override
State<StatefulWidget> createState() {
return _TestPageState();
}
}
class _TestPageState extends State<TestPage> {
var _scaffoldkey = new GlobalKey<ScaffoldState>(); // 声明一个 key
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldkey, // 绑定key
appBar: new AppBar(
title: new Text('test'),
leading: IconButton(icon: Icon(Icons.dashboard), onPressed: () {}),
actions: <Widget>[
// 调用 showSnackBar
IconButton(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
showDialog
is 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), // 渲染为底层页面的 label
onTap: () {
// 注意不是调用底层页面的setState, 而是要调用StatefulBuilder中的state
state(() {
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 {
@override
createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
var _isExpanded = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(title: Text('首页')),
body: new Builder(builder: (BuildContext context) {
return
Container(
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: flutter
flutter_localizations:
sdk: flutter
flutter_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风格的对话框