flutter 项目 ToDo之登录页面

作一件很容易的小事并不难,困难的是把一件小事作到极致,这就十分不容易了!好比一个登录页,无非就一个logo、两个输入框以及几个按钮,以下图:html

面临的问题

为何说困难呢?咱们一块儿来看看细节:输入类型、输入长度、是否多行、自动校验、异常提醒、输入法的键盘动做按钮图标(即回车键位图标)修改并实现点击回调、提示文本、输入框样式设置、一键删除等等,接下来咱们就来解决这些问题。前端

搭建布局

import 'package:flutter/material.dart';

class Login extends StatefulWidget {
  @override
  _LoginState createState() => new _LoginState();
}

class _LoginState extends State<Login> {


  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Padding(
          padding: EdgeInsets.fromLTRB(24.0, 0.0, 24.0, 0.0),
          child: ListView(
            children: <Widget>[
              SizedBox(
                height: 72,
              ),
              Center(
                child: Image.asset(
                  "res/images/icon_logo.png",
                  scale: 1.5,
                ),
              ),
              SizedBox(
                height: 36,
              ),
              TextField(


              ),
              SizedBox(
                height: 14,
              ),
              TextField(

              ),
              SizedBox(
                height: 32,
              ),
              FlatButton(
                  child: RaisedButton(
                    padding: EdgeInsets.all(10),
                    child: Text(
                      '登陆/注册',
                    ),
                  )),
            ],
          ),
        ));
  }
}
复制代码

丑出天际了吧?我也这么以为,接下来咱们来一一解决上面提出的问题,实现效果图!git

解决问题

1、圆形图片

第一步将上面的图标修改一下,设置为一个圆形图形。通过询问百度,得知使用CircleAvatar能够实现圆形图片,说干就干:github

CircleAvatar(
            radius: 56.0,
            child: Image.asset(
              "res/images/icon_logo.png",
              scale: 1.8,
            ),
          )
复制代码

可是后来使用过程当中发现个人图片资源是背景是透明色,而全部的Image都会默认设置主题颜色为背景色,仍是丑得哭兮流了!正则表达式

CircleAvatar的背景色修改成蓝色,而后将图片修改一下,增长一个白色的背景色,代码以下:编程

CircleAvatar(
            backgroundColor: Colors.blue,
            radius: 56.0,
            child: Image.asset(
              "res/images/icon_logo.png",
              color: Colors.white,
              scale: 1.8,
            ),
          ),
复制代码

看上去将个就了,接下来修改输入框样式。

2、设置输入框样式

这里须要解决上面提到的问题:api

  • 输入类型
  • 输入长度
  • 是否多行
  • 提示文本
  • 密码关闭明文展现
  • 下划线自定义
  • 输入法焦点换行

这些问题看似都很简单,可是细节有魔鬼,看似简单的地方每每埋藏着“炸弹”!接下来咱们就先来看看输入框的Widget提供了哪些api供咱们调用?bash

const TextField({
  ...
    this.controller,
    this.focusNode,
    this.decoration = const InputDecoration(),
    TextInputType keyboardType,
    this.textInputAction,
    this.textCapitalization = TextCapitalization.none,
    this.style,
    this.strutStyle,
    this.textAlign = TextAlign.start,
    this.textDirection,
    this.autofocus = false,
    this.obscureText = false,
    this.autocorrect = true,
    this.maxLines = 1,
    this.minLines,
    this.expands = false,
    this.maxLength,
    this.maxLengthEnforced = true,
    this.onChanged,
    this.onEditingComplete,
    this.onSubmitted,
    this.inputFormatters,
    this.enabled,
    this.cursorWidth = 2.0,
    this.cursorRadius,
    this.cursorColor,
    this.keyboardAppearance,
    this.scrollPadding = const EdgeInsets.all(20.0),
    this.dragStartBehavior = DragStartBehavior.start,
    this.enableInteractiveSelection,
    this.onTap,
    this.buildCounter,
    this.scrollPhysics,
  ...
})
复制代码
  • controller:编辑框的控制器,经过它能够设置/获取编辑框的内容、选择编辑内容、监听编辑文本改变事件。大多数状况下咱们都须要显式提供一个controller来与文本框交互。若是没有提供controller,则TextField内部会自动建立一个。
  • focusNode:用于控制TextField是否占有当前键盘的输入焦点。它是咱们和键盘交互的一个handle。
  • InputDecoration:用于控制TextField的外观显示,如提示文本、背景颜色、边框等。
  • keyboardType:用于设置该输入框默认的键盘输入类型,取值以下:
