上一篇博客中我们了解到,flutter中一切皆widget,并且我们学习了widget相关比较详细的生命周期等相关的介绍,但是,我们还需要了解一个概念,那就是Element
Element
flutter中我们开发的时候都是开发的一个个widget,在builder方法中进行每个widget的ui构建,使用setState进行状态的更新,从而刷新ui,但是在flutter中真正的元素相关的类不是widget,而是Element,而widget属于flutter中用来描述ui的一个基本数据,我们可以理解为一个组件的完整构建包装类,而每一个widget都会绑定对应的Element元素,但是我们需要注意的是一个Element元素只能对应一个widget,但是一个widget可以对应多个Element元素,原因就在于widget可以包含多个其他的widget,相当于一个ui树可以挂载其他的节点一般
接下来我们大概看一下flutter中的所有widget的基类的大概源码申明:
//widget继承自DiagnosticableTree,DiagnosticableTree是一个诊断树,主要用来提供debug调试诊断功能的ui树
@immutable
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
//每个widget都可以设置一个唯一的key,这个key在js的组件开发中并不陌生类似于组件的ref,在安卓原生开发中类似于每一个ui元素的唯一id,可以用来在canUpdate方法中判断是不是能够复用原来的widget
final Key key;
//我们刚刚说一个element元素肯定指定了一个widget,就是这个方法,我们开发基本不使用,系统也会默认在创建widget的时候创建一个element,挂载在ui树上
@protected
Element createElement();
@override
String toStringShort() {
return key == null ? '$runtimeType' : '$runtimeType-$key';
}
//用来debug的模式下可以进行诊断的方法
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
//widget具体的更新ui是否复用旧的组件的策略方法,源码会将旧的widget的key和runtimeType(类型)与调用修改ui的方法以后的widget的key和类型进行比较,如果都是一样的,说明我们当前需要更新操作,就会将原来的widget的对应改变的属性修改,如果不一样,就会创建调用修改ui方法后指定的widget挂载在ui树对应的位置上(同时我们也能看出来,flutter的组件中如果都指定了key的话,可以有效提升ui效率)
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
从上面大概可以看出来一个基本的widget包含的大概的方法结构,而我们开发使用的widget只有两种,一个是StatelessWidget 和StatefulWidget ,这两个都直接或者间接继承了Widget,而我们flutter的sdk中默认提供了常见的组件,全部都基于这两类widget,所以接下来我们来开始正式的了解flutter常见的ui组件相关的内容
渲染组件分类
我们上面说过,flutter中真正渲染的不是widget,而是Element,而widget和Element之间是createElement方法绑定的,而flutter中将Element总共分成了三类(博主基本上组件都看了一遍,应该只有这三类),大概如下:
Widget类型 | Element类型 | 说明 |
---|---|---|
LeafRenderObjectWidget | LeafRenderObjectElement | 叶子节点,一般来说是flutter中最基础的最小的组件,不能包裹其他任何Widget的常见组件,比如Text、Icon等 |
SingleChildRenderObjectWidget | SingleChildRenderObjectElement | 包裹类的组件,这种组件普遍是用来提供约束或者提供特殊功能如提供点击事件等的组件,内部可以包含一个基础组件,比如 InkWell等 |
MultiChildRenderObjectWidget | MultiChildRenderObjectElement | 容器类组件,这种组件一般可以用作布局操作,类似原生的布局容器,内部包裹多个widget,比如Stack、Row等 |
我们可以看到渲染组件widget其实对应的类型有三种,其实我们开发中使用的所有的组件几乎都是直接或者间接继承了这三类widget,而这三种wieget最终都是来源于RenderObjectWidget ,这个RenderObjectWidget 才是我们最终进行渲染ui的对象,所以整体的继承关系如下:
Widget > RenderObjectWidget > (Leaf/SingleChild/MultiChild)RenderObjectWidget
基础组件(子组件)
Text组件
flutter中,我们如果需要进行文本或者文本相关属性的样式等,就需要使用Text组件,(类似于原生安卓中的TextView),接下来我们来看下Text组件的构造
/// Creates a text widget.
///
/// If the [style] argument is null, the text will use the style from the
/// closest enclosing [DefaultTextStyle].
const Text(this.data, {//data必传,说明必须要有一个文本的内容
Key key,//每一个widget都会有key字段,用作唯一索引
this.style,//样式,类型为TextStyle
this.strutStyle,
this.textAlign,//文本对齐方式,类型为TextAlign类型
this.textDirection,//文本的方向,类型为TextDirection类型
this.locale,//很少使用,用于在相同的Unicode字符可以选择字体时,会根据当前的国际化语言展示不一样的内容
this.softWrap,//柔软包裹,默认false,作用是是否选择在换行符的地方触发换行操作,默认是不限制水平空间包裹范围
this.overflow,//处理溢出的方式,参数类型为TextOverflow
this.textScaleFactor,//文本缩放因子
this.maxLines,//最大的行数,可以限制文本能展示的最大的行数
this.semanticsLabel,
}) : assert(data != null),
textSpan = null,
super(key: key);
上面的构造我们大概知道了每个属性的作用,这里我们通过一个案例来说明overflow、maxLines 和textScaleFactor 等看起来具有特殊效果的属性,如下:
Text("Hello world",
textAlign: TextAlign.center,//案例1
);
Text("Hello world! . " * 6
maxLines: 1,
overflow: TextOverflow.ellipsis,//案例2
);
Text("Hello world",
textScaleFactor: 1.5,//案例3
);
Text("Hello world "*10, //案例4:字符串后面使用*代表当前的字符串重复出现n次(这是因为dart语言中可以重写运算符)
textAlign: TextAlign.center,
);
上面的案例可以显示出来一个很简单的文本效果,但是我们要知道的是:
- 案例1中我们指定了textAlign属性,这个属性指定了居中,但是如果我们运行起来的话,会发现文本不是居中显示的,这是因为textAlign属性针对的是当前的子组件生效,只要我们的Text宽度没有指定为一行,这个属性指定居中没有任何效果
- 如果说案例1是因为文本不够的话,那么案例2我们重复出现了6次一样的内容,而在flutter中Text展示文本的内容如果满了一行会自动换行的,所以案例2应该换行显示出来结果,可是如果我们运行起来会发现结果根本不是这样,而是一行显示,超过的内容自动变成了省略号代替,看到这里我们可能理解了overflow的作用,用来处理文本内容超了以后的效果,而设置为TextOverflow.ellipsis以后就是设置超过的内容为省略号
- 案例3我们可以看到设置的属性是textScaleFactor,而当我们运行起来的时候,会惊讶的发现,文本变大到原来的150%,看到这里就明白了textScaleFactor所谓的缩放因子作用就是把整个文本的内容按照比例缩放
- 最后一个案例,我们可以理解为对第一个案例最有利的补充,当然我们运行起来以后的效果的确证明了Text会自动换行,并且有文本进行居中,但是我们需要注意一点,TextAlign.center文本居中并不是按照总共的文本整体进行居中以后计算该有的行数和位置,而是满一行的内容占满一行,多出来并且不足一行的文本进行居中显示
TextStyle
上面我们也看到文本可以设置样式,并且类型是TextStyle类型,那么我们接下来看看TextStyle的构造函数
const TextStyle({
this.inherit = true,//是否使用外部组件(或者全局)的样式来替换当前的style,默认是true
this.color,//文本的颜色
this.fontSize,//文本大小
this.fontWeight,//文本字体粗细
this.fontStyle,//文本的样式
this.letterSpacing,//每一个字母之间的距离,可以使用负值缩小空白
this.wordSpacing,//每个单词(相当于一个连续的文本)之间的距离,可以使用负值缩小空白
this.textBaseline,//文本对齐的基准线
this.height,//文字高度
this.locale,//根据你当前国际化本地环境的情况自动切换文本语言
this.foreground,//绘制一个前景(与背景相反),类型为Paint
this.background,//绘制一个背景,类型为Paint
this.shadows,//将在文本下方绘制阴影列表
this.decoration,//可以在文本下方进行装饰,比如绘制一个下划线,类型为TextDecoration
this.decorationColor,//文本下方装饰器的颜色
this.decorationStyle,//文本装饰器的样式
this.debugLabel,//debug模式下默认显示的提示
String fontFamily,//用来重新创建文本样式(一般使用在我们外部引入的字体库的时候,但是外部引入字体库以后这里必须指定package,package+fontFamily组合确定fontFamily属性的具体样式)
List fontFamilyFallback,
String package,//当我们需要fontFamily的时候我们就需要指定当前的字体库的包名
}) : fontFamily = package == null ? fontFamily : 'packages/$package/$fontFamily',
_fontFamilyFallback = fontFamilyFallback,
_package = package,
assert(inherit != null),
assert(color == null || foreground == null, _kColorForegroundWarning);
好了,TextStyle相关的属性基本上博主都标注起来了,接下来我们举一个案例来加深对TextStyle的部分容易混淆的属性的理解:
Text("Hello world",
style: TextStyle(
color: Colors.green,
fontSize: 20.0,
height: 1.5,
fontFamily: "Courier",
background: new Paint()..color=Colors.redAccent,
decoration:TextDecoration.underline,
decorationStyle: TextDecorationStyle.dashed
),
);
但是这里我们需要针对几点属性进行一下说明:
- height属性我们这里指定了1.5,可以看出来该属性是一个比例因子,并不是一个具体的值,默认是1,而行高的计算方式是fontsize属性 * height属性则为每一行文本的高度
- fontsize我们可以看出来和原生开发的文本大小似乎一样,可以指定具体的值,而textScaleFactor 只是指定了整体的文本的缩放比例,我们可以理解为文本具体展示的大小为textScaleFactor * fontSize的值
TextSpan
上面我们介绍的文本及其样式只能指定一种,而我们开发的过程中往往遇到比较复杂的样式,这个时候我们就可以使用TextSpan指定了,比如需要实现一个带链接效果的文本或者一个多种字体颜色的文本等,我们来看下构造:
const TextSpan({
TextStyle style, //文本样式
Sting text,//文本内容
List children,//这里就是具体指定了多个样式效果的组件集合
GestureRecognizer recognizer,//手势操作识别管理器,可以对绘制样式的流程进行操作
});
我们可以看到TextSpan的构造属性很少,比较简单
默认全局样式
我们上面可以看到TextStyle中有inherit属性,该属性的作用就是是否选择继承默认的样式作为当前的样式,也就是说在开发的过程中,我们可以指定一个全局的样式,这样需要复用的时候就可以直接继承使用,不需要再去指定新的样式(可以理解为原生开发中xml布局文件中style样式复用),接下来我们看下使用的简单案例:
DefaultTextStyle(
//1.设置文本默认样式
style: TextStyle(
color:Colors.red,
fontSize: 20.0,
),
textAlign: TextAlign.start,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("hello world"),
Text("not extends DefaultTextStyle",
style: TextStyle(
inherit: false, //2.不继承默认样式,所以运行的时候的样式就不会是红色
),
),
],
),
);
按钮
在flutter中,按钮的类型比较丰富,不过md风格中的所有的按钮都直接或者间接是RawMaterialButton 的子类,都默认实现了md风格,而在flutter中只要是md风格的按钮,都是默认有按下水波纹的效果,并且都有onPressed 函数作为点击事件触发的回调,如果当前方法并没有重写,那么就默认为禁用点击事件(onPressed为必须填写的参数,所以我们不需要点击事件的时候只要指定一个空方法即可)
RaisedButton
RaisedButton 又叫做漂浮按钮,默认情况下,我们按下的时候,会有阴影和灰色的变大的背景,从而造成一个漂浮的效果,使用方法如下:
RaisedButton(
child: Text("normal"),//按钮中挂载一个内容为normal的文本,即文本漂浮按钮
onPressed: (){
//点击事件
},
);
FlatButton
FlatButton 指的是扁平类的按钮,这种按钮和漂浮按钮比起来,区别在于背景透明并且不带任何阴影,按压以后只有背景色出现,使用如下:
FlatButton(
child: Text("normal"),//按钮中挂载一个内容为normal的文本,即扁平文本按钮
onPressed: (){
//点击事件
},
)
OutlineButton
OutlineButton 是一种带有边框,背景透明并且无阴影的按钮,该按钮在按下以后会出现一个高亮的按钮边框,同时按压的时候会有一个比较淡的背景,使用如下:
OutlineButton(
child: Text("normal"),//按钮中挂载一个内容为normal的文本,即边框文本按钮
onPressed: (){
//点击事件
},
)
IconButton
iconButton是一个可以点击的icon按钮,和其他按钮不一样的是,iconButton不需要指定child,默认只需要指定icon为child即可,并且该按钮同样只有在被按下的时候才会出现背景:
IconButton(
icon: Icon(Icons.thumb_up),//指定一个icon为系统自带的某个icon
onPressed: (){
//点击事件
},
)
由于几种按钮源码几乎都一样,这里我们就来看看IconButton的源码:
const RaisedButton({
Key key,
@required VoidCallback onPressed, //点击事件的回调函数
ValueChanged onHighlightChanged,//基础数据更改的时候触发回调函数
ButtonTextTheme textTheme,//用来指定按钮的主题样式,我们看到的按下效果和有无边框等效果就是当前属性指定,类型为ButtonTextTheme,可以和ButtonTheme、ButtonThemeData一起指定基础按钮颜色背景大小等
Color textColor,//按钮中的文本颜色
Color disabledTextColor,//禁用的时候文本的颜色
Color color,//按钮的颜色
Color disabledColor,//禁用的时候按钮颜色
Color highlightColor,//选中的时候的颜色
Color splashColor,//点击时,水波动画中水波的颜色
Brightness colorBrightness,//按钮的主题颜色,默认是浅色系
double elevation,//未按下的时候,弹出样式的高度(漂浮效果的视觉高度)
double highlightElevation,//当按下的时候,弹出样式的高度(漂浮效果的视觉高度)
double disabledElevation,//禁用的时候按钮的弹出样式的高度
EdgeInsetsGeometry padding,//按钮内部的内边距
ShapeBorder shape,//形状边框
Clip clipBehavior = Clip.none,
MaterialTapTargetSize materialTapTargetSize,//指定当前按钮所指定的material可以点击的部分的尺寸大小,类型为MaterialTapTargetSize
Duration animationDuration,
Widget child,
}) : assert(elevation == null || elevation >= 0.0),
assert(highlightElevation == null || highlightElevation >= 0.0),
assert(disabledElevation == null || disabledElevation >= 0.0),
super(
key: key,
onPressed: onPressed,
onHighlightChanged: onHighlightChanged,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
color: color,
disabledColor: disabledColor,
highlightColor: highlightColor,
splashColor: splashColor,
colorBrightness: colorBrightness,
elevation: elevation,
highlightElevation: highlightElevation,
disabledElevation: disabledElevation,
padding: padding,
shape: shape,
clipBehavior: clipBehavior,
materialTapTargetSize: materialTapTargetSize,
animationDuration: animationDuration,
child: child,
);
大概了解了这些属性以后,我们来实现一个简单的自定义按钮:
new FlatButton(
color: Colors.blue,
highlightColor: Colors.blue[700],//按下的颜色为蓝[700]
colorBrightness: Brightness.dark,//按钮的主题为暗色系
splashColor: Colors.grey,//指定按下的时候水波纹的颜色为灰色
child: Text("RaisedButton Submit"),
shape:RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),//指定按钮的边框为圆形边框
onPressed: (){
//点击事件
},
)
这样我们就实现了一个蓝色按钮,并且按下的时候会有灰色的水波纹覆盖,文本内容为‘RaisedButton Submit’的简单自定义按钮
多选框/单选框
在日常开发中,无论是web前端开发,还是原生安卓开发,一定都会使用到单选按钮和多选按钮这两种基础组件,flutter默认也提供了这两个基础组件,由于这两个组件并不复杂,我们大概看下这两个组件的申明:
单选按钮Switch
const Switch({
Key key,
@required this.value,//单选按钮的值
@required this.onChanged,//当值改变以后触发的回调函数
this.activeColor,//选中时的颜色
this.activeTrackColor,//选中时的单选框轨道颜色,默认的情况下不透明为50%,如果使用Switch.adaptive构造创建当前单选框,当前属性无效
this.inactiveThumbColor,//关闭的时候按压的颜色,如果使用Switch.adaptive构造创建当前单选框,当前属性无效
this.inactiveTrackColor,//在关闭的时候单选框轨道使用的颜色,如果使用Switch.adaptive构造创建当前单选框,当前属性无效
this.activeThumbImage,//选中的时候的图案,如果使用Switch.adaptive构造创建当前单选框,当前属性无效
this.inactiveThumbImage,//关闭的时候按压的图案,如果使用Switch.adaptive构造创建当前单选框,当前属性无效
this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.down,//监听的手势事件为按下事件
}) : _switchType = _SwitchType.material,
assert(dragStartBehavior != null),
super(key: key);
多选按钮checkbox:
const Checkbox({
Key key,
@required this.value,//必传参数,当前多选框的内容
this.tristate = false,//是否开启三态,默认为false
@required this.onChanged,//当前选中状态改变的时候触发的回调函数
this.activeColor,//选中的时候的颜色
this.checkColor,//进行验证的时候的颜色
this.materialTapTargetSize,
}) : assert(tristate != null),
assert(tristate || value != null),
super(key: key);
从上面的属性注释大概可以看出来每一个属性大概的作用,但是我们注意到checkbox上有一个属性tristate,从字面意义来看,是三态的意思,何为三态?这里我们需要注意一下,flutter中的checkbox兼容了web和原生开发,可以支持最大三个值,分别为 false,null和true,而在安卓开发中,一般都是flase或者true,所以这里flutter默认禁止开启三态,即不允许使用null属性作为当前是否选中的标志,如果为true,那么会多一个null属性作为选中标识,这里建议不开启三态,仅仅作为扩展进行了解
表单
除了上述的三种基础组件类型以外,我们平时开发的过程中,使用最多的还有表单组件及其其他组件,比如TextField,Form等
TextField
在前端编程中,我们需要指定一个输入框一般都是Input及其变种组件,而在原生安卓中,这类组件一般为EditText,而在flutter中,这类组件为TextField,接下来我们来看看这类组件的构造:
const TextField({
Key key,
this.controller,//由于是活动的动态的组件,一般会有一个对应的控制器,可以通过绑定控制器,在控制器中获取当前的属性值,焦点操作等,类型为TextEditingController
this.focusNode,//定义了当前组件焦点操作的handler,属于一个长期存在的对象,一般可以交给父组件来管理当前handler,同时,我们可以在当前handler中对焦点事件进行监控,只需要在当前handler中添加一个监听器即可,在FocusScope.of(context).requestFocus(myFocusNode)这种上下文切换的时候或者我们手动调用FocusScope对当前焦点进行改变的时候,就会触发当前的handler监听方法。参数类型为FocusNode
this.decoration = const InputDecoration(),//默认的装饰为在文本下绘制一条线,可以对其设置图标,校验成功的提示文本和错误的时候提示的文本信息
TextInputType keyboardType,//该属性可以指定我们在输入键盘上最后一个按钮是什么,比如我们输入的时候,很多厂商默认的最后一个是完成按钮或者是放大镜图标按钮等,当前属性可以动态修改为其他的按钮,可取值为:(text-文本输入键盘,multiline-多行输入文本,number-只能输入数字的键盘,phone-只能输入电话号码的键盘,datetime-输入日期的键盘,emailAddress-可以输入email的键盘,url-显示url地址的键盘)
this.textInputAction,//当前属性与上一个keyboardType相关,用来定义自定义按钮的类型,只有在按钮的keyboardType是TextInputType.multiline的时候才是TextInputAction.newline,否则其他情况下都是TextInputAction.done
this.textCapitalization = TextCapitalization.none,//文本大写,默认为none
this.style,//当前文本输入框的样式,类型为TextStyle
this.textAlign = TextAlign.start,//指定编辑框内文本的对齐方式,默认是从头开始对齐
this.textDirection,//文字的方向,类型为TextDirection
this.autofocus = false,//当前组件是否默认就是获取焦点的,一般情况下多个组件组合的时候,我们制定第一个为true,其他的都是false,手动控制焦点切换
this.obscureText = false,
this.autocorrect = true,//是否开启自动更正,默认会开启当前辅助功能
this.maxLines = 1,//默认的最大行数,如果上面的keyboardType属性我们设置为multiline类型的话,当前属性设置其他值才会有效,否则默认都是一行
this.maxLength,//设置我们可以输入的最大的文本长度
this.maxLengthEnforced = true,//是否允许超过我们指定的maxLength,如果为true,则会拦截超过文本的输入,不允许其输入,如果为false,不会拦截,但是会在开启验证以后提供计数器和验证后的警告
this.onChanged,//输入改变的回调函数
this.onEditingComplete,//输入完成以后的回调函数
this.onSubmitted,//提交的回调函数
this.inputFormatters,//我们手动指定的输入文本的格式化条件,可以用来校验,参数类型为List
this.enabled,//是否为禁用
this.cursorWidth = 2.0,//默认光标的宽度为2.0
this.cursorRadius,//光标的半径,特殊类型下有必要使用
this.cursorColor,//光标的颜色(默认情况下使用当前主题一致的颜色)
this.keyboardAppearance,//当前属性仅对ios有效,可以调节外观
this.scrollPadding = const EdgeInsets.all(20.0),//默认的padding为全部20px
this.dragStartBehavior = DragStartBehavior.down,//默认手势监听为获取焦点的事件为down事件
this.enableInteractiveSelection,//是否启动交互选择,目前为止从来没用过
this.onTap,//flutter中的组件的自带的点击事件都为onTap
this.buildCounter,//使用widget构建者的回调方法,我们可以在当前方法中使用InputCounterWidgetBuilder进行构建当前的ui
}) : assert(textAlign != null),
assert(autofocus != null),
assert(obscureText != null),
assert(autocorrect != null),
assert(maxLengthEnforced != null),
assert(scrollPadding != null),
assert(dragStartBehavior != null),
assert(maxLines == null || maxLines > 0),
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
super(key: key);
上面我们提到,TextField这类动态组件一般都会绑定controller,用来操作焦点以及获取值等操作,接下来,我们就简单实现一个切换焦点的案例:
import 'package:flutter/material.dart';
class FocusDemo extends StatefulWidget {
@override
_FocusDemoState createState() => new _FocusDemoState();
}
class _FocusDemoState extends State {
FocusNode focusNode1 = new FocusNode();//用来控制用户名焦点的handler
FocusNode focusNode2 = new FocusNode();//用来控制密码焦点的handler
TextEditingController userNameController = new TextEditingController();//用户名绑定的控制器
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
autofocus: true,
focusNode: focusNode1,//关联handler1
controller: userNameController,//关联用户名控制器
decoration: InputDecoration(
labelText: "用户名",
hintText: "请输入用户名",
prefixIcon: new Icon(Icons.person)
),
),
TextField(
focusNode: focusNode2,//关联handler2
decoration: InputDecoration(
labelText: "密码",
hintText: "请输入登录密码",
prefixIcon: new Icon(Icons.lock)
),
),
new Builder(builder: (ctx) {//这里调用builder其实就是new 一个自定义的无状态的widget,快捷创建的封装类,原理就是内部的build方法中传递上下文给我们实现的子组件
return Column(
children: [
RaisedButton(
child: Text("点击当前按钮切换到第二个焦点"),
onPressed: () {//按钮的点击事件
FocusScope.of(context).requestFocus(focusNode2);//将当前的焦点交给第二个,context包含了当前整个完整的节点树的完整信息记录,可以获取当前的焦点所在的组件等
//切换完成,我们这里使用控制器获取当前的输入的用户名的值,输出出来
print('用户名为:'+userNameController.text);
},
)
],
);
},
),
],
),
);
}
}
Form表单
Form表单作为一个很重要的组件,flutter中也提供了常见的操作方法,构造很简单,如下:
const Form({
Key key,
@required this.child,//Form中的子组件为必传,并且这里可以传递多个组件,但是需要注意的是,这里的子组件全部都是FormField类型,而FormField类型其实是TextField的包装类,之间的区别就是内部多了几个针对表单的属性,比如验证等
this.autovalidate = false,//是否对当前表单中所有可以验证的子组件进行自动验证,默认为false
this.onWillPop,//这里如果我们传递了一个最终结果为false的Future,那么就不会出现表单的路由
this.onChanged,//表单的所有元素的焦点改变或者内容改变的时候触发的改变的回调函数
}) : assert(child != null),
super(key: key);
可以看出来Form表单是个包裹的容器,属性很少,但是我们一般使用Form进行验证操作,用来完善业务,而进行校验操作,往往需要涉及到FormState 的概念,FormState 是Form的state类,我们使用的时候可以通过Form.of()或者使用key(GlobalKey )来获取,该状态中我们可以对子组件进行统一的操作,具体如下:
方法 | 说明 |
---|---|
FormState.validate() | 当前方法会触发FormField的 validate回调,如果有一个子组件检验失败,则结果就为false |
FormState.save() | 当前方法会触发FormField的save回调,将每一个子组件的值保存进表单 |
FormState.reset() | 当前方法会将当前Form的所有子组件的值以及校验的结果全部清空,重置整个表单 |
那么,我们大概知道了方法以及操作以后,将上面的登录案例修改一下,加入两个简单的验证,用户名和密码不能为空,并且密码不能小于6位长度,如下:
import 'package:flutter/material.dart';
class FormDemo extends StatefulWidget {
@override
_FormDemoState createState() => new _FormDemoState();
}
class _FormDemoState extends State {
TextEditingController _unameController = new TextEditingController();//用来控制用户名的控制器
TextEditingController _pwdController = new TextEditingController();//用来控制密码的控制器
GlobalKey _formKey= new GlobalKey();//传说中的key,我们用来和组件关联的key,可以获取组件的很多属性和操作,以及当前的状态等
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),//垂直方向上下分别16px,水平方向左右分别24px
child: Form(
key: _formKey, //设置globalKey,用于后面获取FormState
autovalidate: true, //开启自动校验,只有开启以后,子组件才能在输入以后自动校验,否则只能调用FormState.validate() 进行手动校验
child: Column(
children: [
TextFormField(
autofocus: true,//第一个输入框一般我们默认是获取焦点状态
controller: _unameController,//绑定用户名控制器
decoration: InputDecoration(
labelText: "用户名",
hintText: "请输入用户名",
icon: Icon(Icons.person)
),
// 校验用户名
validator: (value) {//校验的回调,参数为当前组件的值
if(null == value || "" == value || value.trim().length == 0){//dart中字符串比较直接==
return "用户名不能为空";
}
}
),
TextFormField(
controller: _pwdController,
decoration: InputDecoration(
labelText: "密码",
hintText: "请输入密码",
icon: Icon(Icons.lock)
),
obscureText: true,
//校验密码
validator: (value) {
if(null == value || "" == value || value.trim().length == 0){//dart中字符串比较直接==
return "密码不能为空";
}
if(value.trim().length < 6){
return "密码不能小于六位数";
}
}
),
// 登录按钮
Padding(
padding: const EdgeInsets.only(top: 28.0),
child: Row(
children: [
Expanded(
child: RaisedButton(
padding: EdgeInsets.all(15.0),//上下左右都是15px边距
child: Text("点击登录"),
color: Theme.of(context).primaryColor,//这里获取的颜色为当前app系统主题颜色
textColor: Colors.white,
onPressed: () {
//在点击事件中,我们需要再去手动校验一次,然后就可以做其他操作了,也可以选择不去手动表单校验,获取控制器对应的值进行校验
print(_unameController.text);
print(_pwdController.text);
//我们也可以用过key获取当前的组件以及当前组件的state(datr语法中使用as就会将当前类型强制转换为后面的类型,这个时候的类型动态的变成后面的了,所以可以直接用对应类型的方法或者属性,但是as方法有风险,不是父子类或者with等关系下,不要使用,否则会出现异常)
if((_formKey.currentState as FormState).validate()){
//校验通过才会执行到回调中,这里我们可以进行提交等操作
}
},
),
),
],
),
)
],
),
),
),
);
}
}
好了,简单的登录以及表单验证操作完成了,现在,我们对常见的基础组件的了解已经很深了,接下来,我们开始学习常见的包裹类组件(本篇博客开头的SingleChildRenderObjectWidget类型的组件)