【Flutter】输入框和表单(五)

一、 输入框

类似UITextField 与 UITextView结合体,

1.属性大概介绍总结:

controller:编辑框的控制器,通过它可以设置/获取编辑框的内容、选择编辑内容、监听编辑文本改变事件。大多数情况下我们都需要显式提供一个controller来与文本框交互。如果没有提供controller,则TextField内部会自动创建一个。

focusNode:用于控制TextField是否占有当前键盘的输入焦点。

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

keyboardType:用于设置该输入框默认的键盘输入类型,类似UITextField的键盘类型,可自行测试。
style:正在编辑的文本样式。
textAlign: 输入框内编辑文本在水平方向的对齐方式。
autofocus: 是否自动获取焦点,若为YES,即键盘弹出成为第一响应者。
obscureText:输入密码用的类型,输入后变成小黑点。
maxLines:输入框的最大行数,默认为1;如果为null,则无行数限制。
maxLength :代表输入框文本的最大长度,设置后输入框右下角会显示输入的文本计数。
maxLengthEnforced决定当输入文本长度超过maxLength时是否阻止输入,为true时会阻止输入,为false时不会阻止输入但输入框会变红。
onChange:输入框内容改变时的回调函数;注:内容改变事件也可以通过controller来监听。
onEditingComplete和onSubmitted:这两个回调都是在输入框输入完成时触发,比如按了键盘的完成键(对号图标)或搜索键.
inputFormatters:用于指定输入格式;当用户输入内容改变时,会根据指定的格式来校验。
enable:如果为false,则输入框会被禁用,禁用状态不接收输入和事件,同时显示禁用态样式(在其decoration中定义)。
cursorWidth、cursorRadius和cursorColor:自定义输入框光标宽度、圆角和颜色的。

2.实践结果尝试
测试.gif
3. 核心测试代码

class _FlutterTextFieldFormState extends State<FlutterTextField> {

  TextEditingController _textEditingController = TextEditingController();
// 每一个输入框都有一个FocusNode与之对应
  var _focusTextFieldNode = FocusNode();
  var _focusPwdFieldNode = FocusNode();
  var _focusCustomFieldNode = FocusNode();
  FocusScopeNode focusScopeNode;

  @override
  void initState() {
    super.initState();

// 初始化字符串,与选中字符
    _textEditingController.text = '123er';
    _textEditingController.selection =
        TextSelection(baseOffset: 0, extentOffset: 3);

    //监听输入改变
    _textEditingController.addListener(() {
      print('Controller监听:${_textEditingController.text}');
    });

    _focusTextFieldNode.addListener((){
      print('监听焦点变化 : ${_focusTextFieldNode.hasFocus}');
    });

// 这里监听文本成为第一响应者,从而更新底边颜色
    _focusCustomFieldNode.addListener((){
        setState(() {
        });
    });
  }

  Widget _buildTextField(context) {
    return Theme(
     data: Theme.of(context).copyWith(
       hintColor: Colors.red,
       inputDecorationTheme: InputDecorationTheme(
         labelStyle: TextStyle(
           color: Colors.yellow
         ),
         hintStyle: TextStyle(
           color: Colors.purple,
           fontSize: 20
         )
       )
     ),
     child: TextField(
       controller: _textEditingController,
       focusNode: _focusTextFieldNode,
       keyboardType: TextInputType.text,
       textInputAction: TextInputAction.search,
       style: TextStyle(
         color: Colors.red,
         fontSize: 30,
       ),
       textAlign: TextAlign.center,
       autofocus: true,
       obscureText: false,
       maxLines: 1,
       maxLength: 10,
       maxLengthEnforced: false,
       onChanged: (text) {
         print(text);
       },
       onEditingComplete: () {
         print('完成后:${_textEditingController.text}');
       },
       onSubmitted: (text) {
         print('提交点击');
         _focusTextFieldNode.unfocus();
       },
       //List<TextInputFormatter> inputFormatters,
       enabled: true,
       cursorWidth: 2.0,
       cursorRadius: Radius.circular(20.0),
       cursorColor: Colors.cyan,
       decoration: InputDecoration(
         labelText: "用户名",
         hintText: "用户名或邮箱",
         prefixIcon: Icon(Icons.person),
         icon: Icon(
           Icons.directions_run,
           color: Colors.red,
           size: 40,
         ),
       ),
     ),
   );
  }

  Widget _buidlCustomField(context){
    return Container(
      decoration: BoxDecoration(
        border: Border(
          bottom: BorderSide(
            color: _focusCustomFieldNode.hasFocus ? Colors.lightBlueAccent : Colors.green[200],
            width: 5.0,
          )
        )
      ),
      child: TextField(
        focusNode: _focusCustomFieldNode,
        keyboardType: TextInputType.text,
        decoration: InputDecoration(
            labelText: "Email",
            hintText: "电子邮件地址",
            prefixIcon: Icon(Icons.email),
            border: InputBorder.none //隐藏下划线
        ),
      ),
    );
  }

