跟Vue不同,跟React类似,单选框与复选框需要一个状态维护,改变时使用 setState 更新状态以重新渲染UI。

一、复选框与开关

  1. class SwitchAndCheckBoxTestRoute extends StatefulWidget {
  2. @override
  3. _SwitchAndCheckBoxTestRouteState createState() => new _SwitchAndCheckBoxTestRouteState();
  4. }
  5. class _SwitchAndCheckBoxTestRouteState extends State<SwitchAndCheckBoxTestRoute> {
  6. bool _switchSelected=true; // 维护单选开关状态
  7. bool _checkboxSelected=true; // 维护复选框状态
  8. @override
  9. Widget build(BuildContext context) {
  10. return Column(
  11. children: <Widget>[
  12. Switch(
  13. value: _switchSelected, //当前状态
  14. onChanged:(value){
  15. // 重新构建页面
  16. setState(() {
  17. _switchSelected=value;
  18. });
  19. },
  20. ),
  21. Checkbox(
  22. value: _checkboxSelected,
  23. activeColor: Colors.red, // 选中时的颜色
  24. onChanged: (value) {
  25. setState(() {
  26. _checkboxSelected=value;
  27. });
  28. },
  29. )
  30. ],
  31. );
  32. }
  33. }

二、滑块

  1. class HomePage extends StatefulWidget {
  2. @override
  3. createState() => new _HomePageState();
  4. }
  5. class _HomePageState extends State<HomePage> {
  6. double _sliderValue = 1;
  7. @override
  8. Widget build(BuildContext context) {
  9. return Scaffold(
  10. appBar: new AppBar(
  11. title: Text('首页'),
  12. ),
  13. body: Column(
  14. children: [
  15. Slider(
  16. value: _sliderValue,
  17. max: 10,
  18. min: 0,
  19. onChanged: (value) {
  20. setState(() {
  21. _sliderValue = value;
  22. });
  23. },
  24. )
  25. ]
  26. )
  27. );
  28. }
  29. }

三、单选框

Radio 的话, 只有点击 Radio 本身才能改变其 value, 点击 ListTile 的 title 无效

  1. class HomePage extends StatefulWidget {
  2. @override
  3. createState() => new _HomePageState();
  4. }
  5. enum SingingCharacter { lafayette, jefferson }
  6. class _HomePageState extends State<HomePage> {
  7. SingingCharacter _character = SingingCharacter.lafayette;
  8. @override
  9. Widget build(BuildContext context) {
  10. return Scaffold(
  11. appBar: new AppBar(
  12. title: Text('首页'),
  13. ),
  14. body: Column(
  15. children: [
  16. ListTile(
  17. title: const Text('Lafayette'),
  18. leading: Radio(
  19. value: SingingCharacter.lafayette,
  20. groupValue: _character,
  21. onChanged: (SingingCharacter value) {
  22. setState(() { _character = value; });
  23. },
  24. ),
  25. ),
  26. ListTile(
  27. title: const Text('Thomas Jefferson'),
  28. leading: Radio(
  29. value: SingingCharacter.jefferson,
  30. groupValue: _character,
  31. onChanged: (SingingCharacter value) {
  32. setState(() { _character = value; });
  33. },
  34. ),
  35. ),
  36. ]
  37. )
  38. );
  39. }
  40. }

RadioListTile

与 Radio 不同的是,RadioListTile 独占一行,点击时有水波效果

  1. class HomePage extends StatefulWidget {
  2. @override
  3. createState() => new _HomePageState();
  4. }
  5. enum SingingCharacter { lafayette, jefferson }
  6. class _HomePageState extends State<HomePage> {
  7. SingingCharacter _character = SingingCharacter.lafayette;
  8. @override
  9. Widget build(BuildContext context) {
  10. return Scaffold(
  11. appBar: new AppBar(
  12. title: Text('首页'),
  13. ),
  14. body: Column(
  15. children: <Widget>[
  16. RadioListTile<SingingCharacter>(
  17. title: const Text('Lafayette'),
  18. value: SingingCharacter.lafayette,
  19. groupValue: _character,
  20. onChanged: (SingingCharacter value) { setState(() { _character = value; }); },
  21. ),
  22. RadioListTile<SingingCharacter>(
  23. title: const Text('Thomas Jefferson'),
  24. value: SingingCharacter.jefferson,
  25. groupValue: _character,
  26. onChanged: (SingingCharacter value) { setState(() { _character = value; }); },
  27. ),
  28. ],
  29. )
  30. );
  31. }
  32. }

