做一件很容易的小事并不难,困难的是把一件小事做到极致,这就十分不容易了!比如一个登陆页,无非就一个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系统下键盘样式:
- 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
还有两个图标对象,分别是prefixIcon
和suffixIcon
,经过编程合作伙伴有道翻译的帮助,终于明白我在此处使用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行代码,但是一边查文档一边查资料,写起来还是各种酸爽!希望有大佬指出其中不合理的地方,毕竟我还是菜鸟,希望可以和大佬们一起进步!