  Widget _buildPwdField() {
    return TextField(
      focusNode: _focusPwdFieldNode,
      decoration: InputDecoration(
        labelText: 'password',
        hintText: '输入你的密码',
        prefixIcon: Icon(Icons.arrow_drop_down),
      ),
      obscureText: true,
    );
  }

 Widget _buildFeatherButton(context){
   return Builder(builder: (ctx)
   {
     return Column(
       children: <Widget>[
         RaisedButton(
           child: Text("移动焦点"),
           onPressed: () {
             if (null == focusScopeNode) {
               focusScopeNode = FocusScope.of(context);
             }
             print(focusScopeNode);
             focusScopeNode.requestFocus(_focusPwdFieldNode);
           },
         ),
         RaisedButton(
           child: Text("隐藏键盘"),
           onPressed: () {
             _focusTextFieldNode.unfocus();
             _focusPwdFieldNode.unfocus();
           },
         ),
       ],
     );
   },
   );
  }

 @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('TextField'),
        backgroundColor: Colors.orange,
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            _buildTextField(context),
            _buildPwdField(),
          _buildFeatherButton(context),
            _buidlCustomField(context),
          ],
        ),
      ),
    );
  }
}
4. 补充:控制焦点
  • 焦点可以通过FocusNode和FocusScopeNode来控制。
  • 默认情况下,焦点由FocusScope来管理,它代表焦点控制范围,可以在这个范围内可以通过FocusScopeNode在输入框之间移动焦点、设置默认焦点等。
  • 通过FocusScope.of(context) 来获取widget树中默认的FocusScopeNode。

二、表单

Form widget,它可以对输入框进行分组,然后进行一些统一操作,如输入内容校验、输入框重置以及输入内容保存。

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内容发生变化时会触发此回调。

1. 与Form相关的几个类型

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

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

为了方便使用,Flutter提供了一个TextFormField widget,它继承自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的内容清空。

2. 测试案例运行结果
测试结果.gif
3. 核心测试代码

因为有状态变更,所以依然需要继承自State,监听并更新。

class _FlutterFromState extends State<FlutterForm> {
// TextEditingController用来监听文本
  TextEditingController _accountEditController = TextEditingController();
  TextEditingController _pwdEditController = TextEditingController();

// 设置globalKey,用于后面获取FormState
  GlobalKey _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Form'),
      ),
      body: Padding(
        padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20),
        child: Form(
          key: _formKey,
          autovalidate: true,
          child: Column(
            children: <Widget>[
              _buildAccountTF(),
              _buildPwdTF(),
              _buildLoginButton(),
              _buildClearButton(),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildAccountTF() {
    return TextFormField(
      autofocus: true,
      controller: _accountEditController,
      decoration: InputDecoration(
          labelText: '用户名', hintText: '请输入用户账号', icon: Icon(Icons.person)),
// 验证
      validator: (v) {
        return v.trim().length < 6 ? '账号不能小于6位' : null;
      },
    );
  }

  Widget _buildPwdTF() {
    return TextFormField(
      autofocus: false,
      controller: _pwdEditController,
      decoration: InputDecoration(
        labelText: '密码',
        hintText: '请输入密码',
        icon: Icon(Icons.lock),
      ),
      validator: (v) {
        return v.trim().length > 0 ? null : '密码不能为空';
      },
    );
  }

  Widget _buildLoginButton() {
    return Padding(
      padding: const EdgeInsets.all(20),
      child: Row(
        children: <Widget>[
          Expanded(
              child: RaisedButton(
                  padding: const EdgeInsets.all(5),
                  child: Text(
                    '点我登录',
                    style: TextStyle(
                      fontSize: 20,
                    ),
                  ),
                  textColor: Colors.red,
                  onPressed: () {
                    if ((_formKey.currentState as FormState).validate()) {
                      print('验证通过,可以登录');
                    } else {
                      print('验证不通过');
                    }
                  }))
        ],
      ),
    );
  }

  Widget _buildClearButton() {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          RaisedButton(
              padding: const EdgeInsets.all(5),
              child: Text(
                '清除数据',
                style: TextStyle(fontSize: 20),
              ),
              onPressed: () {
                if (_accountEditController.text.length <= 0 &&
                    _pwdEditController.text.trim().length <= 0) {
                  print('没有数据');
                  return;
                }
                FormState s = _formKey.currentState as FormState;
                s.reset();
              })
        ],
      ),
    );
  }
}
4. 注意context参数

context正是操作Widget所对应的Element的一个接口,由于Widget树对应的Element都是不同的,所以context也都是不同的,注意此context非彼context。

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