四、进度指示器

LinearProgressIndicator

LinearProgressIndicator 是一个线性、条状的进度条,定义如下:

  1. LinearProgressIndicator({
  2. double value,
  3. Color backgroundColor,
  4. Animation<Color> valueColor,
  5. ...
  6. })
  • value: value表示当前的进度,取值范围为[0,1];如果value为null时则指示器会执行一个循环动画(模糊进度);当value不为null时,指示器为一个具体进度的进度条。
  • backgroundColor: 指示器的背景色。
  • valueColor: 指示器的进度条颜色;值得注意的是,该值类型是 Animation<Color>,这允许我们对进度条的颜色也可以指定动画。如果我们不需要对进度条颜色执行动画,换言之,我们想对进度条应用一种固定的颜色,此时我们可以通过AlwaysStoppedAnimation来指定。

模糊进度条(会执行一个动画)

  1. LinearProgressIndicator(
  2. backgroundColor: Colors.grey[200],
  3. valueColor: AlwaysStoppedAnimation(Colors.blue),
  4. );

进度条显示50%, 常用于监控上传等情景 (动态传入value)

  1. LinearProgressIndicator(
  2. backgroundColor: Colors.grey[200],
  3. valueColor: AlwaysStoppedAnimation(Colors.blue),
  4. value: .5,
  5. );

示例:以按钮点击模拟上传:

  1. class Progress extends StatefulWidget {
  2. @override
  3. createState() => new _ProgressState();
  4. }
  5. class _ProgressState extends State<Progress> {
  6. var v = 0.0;
  7. @override
  8. Widget build(BuildContext context) {
  9. return Column(
  10. children: <Widget>[
  11. RaisedButton(
  12. child: Text("normal"),
  13. onPressed: () {
  14. setState(() {
  15. if (v < 1.0) {
  16. v += 0.1;
  17. }
  18. });
  19. },
  20. ),
  21. LinearProgressIndicator(
  22. backgroundColor: Colors.grey[200],
  23. valueColor: AlwaysStoppedAnimation(Colors.blue),
  24. value: v,
  25. )
  26. ],
  27. );
  28. }
  29. }

示例:进度条变色加载

  1. class ProgressRoute extends StatefulWidget {
  2. @override
  3. _ProgressRouteState createState() => _ProgressRouteState();
  4. }
  5. class _ProgressRouteState extends State<ProgressRoute>
  6. with SingleTickerProviderStateMixin {
  7. AnimationController _animationController;
  8. @override
  9. void initState() {
  10. // 动画执行时间3秒
  11. _animationController =
  12. new AnimationController(vsync: this, duration: Duration(seconds: 3));
  13. _animationController.forward();
  14. _animationController.addListener(() => setState(() => {}));
  15. super.initState();
  16. }
  17. @override
  18. void dispose() {
  19. _animationController.dispose();
  20. super.dispose();
  21. }
  22. @override
  23. Widget build(BuildContext context) {
  24. return SingleChildScrollView(
  25. child: Column(
  26. children: <Widget>[
  27. Padding(
  28. padding: EdgeInsets.all(16),
  29. child: LinearProgressIndicator(
  30. backgroundColor: Colors.grey[200],
  31. valueColor: ColorTween(begin: Colors.grey, end: Colors.blue)
  32. .animate(_animationController), // 从灰色变成蓝色
  33. value: _animationController.value,
  34. ),
  35. )
  36. ],
  37. ),
  38. );
  39. }
  40. }

CircularProgressIndicator

CircularProgressIndicator 是一个圆形进度条,定义如下:

  1. CircularProgressIndicator({
  2. double value,
  3. Color backgroundColor,
  4. Animation<Color> valueColor,
  5. this.strokeWidth = 4.0, // 圆形进度条的粗细
  6. ...
  7. })

自定义尺寸

