Flutter UI - 表单系 Widget

2,635 阅读5分钟

包括以下 widget:

  • TextField - 输入框
  • Switch - 选择切换
  • Radio - 单选按钮组
  • Checkbox - 多选按钮组
  • ``
  • ``
  • ``
  • ``
  • ``

TextField

网上的资料包括官方文档都是一上来一大堆属性列出来,输入框可设置的属性太多了,这样的话很难看的,很劝退的,所以我努力把这些属性分门别类按照几个大的范围整理下,以往能看的顺眼

属性大全

  // 虽然不想写,但是还是写上吧
  const TextField({
    Key key,
    this.controller,    //编辑框的控制器,跟文本框的交互一般都通过该属性完成,如果不创建的话默认会自动创建
    this.focusNode,  //用于管理焦点
    this.decoration = const InputDecoration(),   //输入框的装饰器,用来修改外观
    TextInputType keyboardType,   //设置输入类型,不同的输入类型键盘不一样
    this.textInputAction,   //用于控制键盘动作(一般位于右下角,默认是完成)
    this.textCapitalization = TextCapitalization.none,
    this.style,    //输入的文本样式
    this.textAlign = TextAlign.start,   //输入的文本位置
    this.textDirection,    //输入的文字排列方向,一般不会修改这个属性
    this.autofocus = false,   //是否自动获取焦点
    this.obscureText = false,   //是否隐藏输入的文字,一般用在密码输入框中
    this.autocorrect = true,   //是否自动校验
    this.maxLines = 1,   //最大行
    this.maxLength,   //能输入的最大字符个数
    this.maxLengthEnforced = true,  //配合maxLength一起使用,在达到最大长度时是否阻止输入
    this.onChanged,  //输入文本发生变化时的回调
    this.onEditingComplete,   //点击键盘完成按钮时触发的回调,该回调没有参数,(){}
    this.onSubmitted,  //同样是点击键盘完成按钮时触发的回调,该回调有参数,参数即为当前输入框中的值。(String){}
    this.inputFormatters,   //对输入文本的校验
    this.enabled,    //输入框是否可用
    this.cursorWidth = 2.0,  //光标的宽度
    this.cursorRadius,  //光标的圆角
    this.cursorColor,  //光标的颜色
    this.keyboardAppearance,
    this.scrollPadding = const EdgeInsets.all(20.0),
    this.dragStartBehavior = DragStartBehavior.down,
    this.enableInteractiveSelection,
    this.onTap,    //点击输入框时的回调(){}
    this.buildCounter,
  })

键盘控制

1. 键盘样式

Flutter 内置了不少种键盘样式的,有的我也没搞明白,就这么看吧

  • TextInputType.text - 普通完整键盘
  • TextInputType.number - 数字键盘
  • TextInputType.emailAddress - 带有“@”的普通键盘
  • TextInputType.datetime - 带有“/”和“:”的数字键盘)
  • TextInputType.multiline - 带有选项以启用有符号和十进制模式的数字键盘
  • url - 会显示“/ .”
  • phone - 会弹出数字键盘并显示"* #"
 TextField(
   keyboardType: TextInputType.datetime,
 )

2. 大小写控制

  • TextCapitalization.none - 全部小写
  • TextCapitalization.words - 每个单词的首字母大写
  • TextCapitalization.sentences - 每个句子的首字母大写
  • TextCapitalization.characters - 每个字每大写

我试了试,有的键盘貌似不管用,大家还是自己靠正则或是校验来控制输入吧,感觉这个设置不靠谱

3. 修改键盘动作按钮样式

关键词是 textInputAction,改变的是键盘右下角那个按钮的样式,根据安装的输入法的不同,有的显示的是文字,有的显示的是图标。Flutter 内置了不少样式,可惜没有预览,只能看英文自己猜了

TextField(
  textInputAction: TextInputAction.search,
),

4. 键盘动作按钮点击响应 - 无参的

回调是 onEditingComplete,注意是没有返回值的

TextField(
  onEditingComplete: (){
    print("AA");
  },
),

5. 键盘动作按钮点击响应 - 有参的

回调是 onSubmitted,这次是有返回值的,也就是你输入的数据

TextField(
  onSubmitted: (inputValue){
    print("输入数据位: ${inputValue}");
  },
),

监听事件

  • onChanged - 当用户输入就会触发回调,我们从中可以拿到用户输入值,经过测试,每次拿到的都是用户所有的输入值
TextField(
  keyboardType: TextInputType.datetime,
    onChanged: (inputValue) {
      print("input:${inputValue}");       
    },
)
  • TextEditingController - 官文档描述为:文本控制器,目前发现也就是 TextEditingController.text 拿到输入的内容,Flutter widget 一般都不要用 对象.属性的去写,所以提供了这么一个东西。我们要 new 一个 TextEditingController 出来,传给 TextField