TextInputType枚举值 含义
text 文本输入键盘
multiline 多行文本,需和maxLines配合使用(设为null或大于1)
number 数字;会弹出数字键盘
phone 优化后的电话号码输入键盘;会弹出数字键盘并显示"* #"
datetime 优化后的日期输入键盘;Android上会显示“: -”
emailAddress 优化后的电子邮件地址;会显示“@ .”
url 优化后的url输入键盘; 会显示“/ .”
  • textInputAction:键盘动做按钮图标(即回车键位图标),它是一个枚举值,有多个可选值,所有的取值列表读者能够查看API文档,下面是当值为TextInputAction.search时,原生Android系统下键盘样式:

image-20180903181235471

  • style:正在编辑的文本样式。
  • textAlign: 输入框内编辑文本在水平方向的对齐方式。
  • autofocus: 是否自动获取焦点。
  • obscureText:是否隐藏正在编辑的文本,如用于输入密码的场景等,文本内容会用“•”替换。
  • maxLines:输入框的最大行数,默认为1;若是为null,则无行数限制。
  • maxLength和maxLengthEnforced :maxLength表明输入框文本的最大长度,设置后输入框右下角会显示输入的文本计数。maxLengthEnforced决定当输入文本长度超过maxLength时是否阻止输入,为true时会阻止输入,为false时不会阻止输入但输入框会变红。
  • onChange:输入框内容改变时的回调函数;注:内容改变事件也能够经过controller来监听。
  • onEditingComplete和onSubmitted:这两个回调都是在输入框输入完成时触发,好比按了键盘的完成键(对号图标)或搜索键(🔍图标)。不一样的是两个回调签名不一样,onSubmitted回调是ValueChanged<String>类型,它接收当前输入内容作为参数,而onEditingComplete不接收参数。
  • inputFormatters:用于指定输入格式;当用户输入内容改变时,会根据指定的格式来校验。
  • enable:若是为false,则输入框会被禁用,禁用状态不接收输入和事件,同时显示禁用态样式(在其decoration中定义)。
  • cursorWidth、cursorRadius和cursorColor:这三个属性是用于自定义输入框光标宽度、圆角和颜色的。

了解了上面的api之后,咱们就不使用TextField,就是这么任性!先不要想打人,由于咱们为了自动检测输入的内容是否合法,咱们须要使用一个叫Form的东西来实现(固然controller也能够,只是操做起来比较麻烦,像我这样的懒人确定是不会这么作的),并且二者的不少属性都是同样的,不信你看代码:ide

Form(
            //设置globalKey,用于后面获取FormState
            key: _formKey,
            //开启自动校验
            autovalidate: true,
            child: Column(
              children: <Widget>[
                TextFormField(
                autofocus: true,
                focusNode: _userFocusNode,
                keyboardType: TextInputType.emailAddress,
                textInputAction: TextInputAction.next,
                decoration: InputDecoration(
                  labelText: "用户名",
                  hintText: "手机号或邮箱",
                  icon: Icon(Icons.person),
                ),
                // 校验用户名
                validator: (v) {
                  var userName = v.trim();
                  if (isChinaPhoneLegal(userName) ||
                      isEmailValid(userName)) {
                    return null;
                  } else {
                    return "用户名必须是手机号或者邮箱地址";
                  }
                },
                onEditingComplete: () {
                  if (null == focusScopeNode) {
                    focusScopeNode = FocusScope.of(context);
                  }
                  focusScopeNode.requestFocus(_psdFocusNode);
                }),
                TextFormField(

                  textInputAction: TextInputAction.done,
                  keyboardType: TextInputType.text,
                  decoration: InputDecoration(
                    labelText: "密码",
                    hintText: "您的登陆密码",
                    icon: Icon(Icons.lock),
                  ),
                  obscureText: true,
                  //校验密码
                  validator: (v) {
                    // 可在此经过正则表达式校验密码是否符合规则
                    return v.trim().length > 5 ? null : "密码不能少于6位";
                  },
                  onEditingComplete: () {
                    _userFocusNode.unfocus();
                    _psdFocusNode.unfocus();
                  },
                ),
              ],
            ),
          ),
复制代码

目前基本实现了上面说的细节,可是左边的图标并无居中,这个确定逃不过UI的像素眼,因而仔细查看以后,发现InputDecoration还有两个图标对象,分别是prefixIconsuffixIcon,通过编程合做伙伴有道翻译的帮助,终于明白我在此处使用prefixIcon就能够圆满解决问题了,此处就不单独上图了。可是还有两个细节没有完成,就是增长一键删除和密码明文显示的控制按钮,可是有了上面编程合做伙伴的帮助,彷佛问题也不难解决了!函数