我们可以发现LinearProgressIndicator和CircularProgressIndicator,并没有提供设置圆形进度条尺寸的参数;如果我们希望LinearProgressIndicator的现细一些,或者希望CircularProgressIndicator的圆大一些该怎么做?

其实LinearProgressIndicator和CircularProgressIndicator都是取父容器的尺寸作为绘制的边界的。知道了这点,我们便可以通过尺寸限制类Widget,如ConstrainedBox、SizedBox (我们将在后面容器类组件一章中介绍)来指定尺寸,如:

  1. // 线性进度条高度指定为3
  2. SizedBox(
  3. height: 3,
  4. child: LinearProgressIndicator(
  5. backgroundColor: Colors.grey[200],
  6. valueColor: AlwaysStoppedAnimation(Colors.blue),
  7. value: .5,
  8. ),
  9. ),
  10. // 圆形进度条直径指定为100
  11. SizedBox(
  12. height: 100,
  13. width: 100,
  14. child: CircularProgressIndicator(
  15. backgroundColor: Colors.grey[200],
  16. valueColor: AlwaysStoppedAnimation(Colors.blue),
  17. value: .7,
  18. ),
  19. ),

其他内置的进度指示器

第三方进度指示器

五、时间日期选择器

showDatePicker

六、输入框

TextField

TextField 是一个可以输入值的组件

定义:

  1. TextField({
  2. Key key,
  3. this.decoration = const InputDecoration(), // 文本框样式, InputDecoration
  4. this.controller, // 控制器, TextEditingController
  5. this.focusNode, // 焦点控制, FocusNode
  6. this.textAlign = TextAlign.start, // 对齐
  7. TextInputType keyboardType, // 键盘类型, TextInputType
  8. this.textCapitalization = TextCapitalization.none, // 首字母大写
  9. this.textInputAction, // 更改键盘本身的操作按钮, TextInputAction
  10. this.autofocus = false, // 是否自动获取焦点
  11. this.enabled, // 是否可用
  12. this.readOnly = false,
  13. this.obscureText = false,
  14. this.autocorrect = true,
  15. this.expands = false,
  16. this.minLines, // 文本最小行数
  17. this.maxLines = 1, // 文本最大行数
  18. this.maxLength, // 最大长度
  19. this.maxLengthEnforced = true,
  20. this.showCursor,
  21. this.cursorWidth = 2.0, // 光标宽度
  22. this.cursorRadius, // 光标圆角, Radius, 比如 Radius.circular(10.0)
  23. this.cursorColor, // 光标颜色, Color
  24. this.onChanged, // 文本改变事件, 接收一个参数为当前输入值 void Function(String)
  25. this.onTap,
  26. this.onEditingComplete,
  27. this.onSubmitted,
  28. this.textAlignVertical,
  29. this.textDirection,
  30. this.style,
  31. this.strutStyle,
  32. this.inputFormatters,
  33. this.keyboardAppearance,
  34. this.scrollPadding = const EdgeInsets.all(20.0),
  35. this.dragStartBehavior = DragStartBehavior.start,
  36. this.enableInteractiveSelection,
  37. this.buildCounter,
  38. this.scrollController,
  39. this.scrollPhysics,
  40. })

示例:

  1. TextField(
  2. decoration: InputDecoration(
  3. border: OutlineInputBorder(borderSide: BorderSide.none),
  4. labelText: "验证码",
  5. hintText: "请输入验证码",
  6. icon: Icon(Icons.lock),
  7. filled: true,
  8. fillColor: Color.fromRGBO(255,0,0,.1),
  9. ),
  10. )

TextFormField

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

创建一个输入框和 TextField 的使用没有任何区别,唯一多出来的属性时 validator,接收一个方法,并且方法的参数中会传入 value 当前表单的值。

示例:

  1. TextFormField(
  2. controller: _priceController,
  3. keyboardType: TextInputType.number,
  4. decoration: InputDecoration(
  5. labelText: "套餐价格",
  6. hintText: "请输入套餐价格"
  7. ),
  8. ),

键盘类型

