文章已同步至掘金:https://juejin.cn/post/6844903957702148110
欢迎访问😃,有任何问题都可留言评论哦~
Flutter 的Material组件库中提供了输入框组件TextField
和表单组件Form
。
输入框(TextField)
TextField
主要用于文本输入。
源码示例
构造函数如下:
const TextField({
...
TextEditingController controller,
FocusNode focusNode,
InputDecoration decoration = const InputDecoration(),
TextInputType keyboardType,
TextInputAction textInputAction,
TextStyle style,
TextAlign textAlign = TextAlign.start,
bool autofocus = false,
bool obscureText = false,
int maxLines = 1,
int maxLength,
bool maxLengthEnforced = true,
ValueChanged<String> onChanged,
VoidCallback onEditingComplete,
ValueChanged<String> onSubmitted,
List<TextInputFormatter> inputFormatters,
bool enabled,
this.cursorWidth = 2.0,
this.cursorRadius,
this.cursorColor,
...
})
不重要的就没粘
属性解释
-
controller
:编辑框的控制器,通过它可以设置/获取编辑框的内容、选择编辑内容、监听编辑文本改变事件等。
大多数情况下我们都需要显式提供一个controller
来与文本框交互。如果没有提供controller
,则TextField
内部会自动创建一个。 -
focusNode
:用于控制TextField
是否占有当前键盘的输入焦点。它是我们和键盘交互的一个句柄(handle)。 -
InputDecoration
:用于控制TextField
的外观显示,如提示文本、背景颜色、边框等。
源码示例(给出源码,自己下去尝试一下,不做过多解释)
const InputDecoration({
this.icon,
this.labelText,
this.labelStyle,
this.helperText,
this.helperStyle,
this.hintText,
this.hintStyle,
this.hintMaxLines,
this.errorText,
this.errorStyle,
this.errorMaxLines,
this.hasFloatingPlaceholder = true,
this.isDense,
this.contentPadding,
this.prefixIcon,
this.prefix,
this.prefixText,
this.prefixStyle,
this.suffixIcon,
this.suffix,
this.suffixText,
this.suffixStyle,
this.counter,
this.counterText,
this.counterStyle,
this.filled,
this.fillColor,
this.errorBorder,
this.focusedBorder,
this.focusedErrorBorder,
this.disabledBorder,
this.enabledBorder,
this.border,
this.enabled = true,
this.semanticCounterText,
this.alignLabelWithHint,
})
-
keyboardType
:用于设置该输入框默认的键盘输入类型,TextInputType
枚举值如下:- text:文本输入键盘
- multiline:多行文本,需和
maxLines
配合使用(设为null或大于1) - number:数字;会弹出数字键盘
- phone:优化后的电话号码输入键盘;会弹出数字键盘并显示
* #
- datetime:优化后的日期输入键盘;Android上会显示
: -
- emailAddress:优化后的电子邮件地址;会显示
@ .
- url:优化后的url输入键盘; 会显示
/ .
-
textInputAction
:键盘动作按钮图标(即回车键位图标),它是一个枚举值,有多个可选值。全部取值请参考官方文档:https://api.flutter.dev
示例:当值为TextInputAction.search
时,原生Android系统下键盘样式如下:
-
style
:正在编辑的文本样式有关属性在TextStyle
里设置 -
textAlign
:输入框内编辑文本在水平方向的对齐方式。
如:textAlign: TextAlign.right,
则是从后往左显示 -
autofocus
:是否自动获取焦点。值为true
或者false
-
obscureText
:是否隐藏正在编辑的文本,如用于输入密码的场景等,文本内容会用“•”替换。 -
maxLines
:输入框的最大行数,默认为1;如果为null
,则无行数限制。 -
maxLength
和maxLengthEnforced
:maxLength
代表输入框文本的最大长度,设置后输入框右下角会显示输入的文本计数。maxLengthEnforced
决定当输入文本长度超过maxLength
时是否阻止输入,为true
时会阻止输入,为false
时不会阻止输入但输入框会变红。 -
onChanged
:输入框内容改变时的回调函数;注:内容改变事件也可以通过controller
来监听。 -
onEditingComplete
和onSubmitted
:这两个回调都是在输入框输入完成时触发,比如按了键盘的完成键(对号图标)或搜索键(🔍图标)。不同的是两个回调签名不同,onSubmitted
回调是ValueChanged<String>
类型,它接收当前输入内容做为参数,而onEditingComplete
不接收参数。 -
inputFormatters
:用于指定输入格式;当用户输入内容改变时,会根据指定的格式来校验。 -
cursorWidth
、cursorRadius
和cursorColor
:这三个属性是用于自定义输入框光标宽度、圆角和颜色的。
如:cursorColor: Colors.green
、cursorWidth: 10,
、cursorRadius: Radius.circular(4),
代码示例
TextField(
keyboardType: TextInputType.url,
decoration: InputDecoration(
labelText: 'xxx',
prefix: Icon(Icons.lock),
// enabled: false
),
textInputAction: TextInputAction.next,
style: TextStyle(color: Colors.red),
textAlign: TextAlign.right,
autofocus: false,
// obscureText: true,
maxLines: null,
maxLength: 10,
maxLengthEnforced: true,
cursorColor: Colors.green,
cursorWidth: 10,
cursorRadius: Radius.circular(4),
),
运行效果:
重要内容
Column(
children: <Widget>[
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
prefixIcon: Icon(Icons.person)
),
),
TextField(
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
prefixIcon: Icon(Icons.lock)
),
obscureText: true,
),
],
);
运行效果:
获取输入内容
获取输入内容有两种方式:
-
定义两个变量,用于保存用户名和密码,然后在
onChanged
触发时,各自保存一下输入内容。 -
通过
controller
直接获取。
第一种方式比较简单,所以就不说了,重点说第二种方式,以用户名输入框为例:
- 定义一个
controller
:
//定义一个controller
TextEditingController _unameController = TextEditingController();
- 设置输入框
controller
:
TextField(
autofocus: true,
controller: _unameController, //设置controller
...
)
- 通过
controller
获取输入框内容:
print(_unameController.text)
代码示例:
import 'package:flutter/material.dart';
class CategoryPage extends StatefulWidget {
@override
_CategoryPageState createState() => _CategoryPageState();
}
class _CategoryPageState extends State<CategoryPage> {
//定义一个controller
TextEditingController _unameController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("输入框"),
),
body: Column(
children: <Widget>[
TextField(
autofocus: true,
controller: _unameController, //设置
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
prefixIcon: Icon(Icons.person),
),
onChanged: (String) => { //输入框改变时触发onChanged
print(_unameController.text) //打印输入框的内容
},
),
TextField(
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
prefixIcon: Icon(Icons.lock)),
obscureText: true,
),
],
),
);
}
}
运行效果:
监听文本变化
监听文本变化也有两种方式:
- 设置
onChanged
回调,如:
TextField(
autofocus: true,
onChanged: (v) {
print("onChange: $v");
}
)
- 通过
controller
监听,如:
@override
void initState() {
//监听输入改变
_unameController.addListener((){
print(_unameController.text);
});
}
这两种方式相比,onChanged
是专门用于监听文本变化,而controller
的功能却多一些,除了能监听文本变化外,它还可以设置默认值、选择文本,
例子:
- 创建一个
controller
:
TextEditingController _selectionController = TextEditingController();
- 设置默认值,并从第三个字符开始选中后面的字符:
_selectionController.text="hello world!";
_selectionController.selection=TextSelection(
baseOffset: 2,
extentOffset: _selectionController.text.length
);
- 设置
controller
:
TextField(
controller: _selectionController,
)
代码:
class _CategoryPageState extends State<CategoryPage> {
TextEditingController _selectionController = TextEditingController();
@override
Widget build(BuildContext context) {
_selectionController.text = "我好喜欢你吖!";
_selectionController.selection = TextSelection(
baseOffset: 2, extentOffset: _selectionController.text.length);
return Scaffold(
appBar: AppBar(
title: Text("输入框"),
),
body: Column(
children: <Widget>[
TextField(
controller: _selectionController,
),
],
),
);
}
}
运行效果:
控制焦点
焦点可以通过FocusNode
和FocusScopeNode
来控制。
默认情况下,焦点由FocusScope
来管理,它代表焦点控制范围,可以在这个范围内可以通过FocusScopeNode
在输入框之间移动焦点、设置默认焦点等。
我们可以通过FocusScope.of(context)
来获取Widget树中默认的FocusScopeNode
。
示例要求:
创建两个TextField
,第一个自动获取焦点,然后创建两个按钮:点击第一个按钮可以将焦点从第一个TextField挪到第二个TextField,点击第二个按钮可以关闭键盘。
代码如下:
class FocusTestRoute extends StatefulWidget {
@override
_FocusTestRouteState createState() => new _FocusTestRouteState();
}
class _FocusTestRouteState extends State<FocusTestRoute> {
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: <Widget>[
TextField(
autofocus: true,
focusNode: focusNode1,//关联focusNode1
decoration: InputDecoration(
labelText: "input1"
),
),
TextField(
focusNode: focusNode2,//关联focusNode2
decoration: InputDecoration(
labelText: "input2"
),
),
Builder(builder: (ctx) {
return Column(
children: <Widget>[
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();
},
),
],
);
},
),
],
),
);
}
}
因为我这个模拟器,他不显示键盘,所以当输入框获取焦点的时候,下面会显示一个键盘(我这个不显示)
监听焦点状态改变事件
FocusNode
继承自ChangeNotifier
,通过FocusNode
可以监听焦点的改变事件,如:
...
// 创建 focusNode
FocusNode focusNode = new FocusNode();
...
// focusNode绑定输入框
TextField(focusNode: focusNode);
...
// 监听焦点变化
focusNode.addListener((){
print(focusNode.hasFocus);
});
获得焦点时focusNode.hasFocus
值为true
,失去焦点时为false
。
表单(Form)
前面介绍的输入框可以单个操作,但是往往在开发中,需要多多个输入框同时进行操作,比如清空所有输入框的内容。
为此,Flutter提供了一个Form
组件,它可以对输入框进行分组,然后进行一些统一操作,如输入内容校验、输入框重置以及输入内容保存等。
Form
继承自StatefulWidget
对象,它对应的状态类为FormState
。
源码示例
构造函数如下:
Form({
@required Widget child,
bool autovalidate = false,
WillPopCallback onWillPop,
VoidCallback onChanged,
})
属性解释
-
autovalidate
:是否自动校验输入内容;当为true
时,每一个子FormField
内容发生变化时都会自动校验合法性,并直接显示错误信息。否则,需要通过调用FormState.validate()
来手动校验。 -
onWillPop
:决定Form
所在的路由是否可以直接返回(如点击返回按钮),该回调返回一个Future
对象,如果Future
的最终结果是false
,则当前路由不会返回;如果为true
,则会返回到上一个路由。此属性通常用于拦截返回按钮。 -
onChanged
:Form
的任意一个子FormField
内容发生变化时会触发此回调。
FormField
Form
的子孙元素必须是FormField
类型,FormField
是一个抽象类,定义几个属性,FormState
内部通过它们来完成操作,
FormField
部分构造函数如下:
const FormField({
...
FormFieldSetter<T> onSaved, //保存回调
FormFieldValidator<T> validator, //验证回调
T initialValue, //初始值
bool autovalidate = false, //是否自动校验。
})
为了方便使用,Flutter提供了一个TextFormField
组件,它继承自FormField
类,也是TextField
的一个包装类,所以除了FormField
定义的属性之外,它还包括TextField
的属性。
FormState
FormState
为Form
的State
类,可以通过Form.of()
或GlobalKey
获得。
我们可以通过它来对Form
的子孙FormField
进行统一操作。
常用的三个方法:
-
FormState.validate()
:调用此方法后,会调用Form
子孙FormField
的validate
回调,如果有一个校验失败,则返回false
,所有校验失败项都会返回用户返回的错误提示。 -
FormState.save()
:调用此方法后,会调用Form
子孙FormField
的save
回调,用于保存表单内容。 -
FormState.reset()
:调用此方法后,会将子孙FormField
的内容清空。
代码示例
修改上面用户登录的示例,在提交之前校验:
- 用户名不能为空,如果为空则提示“用户名不能为空”。
- 密码不能小于6位,如果小于6为则提示“密码不能少于6位”。
代码:
import 'package:flutter/material.dart';
class CategoryPage extends StatefulWidget {
@override
_CategoryPageState createState() => _CategoryPageState();
}
class _CategoryPageState extends State<CategoryPage> {
TextEditingController _unameController = new TextEditingController();
TextEditingController _pwdController = new TextEditingController();
GlobalKey _formKey = new GlobalKey<FormState>();
@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: <Widget>[
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: <Widget>[
Expanded(
child: RaisedButton(
padding: EdgeInsets.all(15.0),
child: Text("登录"),
color: Theme.of(context).primaryColor,
textColor: Colors.white,
onPressed: () {
//在这里不能通过此方式获取FormState,context不对
//print(Form.of(context));
// 通过_formKey.currentState 获取FormState后,
// 调用validate()方法校验用户名密码是否合法,校验
// 通过后再提交数据。
if ((_formKey.currentState as FormState).validate()) {
//验证通过提交数据
}
},
),
),
],
),
)
],
),
),
),
);
}
}
运行效果:
注意,登录按钮的
onPressed
方法中不能通过Form.of(context)
来获取,原因是,此处的context
为FormTestRoute
的context
,而Form.of(context)
是根据所指定context
向根去查找,而FormState
是在FormTestRoute
的子树中,所以不行。正确的做法是通过Builder
来构建登录按钮,Builder
会将widget节点的context
作为回调参数:
Expanded(
// 通过Builder来获取RaisedButton所在widget树的真正context(Element)
child:Builder(builder: (context){
return RaisedButton(
...
onPressed: () {
//由于本widget也是Form的子代widget,所以可以通过下面方式获取FormState
if(Form.of(context).validate()){
//验证通过提交数据
}
},
);
})
)
一定要注意context
的指向问题。
评论区