Flutter学习笔记08-输入框及表单

Material组件库中提供了输入框组件TextField和表单组件Form。

1.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是否占有当前键盘的输入焦点
  • decoration
    用于控制TextField的外观显示,如提示文本、背景颜色、边框等。
  • keyboardType
    用于设置该输入框默认的键盘输入类型,它是一个枚举值,有多个可选值。可自行查阅文档
  • textInputAction
    键盘动作按钮图标(即回车键位图标),它是一个枚举值,有多个可选值。可自行查阅文档
  • style
    正在编辑的文本样式。
  • textAlign
    入框内编辑文本在水平方向的对齐方式。
  • autofocus
    是否自动获取焦点。
  • obscureText
    是否隐藏正在编辑的文本,文本内容会用“•”替换。
  • maxLines
    输入框的最大行数,默认为1;如果为null,则无行数限制。
  • maxLength
    代表输入框文本的最大长度,设置后输入框右下角会显示输入的文本计数。
  • maxLengthEnforced
    决定当输入文本长度超过maxLength时是否阻止输入,为true时会阻止输入,为false时不会阻止输入但输入框会变红。
  • onChange
    输入框内容改变时的回调函数。
    ps:内容改变事件也可以通过controller来监听。
  • onEditingComplete和onSubmitted
    这两个回调都是在输入框输入完成时触发,比如按了键盘的完成键。不同的是两个回调签名不同,onSubmitted回调是ValueChanged<String>类型,它接收当前输入内容做为参数,而onEditingComplete不接收参数。
  • inputFormatters
    用于指定输入格式;当用户输入内容改变时,会根据指定的格式来校验。
  • enable
    如果为false,则输入框会被禁用,禁用状态不接收输入和事件,同时显示禁用态样式(在其decoration中定义)。
  • cursorWidth、cursorRadius和cursorColor
    这三个属性是用于自定义输入框光标宽度、圆角和颜色。

代码示例:

class TextFieldDemo extends StatefulWidget {
  @override
  _TextFieldDemoState createState() => _TextFieldDemoState();
}

class _TextFieldDemoState extends State<TextFieldDemo> {

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        TextField(
          focusNode: myFocusNode,
          decoration: InputDecoration(
            labelText: '用户名',
            hintText: '用户名或邮箱',
            prefixIcon: Icon(Icons.person),
          ),
          onSubmitted: (v) {
            print('onSubmitted: $v');
          },
          onEditingComplete: () {
            print('onEditingComplete');
          },
        ),
        TextField(
          decoration: InputDecoration(
            labelText: '密码',
            hintText: '您的登录密码',
            prefixIcon: Icon(Icons.lock),
          ),
          obscureText: true,
          onChanged: (v) {
            print('onChange: $v');
          },
        ),
      ],
    );
  }
}

运行效果图如下:

ps:当设置autofocus为true,运行项目控制台可能输出错误日志如下:

flutter: ══╡ EXCEPTION CAUGHT BY FOUNDATION LIBRARY ╞════════════════════════════════════════════════════════
flutter: The following assertion was thrown while dispatching notifications for FocusNode:
flutter: RenderBox was not laid out: RenderEditable#f6818 NEEDS-LAYOUT NEEDS-PAINT
flutter: 'package:flutter/src/rendering/box.dart':
flutter: Failed assertion: line 1687 pos 12: 'hasSize'
flutter:
flutter: Either the assertion indicates an error in the framework itself, or we should provide substantially
flutter: more information in this error message to help you determine and fix the underlying cause.
flutter: In either case, please report this assertion by filing a bug on GitHub:
flutter:   https://github.com/flutter/flutter/issues/new?template=BUG.md
flutter:
flutter: When the exception was thrown, this was the stack:
flutter: #2      RenderBox.size (package:flutter/src/rendering/box.dart:1687:12)
flutter: #3      EditableTextState._updateSizeAndTransform (package:flutter/src/widgets/editable_text.dart:1729:40)
flutter: #4      EditableTextState._openInputConnection (package:flutter/src/widgets/editable_text.dart:1415:7)
flutter: #5      EditableTextState._openOrCloseInputConnectionIfNeeded (package:flutter/src/widgets/editable_text.dart:1441:7)
flutter: #6      EditableTextState._handleFocusChanged (package:flutter/src/widgets/editable_text.dart:1707:5)
flutter: #7      ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:206:21)
flutter: #8      FocusNode._notify (package:flutter/src/widgets/focus_manager.dart:808:5)
flutter: #9      FocusManager._applyFocusChange (package:flutter/src/widgets/focus_manager.dart:1401:12)
flutter: (elided 12 frames from class _AssertionError and package dart:async)
flutter:
flutter: The FocusNode sending notification was:
flutter:   FocusNode#143b7

发生报错的原因是当autofocus自动获取焦点键盘第一次弹出时,textField还未创建成功,解决方案可查看此处

TextField的controller

可以给TextField添加一个控制器(Controller),可以使用它设置文本的初始值,也可以使用它来监听文本的改变。如果我们没有为TextField提供一个Controller,那么会Flutter会默认创建一个TextEditingController的。源码如下:

  @override
  void initState() {
    super.initState();
    ...
    if (widget.controller == null)
      _controller = TextEditingController();
  }

代码示例如下:

class _TextFieldDemoState extends State<TextFieldDemo> {
  TextEditingController _textEditingController = TextEditingController();