TextInputType 可以指定键盘类型, 常见的有:

  • TextInputType.text 普通文本
  • TextInputType.number 数字
  • TextInputType.phone 电话号码
  • TextInputType.emailAddress 电子邮件
  • TextInputType.datetime 日期时间
  • TextInputType.url URL

确定键

TextInputAction 可以更改键盘本身的操作按钮

常用的:

  • TextInputAction.search
  • TextInputAction.go
  • TextInputAction.next
  • TextInputAction.done

文本框样式

InputDecoration 用于指定文本框样式, InputDecoration 包括如下常用参数:

—- 图标 —-

  • icon 输入框图标
  • prefixIcon 输入框内侧左面的控件
  • suffixIcon 输入框内侧右面的图标

—- 文本 —-

  • labelText 输入框标题文本
  • hintText 相当于html的placeholder
  • helperText 帮助文字

—- 填充 —-

  • filled 是否启用填充色
  • fillColor 填充色

—- 前缀后缀 —-

  • prefix 前缀组件, Widget, 不能跟 prefixText 共存
  • prefixText 前缀文字, String, 不能跟 prefix 共存
  • suffix 后缀组件, Widget, 不能跟 suffixText 共存
  • suffixText 后缀文字, String, 不能跟 suffix 共存

—- 文字样式 —-

  • helperStyle 帮助文字样式
  • hintStyle 提示文字样式
  • labelStyle 标签文字样式
  • counterStyle 计数器文字样式
  • prefixStyle 前缀文字样式
  • suffixStyle 后缀文字样式

—- 边框 —-

  • border 普通边线
    • InputBorder.none 无边框 (或 OutlineInputBorder(borderSide: BorderSide.none))
    • UnderlineInputBorder() 下划线边框
    • OutlineInputBorder() 四周带边框
  • focusedBorder 聚焦时的外边线
  • focusedErrorBorder 聚焦时的错误边线

focusedBorder 和 focusedErrorBorder 的属性值同 border

下划线边框

  1. UnderlineInputBorder(
  2. borderSide: BorderSide(
  3. color: Colors.green, //边框颜色为绿色
  4. width: 5, //宽度为5
  5. )),

四周带边框

  1. OutlineInputBorder(
  2. borderRadius: BorderRadius.all(Radius.circular(10)), // 设置边框圆角
  3. borderSide: BorderSide(
  4. color: Colors.green, // 边框颜色为绿色
  5. width: 10, // 宽度为10
  6. ))

InputDecoration示例

  1. TextField(
  2. maxLength: 10,
  3. decoration: InputDecoration(
  4. prefixIcon: Icon(Icons.accessible),
  5. suffixIcon: Icon(Icons.ac_unit),
  6. icon: Icon(Icons.access_time),
  7. hintText: 'hintText',
  8. labelText: 'labelText',
  9. helperText: 'helperText',
  10. border: UnderlineInputBorder(),
  11. filled: true,
  12. fillColor: Colors.green.withAlpha(100),
  13. prefix: Text('prefix '),
  14. suffixText: 'suffixText',
  15. helperStyle: TextStyle(color: Colors.purple),
  16. hintStyle: TextStyle(color: Colors.white),
  17. labelStyle: TextStyle(color: Colors.indigo),
  18. counterStyle: TextStyle(color: Colors.cyan),
  19. prefixStyle: TextStyle(color: Colors.teal),
  20. suffixStyle: TextStyle(color: Colors.deepOrange),
  21. ),
  22. )

效果:
008.png

自定义样式
我们可以不通过 decoration 属性实现样式, 可以使用类似于Web中的技巧, 在TextField外部包一层, 本身不实现任何样式, 而是控制外层容器的样式:

  1. Container(
  2. padding: const EdgeInsets.all(8.0),
  3. alignment: Alignment.center,
  4. height: 60.0,
  5. decoration: new BoxDecoration(
  6. color: Colors.green.withAlpha(100),
  7. border: new Border.all(color: Colors.grey, width: 4.0), // 这里控制边框粗细
  8. borderRadius: new BorderRadius.circular(12.0)),
  9. child: new TextFormField(
  10. decoration: InputDecoration.collapsed(hintText: 'hello'),
  11. ),
  12. )

效果:
009.png

FocusNode