首先,在Dart中一切可使用的变量引用都是对象(惋惜我仍是没有对象),所以咱们能够自定义一个对象继承自suffixIcon,并实现它的点击事件便可解决上面的需求。为何须要自定义一个对象呢?由于若是在同一个类中,经过setState来刷新是整个类的全局刷新,会把全部的输入框内容所有清除,这不是咱们指望看到的效果。以前看到更新Dialog的内容也是同一个思路,因此这里咱们就定义了一个StatefulWidget来实现局部的刷新,代码以下:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

///自带删除的ITextField
typedef void ITextFieldCallBack(String content, bool isValid);

class UserNameField extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _UserNameFiledState();

  UserNameField(
      {@required this.fieldCallBack,
      this.focusNode,
      this.keyboardType,
      this.textInputAction,
      this.autofocus,
      this.maxLength,
      this.onEditingComplete,
      this.labelText,
      this.hintText,
      this.prefixIcon,
      this.suffixIcon,
      this.validator});

  final FocusNode focusNode;
  final TextInputType keyboardType;
  final TextInputAction textInputAction;
  final bool autofocus;
  final FormFieldValidator<String> validator;
  final ITextFieldCallBack fieldCallBack;
  final int maxLength;
  final VoidCallback onEditingComplete;
  final String labelText;
  final String hintText;
  final Widget prefixIcon;
  final Widget suffixIcon;
}

class _UserNameFiledState extends State<UserNameField> {
  bool _isShowCleanIcon = false;
  TextEditingController _controller = TextEditingController();
  GlobalKey _formKey = new GlobalKey<FormState>();

  @override
  void initState() {
    super.initState();
    _controller?.addListener(() {
      widget.fieldCallBack(
          _controller?.text, (_formKey.currentState as FormState).validate());
      bool state = _controller?.text?.length != 0;
      if (_isShowCleanIcon != state) {
        setState(() {
          _isShowCleanIcon = state;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      //开启自动校验
      autovalidate: true,
      key: _formKey,
      child: TextFormField(
        controller: _controller,
        focusNode: widget.focusNode,
        keyboardType: widget.keyboardType,
        textInputAction: widget.textInputAction,
        autofocus: widget.autofocus,
        maxLength: widget.maxLength,
        onEditingComplete: widget.onEditingComplete,
        validator: widget.validator,
        decoration: InputDecoration(
          labelText: widget.labelText,
          hintText: widget.hintText,
          prefixIcon: widget.prefixIcon,
          suffixIcon: GestureDetector(
            onTap: () {
              widget.fieldCallBack("", false);
              setState(() {
                _isShowCleanIcon = !_isShowCleanIcon;
              });
              _controller.clear();
            },
            child: _isShowCleanIcon
                ? widget.suffixIcon
                : IgnorePointer(
                    ignoring: true,
                    child: new Opacity(
                      opacity: 0.0,
                      child: widget.suffixIcon,
                    )),
          ),
        ),
      ),
    );
  }
}

复制代码

密码明文显示的开关与之相似,此处就不赘述了,后面会提供源码。 接下来看看效果:

3、修改Button的样式

flutter对于按钮提供了多种选择(阿里拍卖前端团队写的Flutter开发者必备手册 Flutter Go介绍了八种,官网介绍的是六种)

咱们的要求很简单,就是一个圆角的长方形Button便可,这里为了有“水波动画”和实质感的阴影,咱们选择了RaisedButton来实现:

Padding(
            padding: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0),
            child: RaisedButton(
                color: Colors.blue,
                highlightColor: Colors.blue[700],
                colorBrightness: Brightness.dark,
                splashColor: Colors.grey,
                child: Text(
                  "登陆/注册",
                  style: TextStyle(color: Colors.white, fontSize: 18.0),
                ),
                shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(20.0)),
                onPressed: () => {
                      if (_userNameValid && _psdValid)
                        {
                          // TODO 登陆
                        }
                    }),
          ),
复制代码

最后再优化一下,增长忘记密码和跳过登陆,此处不上代码了,直接上图:


说来一个简单的登陆页,写了四个文件,其中两个是自定义Widget、一个工具类,累计不到400行代码,可是一边查文档一边查资料,写起来仍是各类酸爽!但愿有大佬指出其中不合理的地方,毕竟我仍是菜鸟,但愿能够和大佬们一块儿进步!

源码

参考一:Flutter实战:输入框和表单

参考二:玩安卓 Flutter版本

参考三:flutter 自定义TextField,自带删除

相关文章
相关标签/搜索