class TestWidgetState extends State<TestWidget> {
  var editContore = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          width: 200,
          child: TextField(
            keyboardType: TextInputType.datetime,
            onChanged: (inputValue) {
              print("input:${inputValue}");
            },
            controller: editContore,
          ),
        ),
        RaisedButton(
          child: Text("请点击"),
          onPressed: () {
            print("输入内容:${editContore.text}");
          },
        ),
      ],
    );
  }
}
  • onTap - 在按下开始输入的那一瞬间触发,每次输入只触发一次
TextField(
  keyboardType: TextInputType.datetime,
    onTap: (){              
      print("tap...");
    },
)
  • onEditingComplete - 键盘完成按钮响应 - 无参数的
  • onSubmitted - 键盘完成按钮响应 - 有参数的
  • 实现右边图标点击 - 默认没有回调,需要我们自己写
class TestWidgetState extends State<TestWidget> {
  var isCan = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          width: 200,
          child: TextField(
            decoration: InputDecoration(
              suffixIcon: isCan
                  ? GestureDetector(
                      child: Icon(Icons.clear),
                      onTap: () {
                        print("AAAAAA");
                      },
                    )
                  : null,
              suffixText: isCan ? ":请确认" : null,
              suffixStyle: TextStyle(fontSize: 16),
            ),
          ),
        ),
      ],
    );
  }
}

输入框样式

Flutter 里修改样式啥的基本都是 decoration,contain 里是 BoxDecoration,TextField 的则是 InputDecoration,这部分内容也是相当多的,我拆开来说,我喜欢分门别类

1. 提示文字:

  • labelText/labelStyle - 输入框标题吧,在输入内容的上面,在我们输入之后,labelText 会有一个缩放的动画的,style 是文字样式,注意这个文字样式只能在输入之前管事
  • helperText/helperStyle - 输入内容下部的提示文字
  • errorText/errorStyle - 错误提示文字,也是在输入内容下部
  • hintText/hintStyle - 默认提示文字
TextField(
  labelText: "姓名:",
  labelStyle: TextStyle(
    fontSize: 22,
  ),
  helperText: '请输入你的真实姓名',
  helperStyle: TextStyle(
    fontSize: 8,
  ),
  errorText: "请重新输入",
  errorStyle: TextStyle(
    fontSize: 12,
  ),
  hintText: "请输入文字",
  hintStyle: TextStyle(
    fontSize: 12,
  ),
)

2. 左右提示和图片

  • prefixIcon/prefixText/prefixStyle - 左边的图标和文字,样式看下面图样,提示文字不会进入到输入内容的,这个测试过了
  • suffixIcon/suffixText/suffixStyle - 右边的图标和文字
  • prefix/suffix - 或者直接自己给一个 widget 也可以
  • icon - 左边的图标,但是图标在输入栏的外面,样子看下面
  • 注意点 - 这里的提示文字是只有在输入框拿到焦点时才会显示,否则只会显示 hint 文字
  • suffix 点击 - 默认没提供相关回调,我们自己套一个 GestureDetector 就行啦,测试过没问题的,并且不会和 onTap 冲突的
class TestWidgetState extends State<TestWidget> {
  var editContore = TextEditingController();
  var isCan = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          width: 200,
          child: TextField(
            decoration: InputDecoration(
              icon: Icon(Icons.search),
              prefixIcon: isCan ? Icon(Icons.access_alarm) : null,
              prefixText: isCan ? "输入:" : null,
              prefixStyle: TextStyle(fontSize: 12),
              suffixIcon: isCan
                  ? GestureDetector(
                      child: Icon(Icons.clear),
                      onTap: () {
                        print("AAAAAA");
                      },
                    )
                  : null,
              suffixText: isCan ? ":请确认" : null,
              suffixStyle: TextStyle(fontSize: 16),
            ),
          ),
        ),
      ],
    );
  }
}

3. 右小角统计数字

这个具体的文字样式是自己做的,假如想要 6/10 这种样式的,需要自己 setState 去修改文字

  • counterText - 文字
  • counterStyle - 文字样式
  • counter - 自定义 widget
  • 要是想达到图中的效果:
var count = 0;

TextField(
  decoration: InputDecoration(
    counterText: "${count}/10",
  ),
  onChanged: (inputValue) {
    setState(() {
      count = inputValue.length;
    });
  },
)

4. 改颜色

  • filled/fillColor - 修改输入框背景色,filled 是个 boolean 值,用来控制颜色显示的
TextField(
  decoration: InputDecoration(
    filled: true,
    fillColor: Colors.grey,
  ),
)

5. 修改边框

边框有3种:

  • InputBorder.none - 没有边框,此模式下连下面的线都没有了
  • OutlineInputBorder - 4面边框,可以设置圆角