FocusNode这个类用来通知控件获取/失去焦点, 可以对它实行焦点获取/失去的监听:

  1. class TestPage extends StatefulWidget {
  2. @override
  3. _TestPageState createState() => new _TestPageState();
  4. }
  5. class _TestPageState extends State<TestPage> {
  6. FocusNode _focusNode = new FocusNode(); // 初始化一个FocusNode控件
  7. @override
  8. void initState(){
  9. super.initState();
  10. _focusNode.addListener(_focusNodeListener); // 初始化一个listener
  11. }
  12. @override
  13. void dispose(){
  14. _focusNode.removeListener(_focusNodeListener); // 页面消失时必须取消这个listener!!
  15. super.dispose();
  16. }
  17. Future<Null> _focusNodeListener() async { // 用async的方式实现这个listener
  18. if (_focusNode.hasFocus){
  19. print('TextField got the focus');
  20. } else {
  21. print('TextField lost the focus');
  22. }
  23. }
  24. @override
  25. Widget build(BuildContext context) {
  26. return new Scaffold(
  27. appBar: new AppBar(
  28. title: new Text('Test'),
  29. ),
  30. body: new SafeArea(
  31. top: false,
  32. bottom: false,
  33. child: new Form(
  34. child: new Column(
  35. children: <Widget> [
  36. new TextFormField(
  37. focusNode: _focusNode, // 将listener和TextFormField绑定
  38. decoration: InputDecoration(
  39. labelText: "第一个输入框",
  40. hintText: "第一个输入框"
  41. ),
  42. ),
  43. TextFormField(
  44. keyboardType: TextInputType.number,
  45. decoration: InputDecoration(
  46. labelText: "第二个输入框",
  47. hintText: "第二个输入框"
  48. ),
  49. ),
  50. ],
  51. ),
  52. ),
  53. ),
  54. );
  55. }
  56. }

焦点控制

有的时候我们需要执行某些操作时自动获取文本框焦点或使其失去焦点, 只需要使用 TextField 的 focusNode 参数即可:

  1. FocusNode _codeFocus = FocusNode();
  2. ...
  3. TextFormField(
  4. focusNode: _codeFocus,
  5. decoration: InputDecoration(
  6. border: OutlineInputBorder(borderSide: BorderSide.none),
  7. labelText: "验证码",
  8. hintText: "请输入验证码",
  9. icon: Icon(Icons.lock)),
  10. ),

获取焦点:

  1. FocusScope.of(context).requestFocus(_codeFocus);

失去焦点:

  1. _codeFocus.unfocus();

在多个输入框内切换焦点

下面介绍一下onEditingComplete这个事件。

当用户提交可编辑内容时调用(例如,用户按下键盘上的“done”按钮)。

onEditingComplete的默认实现根据情况执行2种不同的行为:

  • 当完成操作被按下时,例如“done”、“go”、“send”或“search”,用户的内容被提交给[controller],然后焦点被放弃。
  • 当按下一个未完成操作(如“next”或“previous”)时,用户的内容被提交给[controller],但不会放弃焦点,因为开发人员可能希望立即将焦点转移到[onsubmit]中的另一个输入小部件。

示例:

  1. FocusNode secondTextFieldNode = FocusNode();
  2. Column(
  3. children: <Widget>[
  4. TextFormField(
  5. onEditingComplete: () {
  6. FocusScope.of(context).requestFocus(secondTextFieldNode);
  7. }
  8. ),
  9. TextField(
  10. focusNode: secondTextFieldNode,
  11. ),
  12. ],
  13. )

输入控制器

