写点什么

Flutter 开发一个常用的登录界面

作者:岛上码农
  • 2022 年 3 月 18 日
  • 本文字数:3979 字

    阅读完需:约 13 分钟

Flutter 开发一个常用的登录界面

登录页面在 App 开发中非常常见,本篇借登录页面的开发介绍了文本框 TextField 组件的使用,同时使用文本框的装饰属性实现了个性化文本框设置。

业务逻辑

为了演示登录跳转,在分类浏览先做了一个简单的按钮,点击跳转到登录页面。实际的 App 中,通常会是触发某些需要登录才能查看的操作后再跳转到登录界面。

布局分析


界面如上图所示,从界面上看,整体内容区域是居中的,内容的布局是一个简单的列式布局,包括了顶部的一个 Logo(通常是 App 图标),再往下是两个文本输入框,最后是登录按钮。整体布局比较简单,使用 Center 下嵌一个 Column 进行列布局即可。

图片圆形裁剪

在 Flutter 中实行图片圆形裁剪有两个方式,一是使用外层的容器,通过将正方形的按圆形裁剪即可;二是使用内置的 CircleAvatar。不过从名字上看 CircleAvatar 用于头像的,因此这里使用容器的来实现圆形裁剪。封装一个获取圆形图片的方法_getRoundImage,传入图片资源名称和正方形边长,代码如下所示:


Widget _getRoundImage(String imageName, double size) {    return Container(      width: size,      height: size,      clipBehavior: Clip.antiAlias,      decoration: BoxDecoration(        borderRadius: BorderRadius.all(Radius.circular(size / 2)),      ),      child: Image.asset(        imageName,        fit: BoxFit.fitWidth,      ),    );  }
复制代码


这里使用了 BoxDecoration 将边框设置为圆形的边框,半径为边长的一半,这样就达到边框是圆形的效果了。但是,需要额外设置一个属性就是 clipBehavior,这是边缘裁剪类型,默认是不裁剪的。这里使用了 Clip.antiAlias(抗锯齿)的方式进行裁剪,这种方式的裁剪效果最好,但是更耗资源,其他的裁剪方式如下:


  • Clip.hardEdge:从名字就知道,这种方式很粗糙,但是裁剪的效率最快;

  • Clip.antiAliasSaveLayer:最为精细的裁剪,但是非常慢,不建议使用;

  • Clip.none:默认值,如果内容区没有超出容器边界的话,不会做任何裁剪。内容超出边界的话需要使用别的裁剪方式防止内容溢出。

圆形扁平按钮

这里需要提一下, Flutter 2.0 以前的扁平按钮是 FlatButton,使用起来很简单,但是很多场合不太满足,因此 2.0 以后引入了 TextButton 替代。TextButton 多了一个 style 来装饰按钮样式。具体可以看官方的文档。这里我们的按钮需要设置背景色为主题色,然后按钮文字颜色为白色,同时需要切成圆角,因此还是使用 Container 的边界圆弧来实现。需要注意的是,默认按钮的宽度是根据内容来的,因此为了让按钮撑满屏幕,我们设置了 Container 的宽度为 double.infinity。代码如下所示:


Widget _getLoginButton() {    return Container(      height: 50,      width: double.infinity,      margin: EdgeInsets.all(10),      decoration: BoxDecoration(        color: Theme.of(context).primaryColor,        borderRadius: BorderRadius.circular(4.0),      ),      child: TextButton(        style: ButtonStyle(          foregroundColor: MaterialStateProperty.all<Color>(Colors.white),          backgroundColor:              MaterialStateProperty.all<Color>(Theme.of(context).primaryColor),        ),        child: Text(          '登录',        ),        onPressed: () {          print(              'Login: username=${_username.trim()}, password=${_password.trim()}');        },      ),    );  }
复制代码


按钮点击回调事件为 onPressed,这里只是简单地打印了表单的内容。

TextField 文本框

TextField 是 Flutter 提供的文本输入框,TextField 的属性非常多,常用的属性如下:


  • keyboardType:键盘类型,可以指定是数字、字母、电话号码、邮箱、日期等多种方式,通过与表单内容匹配的键盘类型可以提供输入效率,进而改善用户体验。

  • controller:TextEditingController 对象,TextEditingController 主要用于控制文本框的初始值,清除内容的操作。

  • obscureText:是否需要隐藏输入内容,如果为 true,则输入内容会使用圆点显示,通常用与密码。

  • decoration:文本框的装饰,属性也很多,可以指定前置图标,边框类型、后置组件等多种属性,因此可以通过 decoration 获得想要的文本框样式。

  • focusNode:聚焦点,可以通过这个来控制文本框是否获取焦点,从而实现类似上一个下一个的输入控制。

  • onChanged:输入值改变事件回调,通常用这个方法实现双向绑定。


