Flutter | 常用组件

0005b72e6a0746b0be300e6117d1595b_tplv-k3u1fbpfcp-zoom-1

文本

  • 常用的配置

1class TextTest extends StatelessWidget { 2 @override 3 Widget build(BuildContext context) { 4 return Scaffold( 5 body: Padding( 6 padding: EdgeInsets.all(30), 7 child: Column( 8 children: <Widget>[ 9 Text("hello "), 10 //最大行数为一 ,TextOverflow.ellipsis :省略号代替 11 Text( 12 "hello! I'm 345 " * 5, 13 maxLines: 1, 14 overflow: TextOverflow.ellipsis, 15 ), 16 //文本缩放因子 17 Text("hello 345", textScaleFactor: 2), 18 // 19 Text("hello 345", 20 style: TextStyle( 21 //颜色 22 color: Colors.red, 23 //字号,默认 14 24 fontSize: 18, 25 //粗细 26 fontWeight: FontWeight.w800, 27 //斜体 28 fontStyle: FontStyle.italic, 29 //underline:下划线,overline:上划线,lineThrough:删除线 30 decoration: TextDecoration.underline, 31 decorationColor: Colors.black, 32 //solid:实线,double:双线,dotted:点虚线,dashed:横虚线,wavy:波浪线 33 decorationStyle: TextDecorationStyle.wavy)) 34 ], 35 ), 36 )); 37 } 38} 39 40

​ textAlign:文本的对齐方式;可以选择左对齐、右对齐还是居中。注意,对齐的参考系是Text widget本身

  • DefaultTextStyle

在 widget 树中,文本的样式默认是可以继承的,因此,如果在 widget 树中的某一个节点设置一个默认的样式,那么该接口的子树中所有文本都会默认使用这个样式

1Widget build(BuildContext context) { 2 return Scaffold( 3 body: Padding( 4 padding: EdgeInsets.all(30), 5 child: DefaultTextStyle( 6 style: TextStyle( 7 //颜色 8 color: Colors.red, 9 //字号,默认 14 10 fontSize: 20, 11 //粗细 12 fontWeight: FontWeight.w900, 13 ), 14 child: Column( 15 children: [ 16 Text("hello "), 17 Text("hello! I'm 345 " * 5, maxLines: 1,overflow: TextOverflow.ellipsis,), 18 //替换默认文本 19 Text("hello 345",style: TextStyle(fontSize: 25, color: Colors.black)) 20 ], 21 )))); 22} 23 24

image-20210201212551841

  • TextSpan

如果需要对一个 Text 按照不同部分进行不同的显示,这个时候就可以使用 TextSpan,他代表文本中的一个片段

const TextSpan({
TextStyle style,
Sting text,
List children,
GestureRecognizer recognizer,
});

1 2style 和 text 表示样式和内容,children 是一个数组,也就是说 TextSpan 可以包含其他的 TextSpan,recognizer 用于对该文本片段上用手势进行识别处理 3 4```dart 5Widget _richText() { 6var textSpan = TextSpan(text: "hello", children: [ 7 TextSpan(text: "3", style: TextStyle(color: Colors.blueAccent)), 8 TextSpan(text: "4", style: TextStyle(color: Colors.black)), 9 TextSpan(text: "5", style: TextStyle(color: Colors.green)) 10]); 11 return Text.rich(textSpan); 12} 13 14

上面使用 Text.rich 方法将 TextSpan 添加到 Text 中,之所以这样做,就是应为 Text 本身就是 RichText 的一个包装,而 RichText 是可以显示多种样式(富文本)的 widget,样式如下:

  • 字体

在 flutter 中使用字体需要两个步骤,首先是在 pubspec.yaml文件中声明,然后通过 textStyle 属性使用字体

1flutter: 2 fonts: 3 - family: Raleway 4 fonts: 5 - asset: assets/fonts/Raleway-Regular.ttf 6 - asset: assets/fonts/Raleway-Medium.ttf 7 weight: 500 8 - asset: assets/fonts/Raleway-SemiBold.ttf 9 weight: 600 10 - family: AbrilFatface 11 fonts: 12 - asset: assets/fonts/abrilfatface/AbrilFatface-Regular.ttf 13 14

使用字体

1// 声明文本样式 2const textStyle = const TextStyle( 3 fontFamily: 'Raleway', 4); 5 6// 使用文本样式 7var buttonText = const Text( 8 "Use the font for this text", 9 style: textStyle, 10); 11 12

按钮

Material 组件库中提供了很多按钮组件,如 RaisedButton,FlatButton,OutlineButton,等,他们都是间接或者直接对 RawMaterialButton 组件的包装定制,所以他们大多是属性都和 RawMaterialButton 一样

另外,所有的 Material 库中的按钮都有如下的相同点:

1,按下都会有 “水波纹动画”

2,都有一个 onPressed 属性来设置点击事件的回调,若没有该回调则按钮会处于禁用状态,禁用状态不响应用户点击

各种常见的按钮

1class Button extends StatelessWidget { 2 @override 3 Widget build(BuildContext context) { 4 return Scaffold( 5 body: Padding( 6 padding: EdgeInsets.all(20), 7 child: Column( 8 children: [ 9 //漂浮按钮,默认有阴影和灰色背景 10 RaisedButton( 11 child: Text("RaisedButton"), 12 onPressed: () => print('RaisedButton'), 13 ), 14 //扁平按钮,默认背景透明不带阴影 15 FlatButton( 16 child: Text("flatButton"), 17 onPressed: () => print('flatButton'), 18 ), 19 //默认有一个边框,不带阴影且背景透明 20 OutlineButton( 21 child: Text("OutlineButton"), 22 onPressed: () => print('OutlineButton'), 23 ), 24 //可点击的 Icon 25 IconButton( 26 icon: Icon(Icons.thumb_up_alt), 27 onPressed: () => print('点赞'), 28 ), 29 //带图标的按钮,通过 icon 构造函数创建带图标的按钮 30 RaisedButton.icon( 31 icon: Icon(Icons.send), 32 label: Text("发送"), 33 onPressed: () => print('发送'), 34 ), 35 FlatButton.icon( 36 icon: Icon(Icons.live_help), 37 label: Text("疑问"), 38 onPressed: () => print('疑问'), 39 ) 40 ], 41 ), 42 )); 43 } 44} 45 46

有些按钮默认是由 icon 这个构造函数的,同个这个构造可以轻松创建出带图标的按钮,如 RaisedButton 等

自定义按钮外观

按钮的外观可以通过属性来定义,不同的按钮属性都大同小异,以 FlatButton 为例,看一下常用的按钮属性,详细的可以查看 api

1const FlatButton({ 2 ... 3 @required this.onPressed, //按钮点击回调 4 this.textColor, //按钮文字颜色 5 this.disabledTextColor, //按钮禁用时的文字颜色 6 this.color, //按钮背景颜色 7 this.disabledColor,//按钮禁用时的背景颜色 8 this.highlightColor, //按钮按下时的背景颜色 9 this.splashColor, //点击时,水波动画中水波的颜色 10 this.colorBrightness,//按钮主题,默认是浅色主题 11 this.padding, //按钮的填充 12 this.shape, //外形 13 @required this.child, //按钮的内容 14}) 15 16

栗子:定义一个提交按钮

1FlatButton( 2 color: Colors.blue, 3 child: Text("提交"), 4 splashColor: Colors.grey, 5 highlightColor: Colors.red, 6 shape: 7 RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), 8 onPressed: () => print('提交'), 9) 10 11

Flutter 中 没有提供去除背景的设置,如果需要去除背景,可通过将背景颜色设置为透明来实现,将 color: Colors.blue 替换为 color: Color(0x000000) 即可

FlatButton 是没有 阴影的,这样总会感觉差了点啥,如果需要阴影,可直接使用 RaisedButton 即可

1const RaisedButton({ 2 ... 3 this.elevation = 2.0, //正常状态下的阴影 4 this.highlightElevation = 8.0,//按下时的阴影 5 this.disabledElevation = 0.0,// 禁用时的阴影 6 ... 7} 8 9

通过设置以上属性即可设置阴影,elevation 这个属性会在很多组件中见到,都是用来控制阴影的

图片

在 Flutter 中,我们可以通过 Image 组件来加载并显示图片,Image 的加载源可能是 asset,文件,内存,以及网络

  • ImageProvider

ImageProvider 是一个抽象类,主要定义了图片获取的接口 load , 从不同的数据源获取图片需要实现不同的 ImageProvider,如 AssetImage 就是实现了 Asset 中加载图片的 ImageProvider,而 NetWorkImage s实现了从网络加载图片的 ImageProvider。

  • Image

Image widget 有一个必选的参数,对应一个 ImageProvider

  • 加载图片

加载图片等资源可参考此文章

1,加载 assets 中图片

1Image( 2 image: AssetImage("images/avatar.png"), 3 width: 100.0 4); 5Image.asset("images/avatar.png", 6 width: 100.0, 7) 8 9

2,加载网络图片

1Image( 2 image: NetworkImage( 3 "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"), 4 width: 100.0, 5) 6Image.network( 7 "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4", 8 width: 100.0, 9) 10 11

3,加载本地图片文件

1Image.file 2 3

4,加载内存图片

1Image.memory 2 3
  • 参数

1const Image({ 2 ... 3 this.width, //图片的宽 4 this.height, //图片高度 5 this.color, //图片的混合色值 6 this.colorBlendMode, //混合模式 7 this.fit,//缩放模式 8 this.alignment = Alignment.center, //对齐方式 9 this.repeat = ImageRepeat.noRepeat, //重复方式 10 ... 11}) 12 13
  • width ,height :设置图片的宽高,若不指定,图片图片会根据当前父容器的限制,尽可能的显示其原始大小,如果只设置了其中的一个,则另一个则会按比例缩放,但是可通过 fit 属性来适应规则

    • fit:用于在图片的显示空间和图片本身大小不同的时候指定图片的适应模式

BoxFit.fill 填充,忽略原来的宽高比,填满为止 BoxFit.contain 包含,不改变原有比例让容器包含整个图片,容器多余部分填充背景 BoxFit.cover 覆盖,不改变原有比例,让图片充满整个容器,图片多余部分裁剪 BoxFit.fitWidth 图片横向填充 BoxFit.fitHeight 图片纵向填充 BoxFit.none 无样式,原始大小居中,如果图片比显示空间大,则只会显示图片中间的部分 BoxFit.scaleDown 图片大小小于容器事相当于none,图片大小大于容器时缩小图片大小实现contain * color 和 colorBlendMode :在图片绘制时可以对每一个像素的颜色进行混合处理,color 指定混合色,colorBlenMode 指定混合模式

1Image( 2 image:NetworkImage("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.houpao.com%2Fdy%2Fdyrs%2F20200711%2F94fb713a5985aa0c0c6f29a20357880f.jpeg&refer=http%3A%2F%2Fimg.houpao.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1614828035&t=cf5430f8cc9a51b7000cde9c9cc30b5a"), 3 width: 500, 4 height: 300, 5 fit: BoxFit.cover, 6 color: Colors.red, 7 colorBlendMode: BlendMode.difference, 8) 9 10
1* repeat:当图片本身大小小于显示空间时,指定图片的重复规则 2
  • Image 缓存

Flutter 框架对加载获得图片是有缓存的(内存),默认最大缓存数量是 1000,最大缓存空间为 100M

  • 常用的图片组件

  • CircleAvatar

1CircleAvatar( 2backgroundImage:NetworkImage("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.houpao.com%2Fdy%2Fdyrs%2F20200711%2F94fb713a5985aa0c0c6f29a20357880f.jpeg&refer=http%3A%2F%2Fimg.houpao.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1614828035&t=cf5430f8cc9a51b7000cde9c9cc30b5a"), 3 radius: 50, 4), 5 6

圆形图片

1* FadeInImage 2
1FadeInImage( 2 image: NetworkImage("https://gimg2.baidu.com/i......."), 3 placeholder: AssetImage("images/icon.png"), 4) 5 6

如果直接从网络加载图片然后在显示会有些突兀,使用 FadeInImage 之后会在图片的加载过程中显示一个占位符,在图片加载完成之后显示淡入

ICON

在 Flutter 中,可以直接使用 字体图标,它是将图标做成字体文件,然后通过指定不同的字符而现实不同的图片

在字体文件中,每个字符都对应一个码,每个码对应一个显示字形,不同的字体就是指字形不同,及字符对应的字形是不同的。而在 iconfont 中,只是将位码对应的字形做成了图标,所以不同的字符最终就会渲染成不同的图标

在 Flutter 中,iconfont 和图片相比有如下优势

1,体积小

2,矢量的图标,放大不会影响清晰度

3,可以应用文本样式,可以像文本一样改变字体图标颜色,大小对齐等

4,可以通过 TextSpan 和文本混用

使用 Material Design 字体图标

Flutter 默认包含了一套 Material Design 的字体图标库,在 pubspec.yaml 文件中配置如下

1flutter: 2 uses-material-design: true 3 4

看一个简单栗子

1String icons = ""; 2icons += "\uE814"; 3icons += " \uE200"; 4icons += " \uE80D"; 5 6 Text( 7 icons, 8 style: TextStyle( 9 fontFamily: "MaterialIcons", fontSize: 40, color: Colors.green), 10 ) 11 12

image-20210204101921033

通过上面可以看到,使用图标就像使用文本一样,但这种需要提供每个图标的码点,这点对开发者并不友好,所以 Flutter 封装了 IconData 和 Icon 来专门显示字体图标,上面栗子可使用如下方式实现

1Row( 2 mainAxisAlignment: MainAxisAlignment.center, 3 children: [ 4 Icon(Icons.accessible, color: Colors.green), 5 Icon(Icons.error, color: Colors.green), 6 Icon(Icons.fingerprint, color: Colors.green) 7 ], 8) 9 10

使用自定义图标库

1,导入 ttf 文件

1fonts: 2 - family: myIcon #指定一个字体名 3 fonts: 4 - asset: fonts/iconfont.ttf 5 6

2,自定义 icon 类,功能和上面的 Icons 一样,将字体文件中的所有文件都定义为静态变量

1class MyIcons{ 2 static const IconData book = const IconData( 3 0xe614, 4 fontFamily: 'myIcon', 5 matchTextDirection: true 6 ); 7 static const IconData wechat = const IconData( 8 0xec7d, 9 fontFamily: 'myIcon', 10 matchTextDirection: true 11 ); 12} 13 14

3,使用

1Row( 2 mainAxisAlignment: MainAxisAlignment.center, 3 children: <Widget>[ 4 Icon(MyIcons.book,color: Colors.purple,), 5 Icon(MyIcons.wechat,color: Colors.green,), 6 ], 7) 8 9

单选按钮和复选框

Material 组件库中提供了单选开关 Switch 和 复选框 Checkbox,他们本身都是继承自 StatefulWidget ,他们本身不会保存当前选择状态,选中状态都是由父组件来管理的。

当 Switch 或者 CheckBox 被点击时,会触发 onChanged 回调,我们可以回调中改变逻辑

1class SwitchAndCheckboxTest extends StatefulWidget { 2 @override 3 State<StatefulWidget> createState() { 4 return _SwitchAndCheckboxTest(); 5 } 6} 7 8class _SwitchAndCheckboxTest extends State<SwitchAndCheckboxTest> { 9 bool _switchSelected = true; //单选状态 10 bool _checkboxSelected = true; //复选框状态 11 var groupValue = 0; //单选框,默认选中的值 12 @override 13 Widget build(BuildContext context) { 14 return Scaffold( 15 appBar: AppBar( 16 title: Text("单选开关和复选框"), 17 ), 18 body: Column( 19 children: [ 20 Switch( 21 value: _switchSelected, 22 activeColor: Colors.red, 23 onChanged: (value) { 24 setState(() { 25 _switchSelected = value; 26 }); 27 }, 28 ), 29 Checkbox( 30 value: _checkboxSelected, 31 activeColor: Colors.red, 32 onChanged: (value) => setState(() => _checkboxSelected = value), 33 ), 34 Row( 35 children: [ 36 Radio( 37 activeColor: Colors.red, 38 //此单选框绑定的值 39 value: 0, 40 //点击状态改变的回调 41 onChanged: (value) => setState(() => this.groupValue = value), 42 //当前组件中选定的值 43 groupValue: groupValue, 44 ), 45 Radio( 46 //此单选框绑定的值 47 activeColor: Colors.red, 48 value: 1, 49 //点击状态改变的回调 50 onChanged: (value) => setState(() => this.groupValue = value), 51 //当前组件中选定的值 52 groupValue: groupValue, 53 ), 54 Radio( 55 activeColor: Colors.red, 56 //此单选框绑定的值 57 value: 2, 58 //点击状态改变的回调 59 onChanged: (value) => setState(() => this.groupValue = value), 60 //当前组件中选定的值 61 groupValue: groupValue, 62 ) 63 ], 64 ) 65 ], 66 ), 67 ); 68 } 69} 70 71 72

image-20210204114608862

上面代码中,都行需要维护组件的状态,所以继承自 StatefulWidget ,在 build 中,构建了 checkBox 和 Switch 和 Radio,在点击的时候修改状态,然后重新构建 UI

属性

  • 共有属性 activeColor,设置激活状态的颜色
  • 宽高:Checkbox 无法自定义,Switch 只能定义宽度
  • Checkbox 有一个属性 tristate,表示是否为三态,默认值为false,如果为true 时,valude 的值会自动增加一个状态 null

总结

Switch , Checkbox 和 Radio 本身不会维护状态,而是需要父组件来管理状态,当用户点击时,通过事件将状态通知到父组件,因此是否选中就会和用户数据发生关联,而这些用户数据也不是他们的私有状态。因此,我们在自定义组件是应该思考一下那种方式最为合理

输入框和表单

Material 组件库中提供了输入框组件 TextField 和表单组件 From ,下面来具体看一下

TextField

用于文本输入,它提供了很多属性,首先简单看一下关键的属性作用

1const TextField({ 2 ... 3 TextEditingController controller, 4 FocusNode focusNode, 5 InputDecoration decoration = const InputDecoration(), 6 TextInputType keyboardType, 7 TextInputAction textInputAction, 8 TextStyle style, 9 TextAlign textAlign = TextAlign.start, 10 bool autofocus = false, 11 bool obscureText = false, 12 int maxLines = 1, 13 int maxLength, 14 bool maxLengthEnforced = true, 15 ValueChanged<String> onChanged, 16 VoidCallback onEditingComplete, 17 ValueChanged<String> onSubmitted, 18 List<TextInputFormatter> inputFormatters, 19 bool enabled, 20 this.cursorWidth = 2.0, 21 this.cursorRadius, 22 this.cursorColor, 23 ... 24}) 25 26
  • controller :编辑框的控制器,可以通过它设置/获取编辑框的内容,选择编辑框的内容,监听编辑框文本改变事件。大多数情况下我们都需要显示的提供一个 controller 来与文本框交互,如果没有提供,则 TextField 会自动创建一个

  • focusNode :用于控制 TextField 是否占有当前键盘输入的焦点、他是我们和键盘交互的一个句柄(handler)。

  • InputDecoration:用于控制 TextField 的外观显示,如提示文本,背景颜色,边框等

  • keyboardType :用于设置该输入框的键盘输入类型,取值如下:

text 文本输入键盘 multiline 多行文本,需和maxLines配合使用(设为null或大于1) number 数字;会弹出数字键盘 phone 优化后的电话号码输入键盘;会弹出数字键盘并显示“* #” datetime 优化后的日期输入键盘;Android上会显示“: -” emailAddress 优化后的电子邮件地址;会显示“@ .” url 优化后的url输入键盘; 会显示“/ .”

  • textInputAction :键盘动作按钮图标,他是一个枚举值,有多个可选值,具体的可查看 api

  • style:正在编辑的文本样式

  • textAlign:输入框内编辑文本在水平方向的对齐方式

  • autofocus:是否自动获取焦点.

  • obscureText :是否隐藏正在编辑的文本,如输入密码等。

  • maxLines :输入最大行数,默认为 1,如果为 null,则为无限制maxLength 和 maxLengthEnforced :前者代表输入文本的最大长度,设置后输入框右下角会显示输入的文本计数。后者决定输入长度超过 maxLength 后是否阻止

  • onChange:输入框内容改变的回调,也可通过 controller 来监听

  • onEditingComplete 和 onSubmitted :这两者都是在输入完成时触发,例如点击键盘的完成,或者搜索等。不同的是后者的回调是 ValueChanged<String> ,前者不接受参数

  • inputFormatters:用于指定输入格式,当输入内容改变时,会根据指定格式来校验

  • enable:若为 false,则输入框被禁用

  • cursorWidth ,cursorRadius 和 cursorColor:定义光标的宽度,圆角和颜色

栗子

1class InputText extends StatefulWidget { 2 @override 3 State<StatefulWidget> createState() { 4 return _InputText(); 5 } 6} 7 8class _InputText extends State<InputText> { 9 @override 10 Widget build(BuildContext context) { 11 return Scaffold( 12 appBar: AppBar( 13 title: Text("输入框"), 14 ), 15 body: Column( 16 children: [ 17 TextField( 18 autocorrect: true, 19 decoration: InputDecoration( 20 labelText: "用户名", 21 hintText: "用户名或邮箱", 22 prefixIcon: Icon(Icons.person)), 23 ), 24 TextField( 25 decoration: InputDecoration( 26 labelText: "密码", 27 hintText: "您的登录密码", 28 prefixIcon: Icon(Icons.lock)), 29 obscureText: true, 30 ) 31 ], 32 ), 33 ); 34 } 35} 36 37
  • 获取输入内容

1,定义两个变量,则 onChange 触发的时候保存即可

2,通过 controler 直接获取

1//定义一个controller 2TextEditingController _nameController = TextEditingController(); 3 4
1TextField( 2 autofocus: true, 3 controller: _nameController, //设置controller 4 ... 5) 6 7
1//直接输出即可 2print(_nameController.text) 3 4

controller 还可以用来设置默认值,选择文本等

1_nameController.text="hello world!"; 2_nameController.selection=TextSelection( 3 baseOffset: 2, 4 extentOffset: _nameController.text.length 5); 6 7TextField( 8 controller: _nameController, 9) 10 11
  • 控制焦点

焦点可以通过 FocusNode 和 FocusScopeNode 来控制,默认情况下,焦点由FocusScope来管理,它代表焦点控制范围,可以在这个范围内可以通过FocusScopeNode在输入框之间移动焦点、设置默认焦点等。我们可以通过FocusScope.of(context) 来获取Widget树中默认的FocusScopeNode。

  • 简单焦点状态改变事件

1// 创建 focusNode 2FocusNode focusNode = new FocusNode(); 3... 4// focusNode绑定输入框 5TextField(focusNode: focusNode); 6... 7// 监听焦点变化 8focusNode.addListener((){ 9 print(focusNode.hasFocus); 10}); 11 12
  • 自定义样式

  • 隐藏文本

1 TextField( 2 obscureText: true, 3 ) 4 5

隐藏后输入的内容将不可见,变成密码类型了

1* 键盘类型 2
1TextField( 2 keyboardType: TextInputType.number, 3), 4 5

例如,number 就只能输入数字,还有很多的值,如下,可以自行查看

1* 键盘按钮 2

即键盘右下角的按钮,常见的例如完成,是一个对号的按钮等

1* 大小写 2

控制英文字母的大小写,比如但是首字母大写等

1TextField( 2 textCapitalization: TextCapitalization.words, 3), 4 5

textCapitalization 的选项

1,words:单词首字母大写

2,sentences:句子首字母大写

3,characters:所有字母大写

4,none:默认无

1* 光标 2
1TextField( 2 cursorColor: Colors.orange,//颜色 3 cursorWidth: 15,//宽度 4 cursorRadius: Radius.circular(15),//圆角 5), 6 7
1* 计数器 2
1TextField( 2 maxLength: 11, 3), 4 5

设置最大长度计数器就可显示出来

自定义计数器/图标

1 TextField( 2 autocorrect: true, 3 maxLength: 11, 4 controller: _nameController, 5 decoration: InputDecoration( 6 labelText: "用户名", 7 hintText: "用户名或邮箱", 8 counter: Text("计数器 0/100"), 9 prefixIcon: Icon(Icons.person)), 10 ), 11 12

通过 counter 来实现 自定义计数器

并且通过 prefixIcon 可以设置左侧内图标,通过 icon 可设置左侧外图标

1decoration: InputDecoration( 2 suffixIcon: IconButton( 3 icon: Icon(Icons.close), 4 onPressed: () { 5 controller.clear(); 6 }, 7), 8 9

通过 suffixIcon 可以设置右侧内图标,并且可以设置点击事件

1* 错误文字提示 2
1TextField( 2 controller: controller, 3 decoration: InputDecoration( 4 errorText: "请输入内容", 5 ), 6), 7 8
1* 去除下划线 2
1decoration: InputDecoration.collapsed(hintText: "用户名或邮箱")), 2 3
1* 边框 2
1 decoration: InputDecoration( 2 border: OutlineInputBorder( 3 borderRadius: BorderRadius.all(Radius.circular(15)),//圆角 4 borderSide: BorderSide(color: Colors.red, width: 2.0)),//颜色,宽度 5 ), 6 7

颜色使用的是主题颜色,//TODO 这里设置的不生效,日后解决

表单 Form

在实际开发中,在请求接口之前会对输入框中的数据进行校验,如果对每个 TextField 都进行校验会非常麻烦,为此,Flutter 提供了一个 Form 组件,他可以对 输入框进行分组,然后统一进行一些操作,如内容校验,重置,保存等

Form 继承自 StatefulWidget 类,对应的状态为 FormState,定义如下:

1Form({ 2 @required Widget child, 3 bool autovalidate = false, 4 WillPopCallback onWillPop, 5 VoidCallback onChanged, 6}) 7 8
  • autovalidate:是否自动校验输入内容,当 为 true 时,每一个自 FormField 都会自动校验合法性,并直接显示错误信息。否则,需要通过 FormState.validate() 来手动校验
  • onWillPop:决定 Form 所在的路由是否可以直接返回(如点击返回按钮),该回调返回一个 Future 对象,若 Future 结果为 false,则当前路由不会返回,若为 true,则会返回到上一个路由,此属性通常用于拦截按钮
  • onChange:Form 的任意一个字 FormField 内容变化时都会触发此回调

FormField

Form 的子孙元素必须是 FormField 类型,FormField 是一个抽象类,有几个属性,FormState 通过他们来完成操作,FormField 部分定义如下:

1const FormField({ 2 ... 3 FormFieldSetter<T> onSaved, //保存回调 4 FormFieldValidator<T> validator, //验证回调 5 T initialValue, //初始值 6 bool autovalidate = false, //是否自动校验。 7}) 8 9

为了方便使用,Flutter 提供了一个 TextFormField 组件,他继承自 FormField 类,也是一个包装类,所以除了 FormField 之后,它还包括 TextField 的属性

FormState

FormState 为 Form 的 State 类,可以通过 Form.of() 或者 Globalkey 获得,我们可以通过他来对 Form 的子孙 FormField 进行统一的操作

  • FormState.validate():此方法会调用 Form 子孙 FormFile 的1 validate 回调,如果有一个校验失败,则返回 false,所有校验失败的都会返回错误提示
  • FormState.save():此方法会调用 Form 子孙 FormField 的 save 回调,用于保存表单内容
  • FormSata.reset():调用此方法后,会将子孙 FormField 的内容清空

栗子

1class InputText extends StatefulWidget { 2 @override 3 State<StatefulWidget> createState() { 4 return _InputText(); 5 } 6} 7 8class _InputText extends State<InputText> { 9 //定义一个controller 10 TextEditingController _nameController = TextEditingController(); 11 12 GlobalKey _formKey = new GlobalKey<FormState>(); 13 14 @override 15 void initState() { 16 super.initState(); 17 _nameController.addListener(() => print("账号:" + _nameController.text)); 18 } 19 20 @override 21 Widget build(BuildContext context) { 22 return Scaffold( 23 appBar: AppBar( 24 title: Text("输入框"), 25 ), 26 body: Padding( 27 padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), 28 child: Form( 29 key: _formKey,//设置globalKey,用于后面获取FormState 30 autovalidate: true,//开启自动校验 31 child: Column( 32 children: [ 33 TextFormField( 34 autocorrect: true, 35 maxLength: 11, 36 controller: _nameController, 37 decoration: InputDecoration.collapsed(hintText: "用户名或邮箱"), 38 validator: (v) { 39 return v.trim().length > 0 ? null : "用户名不能为空"; 40 }, 41 ), 42 TextFormField( 43 decoration: InputDecoration.collapsed(hintText: "您的登录密码"), 44 validator: (v) { 45 return v.trim().length > 5 ? null : "密码不能少于6位"; 46 }, 47 ), 48 Padding( 49 padding: const EdgeInsets.only(top: 28), 50 child: Row( 51 children: [ 52 Expanded( 53 child: RaisedButton( 54 padding: EdgeInsets.all(15), 55 child: Text("登录"), 56 color: Theme.of(context).primaryColor, 57 textColor: Colors.white, 58 onPressed: () { 59 //获取 formState ,调用 validate 验证合法性, 60 if ((_formKey.currentState as FormState) 61 .validate()) { 62 print('验证成功'); 63 } 64 }, 65 ), 66 ) 67 ], 68 ), 69 ) 70 ], 71 ), 72 ), 73 )); 74 } 75} 76 77

在登录按钮的 onPressed 方法中不能通过 Form.of(context ) 来获取,原因是,此处的 context 为 InputText 的 context**,而 Form.of(context) 是根据所指定 context 向根去查找,而 FormState 是在 InputText 的子树中,所以不行。**

正确的做法是通过 Builder 来构建登录按钮,Builder 会将 widget 节点的 context 作为回调参数:

1Expanded( 2 child: Builder(builder: (context) { 3 return RaisedButton( 4 padding: EdgeInsets.all(15), 5 child: Text("登录"), 6 color: Theme.of(context).primaryColor, 7 textColor: Colors.white, 8 onPressed: () { 9 //获取 formState ,调用 validate 验证合法性, 10 if ((Form.of(context)).validate()) { 11 print('验证成功'); 12 } 13 }, 14 ); 15 }), 16) 17 18

使用这种方式即可


参考自 Flutter 实战

代码交流 2021