TextEditingController 用于控制文本输入, 通常用于获取文本框的输入值

  1. class TestPage extends StatefulWidget {
  2. @override
  3. State<StatefulWidget> createState() {
  4. return _TestPageState();
  5. }
  6. }
  7. class _TestPageState extends State<TestPage> {
  8. final _controller = TextEditingController(); // 定义控制器
  9. void initState() {
  10. _controller.addListener(() {
  11. final text = _controller.text.toLowerCase();
  12. _controller.value = _controller.value.copyWith(
  13. text: text,
  14. selection: TextSelection(baseOffset: text.length, extentOffset: text.length),
  15. composing: TextRange.empty,
  16. );
  17. });
  18. super.initState();
  19. }
  20. void dispose() {
  21. _controller.dispose();
  22. super.dispose();
  23. }
  24. Widget build(BuildContext context) {
  25. return Scaffold(
  26. appBar: new AppBar(
  27. title: new Text('test'),
  28. leading: IconButton(icon: Icon(Icons.dashboard), onPressed: () {}),
  29. actions: <Widget>[ //导航栏右侧菜单
  30. IconButton(icon: Icon(Icons.share), onPressed: () {}),
  31. ],
  32. ),
  33. body: Column(
  34. children: <Widget>[
  35. Container(
  36. alignment: Alignment.center,
  37. padding: const EdgeInsets.all(6),
  38. child: TextFormField(
  39. controller: _controller, // 绑定控制器
  40. decoration: InputDecoration(border: OutlineInputBorder()),
  41. ),
  42. ),
  43. RaisedButton(
  44. onPressed: () {
  45. var text = _controller.text; // 获取输入的值
  46. print(text);
  47. },
  48. child: Text('获取数据'),
  49. )
  50. ],
  51. )
  52. );
  53. }
  54. }

解决输入框被键盘遮挡的问题

原理不说了, 看封装好的组件:

  1. /**
  2. * 作者:Created by H on 2019/1/23 11:08.
  3. * 介绍: 解决输入框被遮挡问题
  4. */
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/rendering.dart';
  7. ///
  8. /// Helper class that ensures a Widget is visible when it has the focus
  9. /// For example, for a TextFormField when the keyboard is displayed
  10. ///
  11. /// How to use it:
  12. ///
  13. /// In the class that implements the Form,
  14. /// Instantiate a FocusNode
  15. /// FocusNode _focusNode = new FocusNode();
  16. ///
  17. /// In the build(BuildContext context), wrap the TextFormField as follows:
  18. ///
  19. /// new EnsureVisibleWhenFocused(
  20. /// focusNode: _focusNode,
  21. /// child: new TextFormField(
  22. /// ...
  23. /// focusNode: _focusNode,
  24. /// ),
  25. /// ),
  26. ///
  27. /// Initial source code written by Collin Jackson.
  28. /// Extended (see highlighting) to cover the case when the keyboard is dismissed and the
  29. /// user clicks the TextFormField/TextField which still has the focus.
  30. ///
  31. class EnsureVisibleWhenFocused extends StatefulWidget {
  32. const EnsureVisibleWhenFocused({
  33. Key key,
  34. @required this.child,
  35. @required this.focusNode,
  36. this.curve: Curves.ease,
  37. this.duration: const Duration(milliseconds: 100),
  38. }) : super(key: key);
  39. /// The node we will monitor to determine if the child is focused
  40. final FocusNode focusNode;
  41. /// The child widget that we are wrapping
  42. final Widget child;
  43. /// The curve we will use to scroll ourselves into view.
  44. ///
  45. /// Defaults to Curves.ease.
  46. final Curve curve;
  47. /// The duration we will use to scroll ourselves into view
  48. ///
  49. /// Defaults to 100 milliseconds.
  50. final Duration duration;
  51. @override
  52. _EnsureVisibleWhenFocusedState createState() => new _EnsureVisibleWhenFocusedState();
  53. }
  54. ///
  55. /// We implement the WidgetsBindingObserver to be notified of any change to the window metrics
  56. ///
  57. class _EnsureVisibleWhenFocusedState extends State<EnsureVisibleWhenFocused> with WidgetsBindingObserver {
  58. @override
  59. void initState(){
  60. super.initState();
  61. widget.focusNode.addListener(_ensureVisible);
  62. WidgetsBinding.instance.addObserver(this);
  63. }
  64. @override
  65. void dispose(){
  66. WidgetsBinding.instance.removeObserver(this);
  67. widget.focusNode.removeListener(_ensureVisible);
  68. super.dispose();
  69. }
  70. ///
  71. /// This routine is invoked when the window metrics have changed.
  72. /// This happens when the keyboard is open or dismissed, among others.
  73. /// It is the opportunity to check if the field has the focus
  74. /// and to ensure it is fully visible in the viewport when
  75. /// the keyboard is displayed
  76. ///
  77. @override
  78. void didChangeMetrics(){
  79. if (widget.focusNode.hasFocus){
  80. _ensureVisible();
  81. }
  82. }
  83. ///
  84. /// This routine waits for the keyboard to come into view.
  85. /// In order to prevent some issues if the Widget is dismissed in the
  86. /// middle of the loop, we need to check the "mounted" property
  87. ///
  88. /// This method was suggested by Peter Yuen (see discussion).
  89. ///
  90. Future<Null> _keyboardToggled() async {
  91. if (mounted){
  92. EdgeInsets edgeInsets = MediaQuery.of(context).viewInsets;
  93. while (mounted && MediaQuery.of(context).viewInsets == edgeInsets) {
  94. await new Future.delayed(const Duration(milliseconds: 10));
  95. }
  96. }
  97. return;
  98. }
  99. Future<Null> _ensureVisible() async {
  100. // Wait for the keyboard to come into view
  101. await Future.any([new Future.delayed(const Duration(milliseconds: 300)), _keyboardToggled()]);
  102. // No need to go any further if the node has not the focus
  103. if (!widget.focusNode.hasFocus){
  104. return;
  105. }
  106. // Find the object which has the focus
  107. final RenderObject object = context.findRenderObject();
  108. final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
  109. assert(viewport != null);
  110. // Get the Scrollable state (in order to retrieve its offset)
  111. ScrollableState scrollableState = Scrollable.of(context);
  112. assert(scrollableState != null);
  113. // Get its offset
  114. ScrollPosition position = scrollableState.position;
  115. double alignment;
  116. if (position.pixels > viewport.getOffsetToReveal(object, 0.0).offset) {
  117. // Move down to the top of the viewport
  118. alignment = 0.0;
  119. } else if (position.pixels < viewport.getOffsetToReveal(object, 1.0).offset){
  120. // Move up to the bottom of the viewport
  121. alignment = 1.0;
  122. } else {
  123. // No scrolling is necessary to reveal the child
  124. return;
  125. }
  126. position.ensureVisible(
  127. object,
  128. alignment: alignment,
  129. duration: widget.duration,
  130. curve: widget.curve,
  131. );
  132. }
  133. @override
  134. Widget build(BuildContext context) {
  135. return widget.child;
  136. }
  137. }