在这个案例中,我们使用了一个前置图标用来表示输入内容的类型,比如使用手机图标代表输入手机号,使用锁代表代表密码。同时使用了一个 Offstage 作为后置的组件,用于在输入内容后可以点击清除内容。Offstage 组件是通过一个属性 offstage 来控制组件是否显示,这样我们可以在没有内容的时候隐藏它,有输入内容的时候再显示。


为了提高代码复用性,使用了一个方法获取通用的文本框,这里主要是使用了 Container 包裹以控制边距和文本框下的分隔线:


Widget _getInputTextField(    TextInputType keyboardType, {    FocusNode focusNode,    controller: TextEditingController,    onChanged: Function,    InputDecoration decoration,    bool obscureText = false,    height = 50.0,  }) {    return Container(      height: height,      margin: EdgeInsets.all(10.0),      child: Column(        children: [          TextField(            keyboardType: keyboardType,            focusNode: focusNode,            obscureText: obscureText,            controller: controller,            decoration: decoration,            onChanged: onChanged,          ),          Divider(            height: 1.0,            color: Colors.grey[400],          ),        ],      ),    );  }
复制代码

完整代码

class _LoginPageState extends State<LoginPage> {  //TextEditingController可以使用 text 属性指定初始值  TextEditingController _usernameController = TextEditingController();  TextEditingController _passwordController = TextEditingController();  String _username = '', _password = '';
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('登录'), brightness: Brightness.dark, ), body: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ _getRoundImage('images/logo.png', 100.0), SizedBox( height: 60, ), _getUsernameInput(), _getPasswordInput(), SizedBox( height: 10, ), _getLoginButton(), ], ), ), ); }
Widget _getUsernameInput() { return _getInputTextField( TextInputType.number, controller: _usernameController, decoration: InputDecoration( hintText: "输入手机号", icon: Icon( Icons.mobile_friendly_rounded, size: 20.0, ), border: InputBorder.none, //使用 GestureDetector 实现手势识别 suffixIcon: GestureDetector( child: Offstage( child: Icon(Icons.clear), offstage: _username == '', ), //点击清除文本框内容 onTap: () { this.setState(() { _username = ''; _usernameController.clear(); }); }, ), ), //使用 onChanged 完成双向绑定 onChanged: (value) { this.setState(() { _username = value; }); }, ); }
Widget _getPasswordInput() { return _getInputTextField( TextInputType.text, obscureText: true, controller: _passwordController, decoration: InputDecoration( hintText: "输入密码", icon: Icon( Icons.lock_open, size: 20.0, ), suffixIcon: GestureDetector( child: Offstage( child: Icon(Icons.clear), offstage: _password == '', ), onTap: () { this.setState(() { _password = ''; _passwordController.clear(); }); }, ), border: InputBorder.none, ), onChanged: (value) { this.setState(() { _password = value; }); }, ); }
//省略了上述列举的代码
}
复制代码

页面跳转

在上层面的登录按钮上,我们增加了一个点击事件,点击后再跳到登录页,按钮的响应代码如下所示。这是页面跳转的最简单的方式,使用 Navigator 导航器的 push 方法实现页面跳转,后续会介绍如何通过路由实现页面跳转,那种方式更为优雅。


//...onPressed: () {  Navigator.of(context).push(    MaterialPageRoute(builder: (context) => LoginPage()),  );},//...
复制代码

总结

从代码上看,功能虽然实现了,但是构建用户名和密码的代码十分相似,有没有办法进一步提高代码复用率,构建一个更为通用的表单组件呢?下篇我们将介绍如何来封装。



发布于: 刚刚阅读数: 2
用户头像

岛上码农

关注

用代码连接孤岛,公众号@岛上码农 2022.03.03 加入

从南漂到北,从北漂到南的业余码农

评论

发布
暂无评论
Flutter 开发一个常用的登录界面_ios_岛上码农_InfoQ写作平台