  @override
  void initState() {
    super.initState();
    // 设置默认值
    _textEditingController.text = 'Hello Flutter!';
    // 文本监听
    _textEditingController.addListener(() {
      print('textEditingController:${_textEditingController.text}');
    });
    // 选择文本
    _textEditingController.selection = TextSelection(
      baseOffset: 2,
      extentOffset: _textEditingController.text.length,
    );
  }
  // ...省略build方法
}

运行效果图如下:
获取焦点

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

class FocusDemo extends StatefulWidget {
  @override
  _FocusDemoState createState() => _FocusDemoState();
}

class _FocusDemoState extends State<FocusDemo> {
  FocusNode focusNode1 = FocusNode();
  FocusNode focusNode2 = FocusNode();
  FocusScopeNode focusScopeNode;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(16.0),
      child: Column(
        children: <Widget>[
          TextField(
            // 关联focusNode1
            focusNode: focusNode1,
            decoration: InputDecoration(labelText: 'input1'),
          ),
          TextField(
           // 关联focusNode2
            focusNode: focusNode2,
            decoration: InputDecoration(labelText: 'input1'),
          ),
          Builder(builder: (ctx) {
            return Column(
              children: <Widget>[
                RaisedButton(
                  onPressed: () {
                    // 将焦点从第一个TextField移到第二个TextField
                    if (null == focusScopeNode) {
                      focusScopeNode = FocusScope.of(context);
                    }
                    focusScopeNode.requestFocus(focusNode2);
                  },
                  child: Text('移动焦点'),
                ),
                RaisedButton(
                  onPressed: () {
                    // 当所有编辑框都失去焦点时键盘就会收起
                    focusNode1.unfocus();
                    focusNode2.unfocus();
                  },
                  child: Text('隐藏键盘'),
                )
              ],
            );
          }),
        ],
      ),
    );
  }
}
自定义TextField样式

代码示例如下:

class CustomTextFieldDemo extends StatefulWidget {
  @override
  _CustomTextFieldDemoState createState() => _CustomTextFieldDemoState();
}

class _CustomTextFieldDemoState extends State<CustomTextFieldDemo> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: <Widget>[
          Container(
            width: 300,
            height: 41,
            child: TextField(
              decoration: InputDecoration(
                contentPadding: EdgeInsets.fromLTRB(15, 13, 20, 0),
                fillColor: Colors.white,
                filled: true,
                hintText: '请输入手机号',
                hintStyle: TextStyle(
                    color: Color.fromRGBO(153, 153, 153, 1.0), fontSize: 14),
                enabledBorder: OutlineInputBorder(
                  borderSide: BorderSide(
                      color: Color.fromRGBO(197, 197, 197, 1.0), width: 1.0),
                  borderRadius: BorderRadius.all(Radius.circular(40)),
                ),
                focusedBorder: OutlineInputBorder(
                  borderSide: BorderSide(
                      color: Color.fromRGBO(197, 197, 197, 1.0), width: 1.0),
                  borderRadius: BorderRadius.all(Radius.circular(40)),
                ),
              ),
              keyboardType: TextInputType.number,
              style: TextStyle(fontSize: 14),
            ),
          ),
        ],
      ),
    );
  }
}

运行效果图如下:

2.表单Form

实际业务中,都会对各个输入框数据进行合法性校验,如果对每一个TextField都分别进行验证,是一件比较麻烦的事情。我们可以通过Form对输入框进行分组,统一进行一些操作。

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部分源码如下:

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的内容清空。

代码示例如下:

class FormDemo extends StatefulWidget {
  @override
  _FormDemoState createState() => _FormDemoState();
}

class _FormDemoState extends State<FormDemo> {
  TextEditingController _userNameController = TextEditingController();
  TextEditingController _passwordController = TextEditingController();
  GlobalKey _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.fromLTRB(27, 18, 27, 0),
      child: Form(
        key: _formKey, // 设置globalKey,用于后面获取FormState
        autovalidate: true, // 开启自动校验
        child: Column(
          children: <Widget>[
            TextFormField(
              textAlignVertical: TextAlignVertical.center,
              autofocus: true,
              controller: _userNameController,
              decoration: InputDecoration(
                hintText: '请输入手机号',
                icon: Image.asset('assets/images/telephone.png'),
              ),
              // 校验用户名
              validator: (value) {
                return value.trim().length > 0 ? null : '用户名不能为空';
              },
            ),
            TextFormField(
              controller: _passwordController,
              decoration: InputDecoration(
                hintText: '请输入密码',
                icon: Image.asset('assets/images/password.png'),
              ),
              obscureText: true,
              // 校验密码
              validator: (value) {
                return value.trim().length > 5 ? null : '密码不能少于6位';
              },
            ),
            Padding(
              padding: EdgeInsets.fromLTRB(27, 30, 27, 0),
              child: Row(
                children: <Widget>[
                  Expanded(
                    child: RaisedButton(
                      padding: EdgeInsets.all(15.0),
                      child: Text('登录'),
                      color: Theme.of(context).primaryColor,
                      textColor: Colors.white,
                      onPressed: () {
                        // 通过_formKey.currentState 获取FormState后
                        // 调用validate()方法校验用户名密码是否合法,校验
                        // 通过后再提交数据。
                        if((_formKey.currentState as FormState).validate()) {
                          // 验证通过提交数据
                          print('验证通过');
                        }
                      },
                    ),
                  )
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

代码传送门

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348