使用方式:

  1. import 'package:flutter/material.dart';
  2. import './widgets/EnsureVisibleWhenFocused.dart';
  3. void main() {
  4. runApp(new StartApp());
  5. }
  6. class StartApp extends StatefulWidget {
  7. @override
  8. State<StatefulWidget> createState() {
  9. return new _StartAppState();
  10. }
  11. }
  12. class _StartAppState extends State<StartApp> {
  13. @override
  14. Widget build(BuildContext context) {
  15. return MaterialApp(
  16. title: '首页',
  17. theme: new ThemeData(
  18. primarySwatch: Colors.blue,
  19. ),
  20. home: TestPage(),
  21. );
  22. }
  23. }
  24. class TestPage extends StatefulWidget {
  25. @override
  26. State<StatefulWidget> createState() {
  27. return _TestPageState();
  28. }
  29. }
  30. class _TestPageState extends State<TestPage> {
  31. FocusNode _focusNode = new FocusNode();
  32. Widget build(BuildContext context) {
  33. return Scaffold(
  34. appBar: new AppBar(
  35. title: new Text('test'),
  36. leading: IconButton(icon: Icon(Icons.dashboard), onPressed: () {}),
  37. actions: <Widget>[
  38. IconButton(icon: Icon(Icons.share), onPressed: () {}),
  39. ],
  40. ),
  41. body: SingleChildScrollView( // 注意外层一定得有一个可滑动的组件
  42. child: Padding(
  43. padding: EdgeInsets.only(top: 300.0, left: 50.0, right: 50.0),
  44. child: new EnsureVisibleWhenFocused( // 这里
  45. focusNode: _focusNode,
  46. child: new TextFormField(),
  47. ),
  48. ),
  49. ));
  50. }
  51. }

效果:
010.gif

七、表单

Form

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

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

FormField

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

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

validator

