目录
1. 表单(TextField、Radio、Switch、CheckboxListTitle、RadioListTitle、SwitchListTitle、Slide、Form、Stepper)
2. 弹框(对话框)
1. Form表单
1. TextField
2. Radio
3. Switch
4. CheckboxListTitle
5. RadioListTitle
6. SwitchListTitle
7. Slide
- TextField (用于文本输入)
1.
style
正在编辑的文本样式
textAlign
编辑文本在水平方向的对齐方式。
默认:TextAlign.start。
2.
readOnly
是否只读。
默认:false。
enabled
是否禁用。
如果为false,则输入框会被禁用,禁用状态不接收输入和事件,同时显示禁用态样式。
obscureText
是否用“•”替换正在编辑的文本。如:输入密码时。
默认:false。
3.
maxLines
输入框的最大行数。
默认:1。如果为null,则无行数限制。
minLines
输入框的最小行数。
maxLength
输入文本的最大长度,设置后输入框右下角会显示输入的文本计数。
maxLengthEnforcement
当输入文本长度超过maxLength时如何处理(截断/超出)
4. 光标
showCursor
是否显示光标
cursorColor
光标颜色。
cursorRadius
光标圆角。
cursorWidth
光标宽度。
默认:2.0
toolbarOptions
长按或鼠标右击时出现的菜单,包括 copy、cut、paste 以及 selectAll。
5. 键盘
keyboardType
用于设置该输入框默认的键盘输入类型(TextInputType)
textInputAction
键盘回车键位的图标
枚举类型。为TextInputAction.search则是个搜索图标。
6. 焦点
autofocus
是否自动获取焦点。
默认:false
focusNode
用于控制TextField是否占有当前键盘的输入焦点。
是我们和键盘交互的一个句柄。
7.
decoration(InputDecoration)
用于控制TextField的外观显示,如提示文本、背景颜色、边框等。
默认情况下,TextField有一个下划线装饰。设置为空会完全删除装饰(包括下划线和为标签保留的空间)。
hintText (placeholder文本)
border(边框,如OutlineInputBorder())
labelText(placeholder文本,输入文本后还显示且位置跑到上边)
labelStyle(labelText的样式)
icon(图标,位于文本框前面)
8. 回调
onChanged
输入框内容改变时的回调函数。也可以通过controller来监听
onEditingComplete
输入框输入完成时的回调函数,比如按了键盘的完成键(对号图标)或搜索键(图标)。
回调是ValueChanged类型,它接收当前输入内容做为参数。
onSubmitted
输入框输入完成时的回调函数。不接收参数
9.
controller
编辑框的控制器,通过它可以设置/获取编辑框的内容、选择编辑内容、监听编辑文本改变事件。
大多数情况下都需要显式提供一个controller来与文本框交互。如果没有提供controller,则TextField内部会自动创建一个。
inputFormatters
用于指定输入格式;当用户输入内容改变时,会根据指定的格式来校验
TextFormField包裹一个TextField并将其集成在Form中。如下情况使用:
1. 要提供一个验证函数来检查用户的输入是否满足一定的约束(例如,一个电话号码)
2. 想将TextField与其他FormField集成时。
示例
Column(
children: [
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
prefixIcon: Icon(Icons.person)
),
),
TextField(
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
prefixIcon: Icon(Icons.lock)
),
obscureText: true,
),
],
);
获取输入内容
两种方式:
1. 定义变量,在onChange触发时保存一下输入内容。
2. 通过controller直接获取。
示例(通过controller直接获取):
// initState方法中:
TextEditingController _unameController = TextEditingController();
_unameController.text='初始文本';
// build方法中
TextField(
autofocus: true,
controller: _unameController, // 设置controller
)
// 自定义方法中
print(_unameController.text)
_unameController.text='hello';
监听文本变化
两种方式:
1. onChange回调
专门用于监听文本变化。输入文本发生变化时会调用。
2. controller监听
除了能监听文本变化外,还可以设置默认值、选择文本。
提供一个TextEditingController作为TextField的controller属性,使用controller的addListener来监听(需要在dispose方法中删除监听器)。
示例1(方法1):
TextField(
autofocus: true,
onChanged: (v) {
print("onChange: $v");
}
)
示例2(方法2):
@override
void initState() {
// 监听输入改变
_unameController.addListener((){
print(_unameController.text);
});
}
示例3(方法2,设置默认值,选择文本)
TextField(
controller: _selectionController,
)
TextEditingController _selectionController = TextEditingController();
_selectionController.text="hello world!";
_selectionController.selection=TextSelection(
baseOffset: 2,
extentOffset: _selectionController.text.length
);
示例4(完整代码):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.yellow,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
createState() => new MyHomePageState();
}
class MyHomePageState extends State {
final TextEditingController _controller = new TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
new TextField(
controller: _controller,
decoration: new InputDecoration(
hintText: '输入点什么呗',
),
),
new RaisedButton(
onPressed: () {
showDialog(
context: context,
child: new AlertDialog(
title: new Text('你输入了:'),
content: new Text(_controller.text),
),
);
},
child: new Text('确认'),
),
],
));
}
}
控制焦点
获取焦点
// 通过FocusScope.of(context)获取Widget树中默认的FocusScopeNode。
// 调用FocusScopeNode的requestFocus(FocusNode对象)来获取焦点。
FocusScope.of(context).requestFocus(focusNode1);
失去焦点(会自动关闭键盘)
focusNode1.unfocus();
监听焦点状态改变事件
FocusNode继承自ChangeNotifier,通过FocusNode可以监听焦点的改变事件。
获得焦点时focusNode.hasFocus值为true,失去焦点时为false。
/*
// 创建 focusNode
FocusNode focusNode = new FocusNode();
// 监听焦点变化
focusNode.addListener((){
print(focusNode.hasFocus);
});
// focusNode绑定输入框
TextField(focusNode: focusNode);
*/
示例
/*
创建两个TextField,第一个自动获取焦点,然后创建两个按钮:
1. 点击第一个按钮可以将焦点从第一个TextField挪到第二个TextField。
2. 点击第二个按钮可以关闭键盘。
*/
class FocusTestRoute extends StatefulWidget {
@override
_FocusTestRouteState createState() => new _FocusTestRouteState();
}
class _FocusTestRouteState extends State {
FocusNode focusNode1 = new FocusNode();
FocusNode focusNode2 = new FocusNode();
FocusScopeNode focusScopeNode;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
autofocus: true,
focusNode: focusNode1,// 关联focusNode1
decoration: InputDecoration(
labelText: "input1"
),
),
TextField(
focusNode: focusNode2,// 关联focusNode2
decoration: InputDecoration(
labelText: "input2"
),
),
Builder(builder: (ctx) {
return Column(
children: [
RaisedButton(
child: Text("移动焦点"),
onPressed: () {
// 将焦点从第一个TextField移到第二个TextField
// 等同于FocusScope.of(context).requestFocus(focusNode2);
if(null == focusScopeNode){
focusScopeNode = FocusScope.of(context);
}
focusScopeNode.requestFocus(focusNode2);
},
),
RaisedButton(
child: Text("隐藏键盘"),
onPressed: () {
// 当所有编辑框都失去焦点时键盘就会收起
focusNode1.unfocus();
focusNode2.unfocus();
},
),
],
);
},
),
],
),
);
}
}
自定义样式
优先通过decoration属性自定义输入框样式。如果decoration实现不了,再用widget组合的方式。
主题也可以自定义输入框样式。
示例(自定义输入框下划线颜色)
TextField(
decoration: InputDecoration(
labelText: "请输入用户名",
prefixIcon: Icon(Icons.person),
// 未获得焦点下划线设为灰色
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
),
// 获得焦点下划线设为蓝色
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.blue),
),
),
),
示例2 (自定义下滑线颜色)
/*
由于TextField在绘制下划线时使用的颜色是主题色里面的hintColor,但提示文本颜色也是用的hintColor, 如果直接修改hintColor,那么下划线和提示文本的颜色都会变。
通过decoration中可以设置hintStyle,会覆盖hintColor,并且主题中可以通过inputDecorationTheme来设置输入框默认的decoration。通过这种方式自定义后,输入框在获取焦点时,labelText不会高亮显示了,并且还是无法自定义下划线宽度。
另一种灵活的方式是直接隐藏掉TextField本身的下划线,然后通过Container去嵌套自定义样式。
*/
Theme(
data: Theme.of(context).copyWith(
hintColor: Colors.grey[200], // 定义下划线颜色
inputDecorationTheme: InputDecorationTheme(
labelStyle: TextStyle(color: Colors.grey),// 定义label字体样式
hintStyle: TextStyle(color: Colors.grey, fontSize: 14.0)// 定义提示文本样式
)
),
child: Column(
children: [
TextField(
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
prefixIcon: Icon(Icons.person)
),
),
TextField(
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock),
labelText: "密码",
hintText: "您的登录密码",
hintStyle: TextStyle(color: Colors.grey, fontSize: 13.0)
),
obscureText: true,
)
],
)
)
示例3(隐藏下划线)
通过这种组件组合的方式,也可以定义背景圆角等。
Container(
child: TextField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: "Email",
hintText: "电子邮件地址",
prefixIcon: Icon(Icons.email),
border: InputBorder.none //隐藏下划线
)
),
decoration: BoxDecoration(
// 下滑线浅灰色,宽度1像素
border: Border(bottom: BorderSide(color: Colors.grey[200], width: 1.0))
),
)
示例
class _LoginPageState extends State {
TextEditingController _usernameController = TextEditingController();
TextEditingController _passwordController = TextEditingController();
String _username = '', _password = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('登录'),
brightness: Brightness.dark,
),
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_getRoundImage('images/logo.png', 100.0),
SizedBox(
height: 60,
),
_getUsernameInput(),
_getPasswordInput(),
SizedBox(
height: 10,
),
_getLoginButton(),
],
),
),
);
}
Widget _getUsernameInput() {
return _getInputTextField(
TextInputType.number,
controller: _usernameController,
decoration: InputDecoration(
hintText: "输入手机号",
icon: Icon(
Icons.mobile_friendly_rounded,
size: 20.0,
),
border: InputBorder.none,
//使用 GestureDetector 实现手势识别
suffixIcon: GestureDetector(
child: Offstage(
child: Icon(Icons.clear),
offstage: _username == '',
),
//点击清除文本框内容
onTap: () {
this.setState(() {
_username = '';
_usernameController.clear();
});
},
),
),
//使用 onChanged 完成双向绑定
onChanged: (value) {
this.setState(() {
_username = value;
});
},
);
}
Widget _getPasswordInput() {
return _getInputTextField(
TextInputType.text,
obscureText: true,
controller: _passwordController,
decoration: InputDecoration(
hintText: "输入密码",
icon: Icon(
Icons.lock_open,
size: 20.0,
),
suffixIcon: GestureDetector(
child: Offstage(
child: Icon(Icons.clear),
offstage: _password == '',
),
onTap: () {
this.setState(() {
_password = '';
_passwordController.clear();
});
},
),
border: InputBorder.none,
),
onChanged: (value) {
this.setState(() {
_password = value;
});
},
);
}
Widget _getInputTextField(
TextInputType keyboardType, {
FocusNode focusNode,
controller: TextEditingController,
onChanged: Function,
InputDecoration decoration,
bool obscureText = false,
height = 50.0,
}) {
return Container(
height: height,
margin: EdgeInsets.all(10.0),
child: Column(
children: [
TextField(
keyboardType: keyboardType,
focusNode: focusNode,
obscureText: obscureText,
controller: controller,
decoration: decoration,
onChanged: onChanged,
),
Divider(
height: 1.0,
color: Colors.grey[400],
),
],
),
);
}
}
======================
优化
class FormUtil {
static Widget textField(
String formKey,
String value, { // 是否显示清除按钮
TextInputType keyboardType = TextInputType.text,
FocusNode focusNode,
controller: TextEditingController,
onChanged: Function,
String hintText,
IconData prefixIcon,
onClear: Function,
bool obscureText = false,
height = 50.0,
margin = 10.0,
}) {
return Container(
height: height,
margin: EdgeInsets.all(margin),
child: Column(
children: [
TextField(
keyboardType: keyboardType,
focusNode: focusNode,
obscureText: obscureText,
controller: controller,
decoration: InputDecoration(
hintText: hintText,
icon: Icon(
prefixIcon,
size: 20.0,
),
border: InputBorder.none,
suffixIcon: GestureDetector(
child: Offstage(
child: Icon(Icons.clear),
offstage: value == null || value == '',
),
onTap: () { // 错误写法:onTap:onClear
onClear(formKey);
},
),
),
onChanged: (value) {
onChanged(formKey, value);
}),
Divider(
height: 1.0,
color: Colors.grey[400],
),
],
),
);
}
}
Map> _formData = {
'username': {
'value': '',
'controller': TextEditingController(),
'obsecure': false,
},
'password': {
'value': '',
'controller': TextEditingController(),
'obsecure': true,
},
};
_handleTextFieldChanged(String formKey, String value) {
this.setState(() {
_formData[formKey]['value'] = value;
});
}
_handleClear(String formKey) {
this.setState(() {
_formData[formKey]['value'] = '';
(_formData[formKey]['controller'] as TextEditingController)?.clear();
});
}
FormUtil.textField(
'username',
_formData['username']['value'],
controller: _formData['username']['controller'],
hintText: '请输入手机号',
prefixIcon: Icons.mobile_friendly,
onChanged: _handleTextFieldChanged,
onClear: _handleClear,
),
FormUtil.textField(
'password',
_formData['password']['value'],
controller: _formData['password']['controller'],
obscureText: true,
hintText: '请输入密码',
prefixIcon: Icons.lock_open,
onChanged: _handleTextFieldChanged,
onClear: _handleClear,
),
- 单选开关(Switch)、复选框(Checkbox、CheckboxListTitle)、 单选框(Radio、RadioListTitle)、滑动选择器(Slider)、步骤(Stepper)
属于Material组件库。
1. 都继承自StatefulWidget,但它们本身不会保存当前状态(如:选中),状态由父组件来管理。
2. 被点击时,会触发它们的onChanged回调。
目前为止:
Checkbox的大小是固定的,无法自定义。
Switch只能定义宽度,高度也是固定的。
Checkbox的tristate属性(是否为三态,默认值为false )为true时,value值除了true(选中状态)和false(不选中状态)多了一个 null(空状态)。
示例(Switch、Checkbox)
class SwitchAndCheckBoxTestRoute extends StatefulWidget {
@override
_SwitchAndCheckBoxTestRouteState createState() => new _SwitchAndCheckBoxTestRouteState();
}
class _SwitchAndCheckBoxTestRouteState extends State {
bool _switchSelected=true; // 维护单选开关状态
bool _checkboxSelected=true;// 维护复选框状态
@override
Widget build(BuildContext context) {
return Column(
children: [
Switch(
// activeColor: Colors.red, // 选中时的颜色
value: _switchSelected, // 是否选中
onChanged:(value){
// 重新构建页面
setState(() {
_switchSelected=value;
});
},
),
// Divider(), 分割线
Checkbox(
value: _checkboxSelected, // 是否选中
activeColor: Colors.red, // 选中时的颜色
// checkColor:Colors.white, // 选中时,对号的颜色
onChanged:(value){ // 状态改变时调用
setState(() {
_checkboxSelected=value;
});
} ,
)
],
);
}
}
// 可以设置List,循环创建Checkbox,value为List中对应的值,onChanged中修改List中对应的值
List checkList=[
{
"checked":true,
"title":"姓名"
},
];
Switch({
Key? key,
required this.value, // 是否为开启状态
required this.onChanged, //
this.activeColor,
this.activeTrackColor,
this.inactiveThumbColor,
this.inactiveTrackColor,
this.activeThumbImage,
this.onActiveThumbImageError,
this.inactiveThumbImage,
this.onInactiveThumbImageError,
this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor,
this.focusColor,
this.hoverColor,
this.splashRadius,
this.focusNode,
this.autofocus = false,
})
SwitchListTile
SwitchListTile({
Key? key,
required this.value,
required this.onChanged,
this.tileColor,
this.activeColor,
this.activeTrackColor,
this.inactiveThumbColor,
this.inactiveTrackColor,
this.activeThumbImage,
this.inactiveThumbImage,
this.title,
this.subtitle,
this.isThreeLine = false,
this.dense,
this.contentPadding,
this.secondary,
this.selected = false,
this.autofocus = false,
this.controlAffinity = ListTileControlAffinity.platform,
this.shape,
this.selectedTileColor,
})
Checkbox
Checkbox({
Key? key,
required this.value, // 值,true/false
this.tristate = false,
required this.onChanged, // 选中状态改变后回调,参数为value
this.mouseCursor,
this.activeColor, // 选中色
this.checkColor,
this.focusColor,
this.hoverColor,
this.splashRadius,
this.materialTapTargetSize,
this.visualDensity,
this.focusNode,
this.autofocus = false,
})
CheckboxListTitle
左边是图片,然后是标题、副标题(从上到下),右边是选择框
1. value
是否选中
2. onChanged
状态改变后的回调
3. activeColor
选中时的颜色
4. title
标题
5. subtitle
副标题
6. secondary
图片
7. selected
是否选中状态
Radio (单选框)
1. value
该项的值
2. groupValue
一组Radio的当前选中的值
3. onChanged
状态改变后的回调,修改groupValue
通常最外层为Row
RadioListTitle
左边是图片,然后是标题、副标题(从上到下),右边是单选框
1. value
该项的值
2. groupValue
一组Radio的当前选中的值
3. onChanged
状态改变后的回调,修改groupValue
4. title
标题
5. subtitle
副标题
6. secondary
图片
7. selected
是否为选中状态,通过判断groupValue==value
Slider
Slider({
Key? key,
required this.value, // 当前值
required this.onChanged, // 值改变后回调
this.onChangeStart, //
this.onChangeEnd, //
this.min = 0.0, // 最小值
this.max = 1.0, // 最大值
this.divisions, // 分成几份
this.label, // 底部显示文本
this.activeColor, // 已划过颜色
this.inactiveColor, // 未划过颜色
this.mouseCursor,
this.semanticFormatterCallback,
this.focusNode,
this.autofocus = false,
})
Stepper 步骤
Stepper({
Key? key,
required this.steps, //
this.physics,
this.type = StepperType.vertical,
this.currentStep = 0, // 当前步骤
this.onStepTapped, // 点击步骤序号后的回调
this.onStepContinue, // 点击继续的回调
this.onStepCancel, // 点击取消的回调
this.controlsBuilder,
})
Step({
required this.title, //
this.subtitle, //
required this.content, //
this.state = StepState.indexed,
this.isActive = false, // 是否激活,激活时则展开,非激活则收缩
})
- 表单Form
继承自StatefulWidget对象,对应的状态类为FormState。
实际业务中,在正式向服务器提交数据前,都会对各个输入框数据进行合法性校验,但是对每一个TextField都分别进行校验将会是一件很麻烦的事。
为此,Flutter提供了一个Form 组件,它可以对输入框进行分组,然后进行统一操作,如输入内容校验、输入框重置以及输入内容保存。
常用属性
0. child
子组件
1. autovalidate
是否自动校验输入内容。
默认false,需要通过调用FormState.validate()来手动校验。当为true时,每一个子FormField内容发生变化时都会自动校验合法性,并直接显示错误信息。
2. onWillPop
决定Form所在的路由是否可以直接返回(如点击返回按钮)。
返回一个Future对象,false则当前路由不会返回;true则会返回到上一个路由。
3. onChanged
Form的任意一个子FormField内容发生变化时会触发此回调。
FormField
Form的子孙元素必须是FormField类型(抽象类):
FormField({
Key key,
@required this.builder,
this.onSaved, // 保存回调
this.validator, // 验证回调
this.initialValue, // 初始值
this.autovalidate = false, // 是否自动校验
this.enabled = true,
})
为了方便使用,Flutter提供了一个TextFormField组件,它继承自FormField类,也是TextField的一个包装类。
FormState(Form的State类,通过它来对Form的子孙FormField进行统一操作)
获取FormState的两种方式:
1. 给Form设置key后通过GlobalKey获取。
2. Form.of(context)
FormState常用的3个方法:
1. validate():会调用Form子孙FormField的validate回调,如果有一个校验失败,则返回false,所有校验失败项都会返回用户返回的错误提示。
2. save():会调用Form子孙FormField的save回调,用于保存表单内容。
3. reset():会将子孙FormField的内容清空。
示例(FormState的2种获取方式)
在提交之前校验:
用户名不能为空,如果为空则提示“用户名不能为空”。
密码不能小于6位,如果小于6为则提示“密码不能少于6位”。
class FormTestRoute extends StatefulWidget {
@override
_FormTestRouteState createState() => new _FormTestRouteState();
}
class _FormTestRouteState extends State {
TextEditingController _unameController = new TextEditingController();
TextEditingController _pwdController = new TextEditingController();
GlobalKey _formKey= new GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title:Text("Form Test"),
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: Form(
key: _formKey, // 设置globalKey,用于后面获取FormState
autovalidate: true, // 开启自动校验
child: Column(
children: [
TextFormField(
autofocus: true,
controller: _unameController,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
icon: Icon(Icons.person)
),
// 校验用户名
validator: (v) {
return v
.trim()
.length > 0 ? null : "用户名不能为空";
}
),
TextFormField(
controller: _pwdController,
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
icon: Icon(Icons.lock)
),
obscureText: true,
// 校验密码
validator: (v) {
return v
.trim()
.length > 5 ? null : "密码不能少于6位";
}
),
// 登录按钮
Padding(
padding: const EdgeInsets.only(top: 28.0),
child: Row(
children: [
Expanded(
child: RaisedButton(
padding: EdgeInsets.all(15.0),
child: Text("登录"),
color: Theme
.of(context)
.primaryColor,
textColor: Colors.white,
onPressed: () {
// 在这里不能通过Form.of(context)来获取FormState,此处的context为FormTestRoute的context,向根去查找不会找到(Form是在FormTestRoute的子树中)
// 通过_formKey.currentState 获取FormState后,调用validate()方法校验用户名密码是否合法,校验通过后再提交数据。
if((_formKey.currentState as FormState).validate()){
// 验证通过,提交数据
}
},
),
),
],
),
)
],
),
),
),
);
}
}
如果通过Builder来构建登录按钮,Builder会将widget节点的context作为回调参数:
Expanded(
// 通过Builder来获取RaisedButton所在widget树的真正context(Element)
child:Builder(builder: (context){
return RaisedButton(
...
onPressed: () {
// 由于本widget(RaisedButton)也是Form的子代widget,所以可以通过下面方式获取FormState
if(Form.of(context).validate()){
// 验证通过,提交数据
}
},
);
})
)
2. 弹框(对话框)
一个对话框通常会包含标题、内容,以及一些操作按钮。
弹出对话框:showDialog();
// 返回一个Future(用于接收对话框的返回值)。如果是通过点击对话框遮罩关闭的,则Future值为null;否则为通过Navigator.of(context).pop(result)返回的result值。
Future showDialog({
@required BuildContext context,
bool barrierDismissible = true, // 点击遮罩时是否关闭。
WidgetBuilder builder, // 构建UI(AlertDialog、SimpleDialog、Dialog、自定义Widget)。
})
- AlertDialog 常用对话框(属于Material库)
AlertDialog({
Key key,
this.title, // 标题组件
this.titlePadding, // 标题内边距
this.titleTextStyle, // 标题文本样式
this.content, // 对话框内容组件。如果内容过长,内容将会溢出,可以用SingleChildScrollView将内容包裹起来。
this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0), // 内容内边距
this.contentTextStyle, // 内容文本样式
this.actions, // 对话框操作按钮组
this.backgroundColor, // 对话框背景色
this.elevation,// 对话框的阴影
this.semanticLabel, // 对话框语义化标签(用于读屏软件)
this.shape, // 对话框外形
})
如果AlertDialog的内容过长,内容将会溢出,可以用SingleChildScrollView将内容包裹起来。
示例(标题、详情、操作按钮)
RaisedButton(
child: Text("对话框1"),
onPressed: () async {
// 弹出对话框并等待其关闭
bool delete = await showDeleteConfirmDialog1();
if (delete == null) {
print("取消删除");
} else {
print("已确认删除");
}
},
),
// 弹出对话框
Future showDeleteConfirmDialog1() {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("提示"),
content: Text("您确定要删除当前文件吗?"),
actions: [
FlatButton(
child: Text("取消"),
onPressed: () => Navigator.of(context).pop(), // 关闭对话框
),
FlatButton(
child: Text("删除"),
onPressed: () {
// 关闭对话框并返回true,这和路由返回的方式是一致的。
// 一般定义为枚举,然后switch处理showDialog的返回值。
Navigator.of(context).pop(true);
},
),
],
);
},
);
}
示例(Loading加载框)
// 自定义方法
showLoadingDialog() {
// 自定义Dialog
showDialog(
context: context,
barrierDismissible: false, // 点击遮罩不关闭对话框
builder: (context) {
return AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
Padding(
padding: const EdgeInsets.only(top: 26.0),
child: Text("正在加载,请稍后..."),
)
],
),
);
},
);
}
如果想自定义对话框宽度,这时只使用SizedBox或ConstrainedBox是不行的,原因是showDialog中已经给对话框设置了宽度限制,可以使用UnconstrainedBox先抵消showDialog对宽度的限制,然后再使用SizedBox指定宽度。
... // 省略无关代码
UnconstrainedBox(
constrainedAxis: Axis.vertical,
child: SizedBox(
width: 280,
child: AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(value: .8,),
Padding(
padding: const EdgeInsets.only(top: 26.0),
child: Text("正在加载,请稍后..."),
)
],
),
),
),
);
- SimpleDialog(列表选择对话框,属于Material组件库)
SimpleDialog({
Key? key,
this.title,
this.titlePadding = const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0),
this.titleTextStyle,
this.children,
this.contentPadding = const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0),
this.backgroundColor,
this.elevation,
this.semanticLabel,
this.shape,
})
SimpleDialogOption({
Key? key,
this.onPressed,
this.padding,
this.child,
})
示例
Future changeLanguage() async {
int i = await showDialog(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: const Text('请选择语言'), // 标题组件
children: [ // children
SimpleDialogOption( // item。列表项组件使用了SimpleDialogOption组件来包装了一下,它相当于一个TextButton,只不过按钮文案是左对齐的,并且padding较小。
onPressed: () {
// 返回1
Navigator.pop(context, 1); // 通常做成枚举
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: const Text('中文简体'),
),
),
SimpleDialogOption(
onPressed: () {
// 返回2
Navigator.pop(context, 2);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: const Text('美国英语'),
),
),
],
);
});
if (i != null) {
print("选择了:${i == 1 ? "中文简体" : "美国英语"}");
}
}
- Dialog
由于AlertDialog和SimpleDialog(对Dialog类进行了封装)中使用了IntrinsicWidth来尝试通过子组件的实际尺寸来调整自身尺寸,这就导致他们的子组件不能是延迟加载模型的组件(如ListView、GridView 、 CustomScrollView等)。这时可以使用Dialog。
如下代码运行后会报错:
AlertDialog(
content: ListView(
children: ...//省略
),
);
这时可以使用Dialog
Dialog(
child: ListView(
children: ...//省略
),
);
示例
Future showListDialog() async {
int index = await showDialog(
context: context,
builder: (BuildContext context) {
var child = Column(
children: [
ListTile(title: Text("请选择")),
Expanded(
child: ListView.builder(
itemCount: 30,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text("$index"),
onTap: () => Navigator.of(context).pop(index),
);
},
)),
],
);
// 使用AlertDialog会报错
//return AlertDialog(content: child);
return Dialog(child: child);
/*
可以返回其他widget来自定义对话框样式
return UnconstrainedBox(
constrainedAxis: Axis.vertical,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 280),
child: Material(
child: child,
type: MaterialType.card,
),
),
);
*/
},
);
if (index != null) {
print("点击了:$index");
}
}
自定义Dialog(继承Dialog,重写build函数来自定义)
默认的Dialog是满屏的。
showDialog(
context: context,
builder: (context){
return MyDialog();
}
)
class MyDialog extends Dialog{
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency, // 透明
child: Column( // 自定义UI
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.all(10),
child: Container(
width: 320,
height: 280,
color: Colors.white,
),
)
],
),
);
}
}
对话框的打开动画、遮罩以及原理
对话框的实现原理:对话框最终都是由showGeneralDialog方法打开的。该方法直接调用Navigator的push方法打开了一个新的对话框路由_DialogRoute,然后返回了push的返回值。
showGeneralDialog()方法:打开一个普通风格(非Material风格)的对话框。
/*
showDialog()方法就是对showGeneralDialog进行了封装,定制了Material风格对话框的遮罩颜色和动画。Material风格对话框打开/关闭动画是一个Fade(渐隐渐显)动画,如果想使用一个缩放动画就可以通过transitionBuilder来自定义。
*/
Future showGeneralDialog({
@required BuildContext context,
@required RoutePageBuilder pageBuilder, // 构建对话框内部UI
bool barrierDismissible, // 点击遮罩是否关闭对话框
String barrierLabel, // 语义化标签(用于读屏软件)
Color barrierColor, // 遮罩颜色
Duration transitionDuration, // 对话框打开/关闭的动画时长
RouteTransitionsBuilder transitionBuilder, // 对话框打开/关闭的动画
}){
// 可以看到对话框实际上是通过路由实现的,所以可以使用Navigator的pop方法来退出对话框。
return Navigator.of(context, rootNavigator: true).push(_DialogRoute(
pageBuilder: pageBuilder,
barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel,
barrierColor: barrierColor,
transitionDuration: transitionDuration,
transitionBuilder: transitionBuilder,
));
}
示例(打开动画、遮罩)
封装一个showCustomDialog方法,它定制的对话框动画为缩放动画,并同时制定遮罩颜色为Colors.black87。
Future showCustomDialog({
@required BuildContext context,
bool barrierDismissible = true,
WidgetBuilder builder,
}) {
final ThemeData theme = Theme.of(context, shadowThemeOnly: true);
return showGeneralDialog(
context: context,
pageBuilder: (BuildContext buildContext, Animation animation,
Animation secondaryAnimation) {
final Widget pageChild = Builder(builder: builder);
return SafeArea(
child: Builder(builder: (BuildContext context) {
return theme != null
? Theme(data: theme, child: pageChild)
: pageChild;
}),
);
},
barrierDismissible: barrierDismissible,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: Colors.black87, // 自定义遮罩颜色
transitionDuration: const Duration(milliseconds: 150),
transitionBuilder: _buildMaterialDialogTransitions,
);
}
Widget _buildMaterialDialogTransitions(
BuildContext context,
Animation animation,
Animation secondaryAnimation,
Widget child) {
// 使用缩放动画
return ScaleTransition(
scale: CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
),
child: child,
);
}
... //省略无关代码
showCustomDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("提示"),
content: Text("您确定要删除当前文件吗?"),
actions: [
FlatButton(
child: Text("取消"),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text("删除"),
onPressed: () {
// 执行删除操作
Navigator.of(context).pop(true);
},
),
],
);
},
);
对话框状态管理
当用户选择删除文件时,应询问是否删除此文件;
当用户选择删除文件夹时,应询问是否删除子文件夹(为了避免二次弹窗确认是否删除子目录,在对话框底部添加一个同时删除子目录的复选框)。
错误代码如下:
运行下面的代码时会发现复选框不会被选中。
根本原因就是context不对,setState方法只会针对当前context的子树重新build,此处的setState方法是_DialogRouteState的(即当前context是_DialogRoute对应的Element),但对话框并不是在_DialogRouteState的build方法中构建的,而是通过showDialog构建的(都不是同一个路由页面),所以在_DialogRouteState的context中调用setState是无法影响通过showDialog构建的UI的。
class _DialogRouteState extends State {
bool withTree = false; // 复选框选中状态
@override
Widget build(BuildContext context) {
return Column(
children: [
RaisedButton(
child: Text("对话框2"),
onPressed: () async {
bool delete = await showDeleteConfirmDialog2();
if (delete == null) {
print("取消删除");
} else {
print("同时删除子目录: $delete");
}
},
),
],
);
}
Future showDeleteConfirmDialog2() {
withTree = false; // 默认复选框不选中
return showDialog(
context: context, 特别注意:这里的context是_DialogRoute的context,而下面的context是对话框的根context。不是同一个!!!
builder: (context) {
return AlertDialog(
title: Text("提示"),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text("您确定要删除当前文件吗?"),
Row(
children: [
Text("同时删除子目录?"),
Checkbox(
value: withTree,
onChanged: (bool value) {
// 复选框选中状态发生变化时重新构建UI
setState(() {
// 更新复选框状态
withTree = !withTree;
});
},
),
],
),
],
),
actions: [
FlatButton(
child: Text("取消"),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text("删除"),
onPressed: () {
// 执行删除操作
Navigator.of(context).pop(withTree);
},
),
],
);
},
);
}
}
让复选框可点击,通常有如下三种方法:
方法1: 单独抽离出StatefulWidget
既然是context不对,那最直接的思路就是将复选框的选中逻辑单独封装成一个StatefulWidget,然后在其内部管理复选状态。
// 单独封装一个内部管理选中状态的复选框组件
// 本质上就是将对话框的状态置于一个StatefulWidget的上下文中,由StatefulWidget在内部管理。
class DialogCheckbox extends StatefulWidget {
DialogCheckbox({
Key key,
this.value,
@required this.onChanged,
});
final ValueChanged onChanged;
final bool value;
@override
_DialogCheckboxState createState() => _DialogCheckboxState();
}
class _DialogCheckboxState extends State {
bool value;
@override
void initState() {
value = widget.value;
super.initState();
}
@override
Widget build(BuildContext context) {
return Checkbox(
value: value,
onChanged: (v) {
// 将选中状态通过事件的形式抛出
widget.onChanged(v);
setState(() {
// 更新自身选中状态
value = v;
});
},
);
}
}
Future showDeleteConfirmDialog3() {
bool _withTree = false; // 记录复选框是否选中
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("提示"),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text("您确定要删除当前文件吗?"),
Row(
children: [
Text("同时删除子目录?"),
DialogCheckbox(
value: _withTree, // 默认不选中
onChanged: (bool value) {
// 更新选中状态
_withTree = !_withTree;
},
),
],
),
],
),
actions: [
FlatButton(
child: Text("取消"),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text("删除"),
onPressed: () {
// 将选中状态返回
Navigator.of(context).pop(_withTree);
},
),
],
);
},
);
}
RaisedButton(
child: Text("话框3(复选框可点击)"),
onPressed: () async {
// 弹出删除确认对话框,等待用户确认
bool deleteTree = await showDeleteConfirmDialog3();
if (deleteTree == null) {
print("取消删除");
} else {
print("同时删除子目录: $deleteTree");
}
},
),
方法2: 使用StatefulBuilder组件
方法1有一个明显的缺点——对话框上所有可能会改变状态的组件都得单独封装在一个在内部管理状态的StatefulWidget中,这样不仅麻烦,而且复用性不大。
/*
Builder组件、StatefulBuilder组件可以获得组件所在位置的真正的Context。
看一下Builder组件的实现:
// Builder实际上只是继承了StatelessWidget,然后在build方法中获取当前context后将构建方法代理到了builder回调。Builder实际上是只获取了StatelessWidget 的context上下文。
class Builder extends StatelessWidget {
const Builder({
Key key,
@required this.builder,
}) : assert(builder != null),
super(key: key);
final WidgetBuilder builder;
@override
Widget build(BuildContext context) => builder(context);
}
看一下StatefulBuilder组件的实现:
class StatefulBuilder extends StatefulWidget {
const StatefulBuilder({
Key key,
@required this.builder,
}) : assert(builder != null),
super(key: key);
final StatefulWidgetBuilder builder;
@override
_StatefulBuilderState createState() => _StatefulBuilderState();
}
class _StatefulBuilderState extends State {
@override
Widget build(BuildContext context) => widget.builder(context, setState);
}
*/
Row(
children: [
Text("同时删除子目录?"),
// 使用StatefulBuilder来构建StatefulWidget上下文
StatefulBuilder(
builder: (context, _setState) {
return Checkbox(
value: _withTree, // 默认不选中
onChanged: (bool value) {
// 调用后builder方法会重新被调用
_setState(() {
// 更新选中状态
_withTree = !_withTree;
});
},
);
},
),
],
),
本质上就是子组件Checkbox通知父组件StatefulBuilder重新build子组件本身来实现UI更新的。
方法3(更好的办法)
调用对话框UI的Element对象(context)的markNeedsBuild()方法将其标为“dirty”。
Future showDeleteConfirmDialog4() {
bool _withTree = false;
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("提示"),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text("您确定要删除当前文件吗?"),
Row(
children: [
Text("同时删除子目录?"),
Checkbox( // 依然使用Checkbox组件
value: _withTree,
onChanged: (bool value) {
// 此时context为对话框UI的根Element,直接将对话框UI对应的Element标记为dirty。
(context as Element).markNeedsBuild();
_withTree = !_withTree;
},
),
],
),
],
),
actions: [
FlatButton(
child: Text("取消"),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text("删除"),
onPressed: () {
// 执行删除操作
Navigator.of(context).pop(_withTree);
},
),
],
);
},
);
}
此时的context用的是对话框的根context,所以会导致整个对话框UI组件全部rebuild,因此最好的做法是将context的“范围”缩小,也就是说只将Checkbox的Element标记为dirty。
优化:
Row(
children: [
Text("同时删除子目录?"),
// 通过Builder来获得构建Checkbox的context,常用的缩小context范围的方式
Builder(
builder: (BuildContext context) {
return Checkbox(
value: _withTree,
onChanged: (bool value) {
(context as Element).markNeedsBuild();
_withTree = !_withTree;
},
);
},
),
],
),
- showModalBottomSheet 底部(列表)弹框
从设备底部向上弹出一个Material风格的全屏菜单列表对话框。
实现原理和showGeneralDialog实现原理相同,都是通过路由的方式来实现的。
示例(showModalBottomSheet)
RaisedButton(
child: Text("显示底部菜单列表"),
onPressed: () async {
int type = await _showModalBottomSheet();
print(type);
},
),
// 弹出底部菜单列表模态对话框
Future _showModalBottomSheet() {
return showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return ListView.builder(
itemCount: 30, // 个数
itemBuilder: (BuildContext context, int index) { // itemBuilder
return ListTile(
title: Text("$index"),
onTap: () => Navigator.of(context).pop(index),
);
},
);
},
);
}
示例(showBottomSheet)
// 通过调用widget树顶部的Scaffold组件的ScaffoldState的showBottomSheet同名方法实现。即调用该方法就必须得保证父级组件中有Scaffold。
// 返回一个controller,包含了一些控制对话框的方法(如close方法关闭该对话框)。
PersistentBottomSheetController _showBottomSheet() {
return showBottomSheet(
context: context,
builder: (BuildContext context) {
return ListView.builder(
itemCount: 30,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text("$index"),
onTap: (){
print("$index");
Navigator.of(context).pop();
},
);
},
);
},
);
}
/*
例
final _bottomSheetScaffoldedKey=GlobalKey();
_openBottomSheet(){
_bottomSheetScaffoldedKey.currentState.showBottomSheet((BuildContext context){
return BottomAppBar(
child: Container(
height: 90.0,
width: double.infinity,
padding: EdgeInsets.all(16.0),
child: Row(
children: [
Icon(Icons.pause_circle_outline),
SizedBox(width: 16.0,),
Text('hello'),
Expanded(
child: Text('world'),
)
],
),
),
)
});
}
*/
示例(showModalBottomSheet)
Future _showCustomModalBottomSheet(context, List options) async {
return showModalBottomSheet(
backgroundColor: Colors.transparent,
isScrollControlled: true, // true全屏、默认为false。
context: context,
builder: (BuildContext context) {
return Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(20.0),
topRight: const Radius.circular(20.0),
),
),
height: MediaQuery.of(context).size.height / 2.0,
child: Column(children: [
SizedBox(
height: 50,
child: Stack(
textDirection: TextDirection.rtl,
children: [
Center(
child: Text(
'底部弹窗',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 16.0),
),
),
IconButton(
icon: Icon(Icons.close),
onPressed: () {
Navigator.of(context).pop();
}),
],
),
),
Divider(height: 1.0),
Expanded(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(options[index]),
onTap: () {
Navigator.of(context).pop(index);
});
},
itemCount: options.length,
),
),
]),
);
},
);
}
- ExpansionPanelList伸缩面板
ExpansionPanelList({
Key? key,
this.children = const [],
this.expansionCallback, // 点击右侧箭头图标后回调,(panelIndex,isExpanded)
this.animationDuration = kThemeAnimationDuration,
this.expandedHeaderPadding = _kPanelHeaderExpandedDefaultPadding,
this.dividerColor,
this.elevation = 2,
})
ExpansionPanel({
required this.headerBuilder, // 头部builder,参数(context,isExpanded)
required this.body, // 主体
this.isExpanded = false, // false收起,true展开
this.canTapOnHeader = false,
})
- SnackBar 弹框
SnackBar({
Key? key,
required this.content,
this.backgroundColor,
this.elevation,
this.margin,
this.padding,
this.width,
this.shape,
this.behavior,
this.action,
this.duration = _snackBarDisplayDuration,
this.animation,
this.onVisible,
})
SnackBarAction({
Key? key,
this.textColor, // 文本色
this.disabledTextColor,
required this.label, // 文本
required this.onPressed, // 点击后回调
})
示例
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(notice.message),
duration: notice.duration,
));
示例(使用Scaffold.of方法)
注意:
Scaffold.of方法不能直接在Scaffold的build方法中调用否则会抛异常。
1. 可以在Scallfold子组件的Build方法中调用
2. 也可以用Builder组件解决
3. 也可以给Scaffold设置key,使用_scaffoldKey.currentState调用showSnackBar方法。
// 在Scallfold子组件的Build方法中调用
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Code Sample for Scaffold.of.',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: MyScaffoldBody(), // 如果是在这里的点击方法中调用Scaffold.of()会报错
appBar: AppBar(title: Text('正确示例')),
),
color: Colors.white,
);
}
}
class MyScaffoldBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: Text('world'),
onPressed: () {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('hello'),
),
);
},
),
);
}
}
- fluttertoast 三方库
1. 添加依赖包,并下载(pubspec.yaml文件中dependencies下)
fluttertoast: #lastversion
2. 导入包
import 'package:fluttertoast/fluttertoast.dart';
3. 使用
Fluttertoast.showToast(
msg: "加载中", // 提示文本
toastLength: Toast.LENGTH_SHORT, // 类型:短、长
gravity: ToastGravity.CENTER, // 位置
timeInSecForIosWeb: 1, // 持续时间
backgroundColor: Colors.red, // 背景色
textColor: Colors.white, // 文本色
fontSize: 16.0, // 文本字体
),
计时器
_showTimer(context){
var timer;
timer = Timer.periodic(
Duration(microseconds: 3000),(t) { // 3s后执行
Navigator.pop(context);
t.cancel();// 取消定时器,timer.cancel() 也一样
},
);
}