TextField(
  decoration: InputDecoration(
    enabledBorder: OutlineInputBorder(
      borderRadius: BorderRadius.all(
      Radius.circular(30), //边角为30
      ),
      borderSide: BorderSide(
        color: Colors.amber, //边线颜色为黄色
        width: 2, //边线宽度为2
      ),
    ),
)
  • UnderlineInputBorder - 底边线,一样可以设置圆角,不过不好看
TextField(
  decoration: InputDecoration(
    enabledBorder: UnderlineInputBorder(
      borderRadius: BorderRadius.all(
        Radius.circular(30), //边角为30
      ),
      borderSide: BorderSide(
        color: Colors.amber, //边线颜色为黄色
        width: 2, //边线宽度为2
      ),
    ),
)

输入验证

就是 TextInputFormatter 这个东西啦,他是一个抽象接口,具体实现有以下几种:

  • WhitelistingTextInputFormatter - 白名单校验,也就是只允许输入符合规则的字符
  • BlacklistingTextInputFormatter - 黑名单校验,除了规定的字符其他的都可以输入
  • LengthLimitingTextInputFormatter - 长度限制,跟maxLength作用类似

效果呢就是输入无效,你输入的非法字符既无法显示,也不被算作输入之内

inputFormatters: [WhitelistingTextInputFormatter(RegExp("[a-z]"))],

inputFormatters 的特点呢,就是可以接受多个格式验证啦

  • 首先 inputFormatters 接受的是 [] 集合参数
  • 再者其实现类,像 WhitelistingTextInputFormatter 他们接受的是 Pattern 类型的数据,而正则 RegExp 恰恰就实现了 Pattern,所以本质上接受的就是正则表达式

当然了,使用 inputFormatters 的话是没办法和 errorText 联动的啦,如果要有这样的效果,那么就得自己在 onChange 中自己做正则判断啦


Switch

Switch 不多做介绍,大家都知道的,flutter 的这个 Switch 可以设置选中时按钮的颜色,图标,横条的颜色:

  • activeColor - 选中时按钮的颜色
  • activeThumbImage - 选中时按钮的图标
  • activeTrackColor - 选中时和横条的颜色
  • inactiveThumbColor - 不选时按钮的颜色
  • inactiveTrackColor - 不选时横条的颜色
  • inactiveThumbImage - 不选时按钮的图标
  • onChanged - 点击影响
  • value - 当前值,这个必须一开始就写上哈

Switch 在这里的最大问题就是此存有点小,大家看下图,这是相对整个屏幕的大小,而且 Switch 本身还不支持大小调整

class TestWidgetState extends State<TestWidget> {
  bool _value = false;

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Container(
      width: 200,
      decoration: BoxDecoration(
        color: Colors.tealAccent,
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('关'),
          Switch(
            value: _value,
            activeColor: Colors.blue,
            activeTrackColor: Colors.green,
            activeThumbImage: AssetImage(('assets/icons/icon_1.gif')),
            inactiveThumbColor: Colors.red,
            inactiveThumbImage: AssetImage(('assets/icons/icon_2.jpg')),
            inactiveTrackColor: Colors.grey,
            onChanged: (newValue) {
              setState(() {
                _value = newValue;
              });
            },
          ),
          Text('开'),
        ],
      ),
    );
  }
}

Radio

Radio 是单选按钮,我总是和多选混。Flutter 的单选按钮设计挺简单的,使用 groupValue 参数表示一组单选按钮的统一 ID,用相同 groupValue 的单选按钮就算一组的,比在外面再加一层容器的思路灵活多了,这样页面好写多了

  • value - 单选按钮值,这里值是无实际意义,此值就是表示用户点个哪个按钮,配合枚举会好用很多
  • groupValue - 单选按钮组统一 ID
  • onChanged - 回调
  • activeColor - 选中时候的颜色
class TestWidgetState extends State<TestWidget> {
  String _value = '1';
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Radio(
                value: 'a',
                activeColor: Colors.blue,
                groupValue: _value,
                onChanged: (newValue) {
                  setState(() {
                    print("a:${newValue}");
                    _value = newValue;
                  });
                }
            ),
            Text('开')
          ],
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Radio(
                value: 'b',
                activeColor: Colors.blue,
                groupValue: _value,
                onChanged: (newValue) {
                  setState(() {
                    print("b:${newValue}");
                    _value = newValue;
                  });
                }
            ),
            Text('关')
          ],
        )
      ],
    );
  }
}

Checkbox

Checkbox Flutter 的一贯设计:书写简洁设计简单,多选按钮组也是没有外层容器那一说了,而是采用有多少个按钮选项就配合多少个 boolean 参数的思路,配合上表驱动法的思路,很 Nice

  • value - true 选中,fasle 不选中
  • activeColor - 选中时填充颜色
  • checkColor - 选中时 Icon 颜色
class TestWidgetState extends State<TestWidget> {
  var _isBook = false;
  var _isPrice = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Row(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            Checkbox(
              value: _isBook,
              onChanged: (newValue) {
                print('book:$newValue');
                setState(() {
                  _isBook = newValue;
                });
              },
              activeColor: Colors.red,
              checkColor: Colors.blue,
            ),
            Text("书"),
          ],
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            Checkbox(
              value: _isPrice,
              onChanged: (newValue) {
                print('price:$newValue');
                setState(() {
                  _isPrice = newValue;
                });
              },
              activeColor: Colors.red,
              checkColor: Colors.blue,
            ),
            Text("价格"),
          ],
        )
      ],
    );
  }
}