validator 校验逻辑
如果不通过,直接可以返回提示的文案,如果通过,则返回 null 即可

调用数据校验
调用数据校验的时机一般放在按钮提交点击的时候,这个时候 _formKey 就派上用处了

  1. if ((_formKey.currentState as FormState).validate()) {
  2. Scaffold.of(context).showSnackBar(SnackBar(
  3. content: Text('提交成功...'),
  4. ));
  5. }

FormState

FormState 为Form的State类,可以通过Form.of()或GlobalKey获得。我们可以通过它来对Form的子孙FormField进行统一操作。我们看看其常用的三个方法:

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

示例: 登录页面

  1. class FormTestRoute extends StatefulWidget {
  2. @override
  3. _FormTestRouteState createState() => new _FormTestRouteState();
  4. }
  5. class _FormTestRouteState extends State<FormTestRoute> {
  6. TextEditingController _unameController = new TextEditingController();
  7. TextEditingController _pwdController = new TextEditingController();
  8. GlobalKey _formKey= new GlobalKey<FormState>();
  9. @override
  10. Widget build(BuildContext context) {
  11. return Scaffold(
  12. appBar: AppBar(
  13. title:Text("Form Test"),
  14. ),
  15. body: Padding(
  16. padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
  17. child: Form(
  18. key: _formKey, // 设置globalKey,用于后面获取FormState
  19. autovalidate: true, // 开启自动校验
  20. child: Column(
  21. children: <Widget>[
  22. TextFormField(
  23. autofocus: true,
  24. controller: _unameController,
  25. decoration: InputDecoration(
  26. labelText: "用户名",
  27. hintText: "用户名或邮箱",
  28. icon: Icon(Icons.person)
  29. ),
  30. // 校验用户名
  31. validator: (v) {
  32. return v.trim().length > 0 ? null : "用户名不能为空";
  33. }
  34. ),
  35. TextFormField(
  36. controller: _pwdController,
  37. decoration: InputDecoration(
  38. labelText: "密码",
  39. hintText: "您的登录密码",
  40. icon: Icon(Icons.lock)
  41. ),
  42. obscureText: true,
  43. // 校验密码
  44. validator: (v) {
  45. return v.trim().length > 5 ? null : "密码不能少于6位";
  46. }
  47. ),
  48. // 登录按钮
  49. Padding(
  50. padding: const EdgeInsets.only(top: 28.0),
  51. child: Row(
  52. children: <Widget>[
  53. Expanded(
  54. child: RaisedButton(
  55. padding: EdgeInsets.all(15.0),
  56. child: Text("登录"),
  57. color: Theme.of(context).primaryColor,
  58. textColor: Colors.white,
  59. onPressed: () {
  60. // 在这里不能通过此方式获取FormState,context不对
  61. // print(Form.of(context));
  62. // 通过_formKey.currentState 获取FormState后,调用validate()方法校验用户名密码是否合法,校验通过后再提交数据。
  63. if((_formKey.currentState as FormState).validate()){
  64. // 验证通过提交数据
  65. print(_unameController.text);
  66. print(_pwdController.text);
  67. }
  68. },
  69. ),
  70. ),
  71. ],
  72. ),
  73. )
  74. ],
  75. ),
  76. ),
  77. ),
  78. );
  79. }
  80. }

登录按钮的onPressed方法中不能通过 Form.of(context) 来获取,原因是,此处的 contextFormTestRoutecontext,而 Form.of(context) 是根据所指定 context 向根去查找,而 FormState 是在 FormTestRoute 的子树中,所以不行。正确的做法是通过 Builder 来构建登录按钮,Builder会将widget节点的context作为回调参数:

  1. Expanded(
  2. // 通过Builder来获取RaisedButton所在widget树的真正context(Element)
  3. child:Builder(builder: (context){
  4. return RaisedButton(
  5. ...
  6. onPressed: () {
  7. // 由于本widget也是Form的子代widget,所以可以通过下面方式获取FormState
  8. if(Form.of(context).validate()){
  9. // 验证通过提交数据
  10. print(_unameController.text);
  11. print(_pwdController.text);
  12. }
  13. },
  14. );
  15. })
  16. )

参考资料