flutter 项目 ToDo之登陆页面

1,969 阅读8分钟

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

面临的问题

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

搭建布局



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(
                      '登录/注册',
                    ),
                  )),
            ],
          ),
        ));
  }
}

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

解决问题

一、圆形图片

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

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,
            ),
          ),

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

二、设置输入框样式

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

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

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

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也可以,只是操作起来比较麻烦,像我这样的懒人肯定是不会这么做的),而且两者的很多属性都是一样的,不信你看代码:

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,
                    )),
          ),
        ),
      ),
    );
  }
}

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

三、修改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,自带删除