登录页几乎是每个联网app必备的界面,下面以我工作中开发的百卓优采云进销存app软件的登录页为例使用Flutter来实现,具体效果图如下:



界面看起来很简单,但麻雀虽小五脏俱全,使用到了实际开发中所需的大多数控件,下面让我们开启实现之旅,首先我们先实现上面的banner,实现之前我们先做好准备工作,把界面中需要的图片资源导入,具体步骤如下:

* lib同级目录下创建目录assets/login/,将所需图片资源放入该目录,最终目录结构为

* 编辑pubspec.yaml , 将图片列表加入assets,编辑该文件时出现错误时,请检查前面缩进的空格
准备工作做好后,我们来一步一步实现登录界面,先上骨架代码
void main() => runApp(AbizApp ()); class AbizApp extends StatelessWidget {
@override Widgetbuild(BuildContext context) { return MaterialApp( title: 'Title'
, theme: ThemeData( primarySwatch: Colors.blue, ), home: //AppFuncBrowse(),
LoginPage(), ); } } class LoginPage extends StatefulWidget { @override
_LoginPageStatecreateState() { // TODO: implement createState return
_LoginPageState(); } } class _LoginPageState extends State<LoginPage> {
@override Widgetbuild(BuildContext context) { return Scaffold( body: Column(
crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
_buildTopBannerWidget(), ], ), ); } }
顶部banner实现很简单,就是显示一张图片, Image有fit属性,用来控制图片的显示方式,具体对应的值建议大家都亲自试一试来加深理解,我就不再赘述了
_buildTopBannerWidget() { return Container( child: Image.asset(
"assets/login/login_banner.png", fit: BoxFit.cover, ), ); }
中国制造网登录的账号提示,很简单,我就不再废话,直接上代码
_buildAccountLoginTip() { return Padding( padding: EdgeInsets.all(15), child:
Text( "百卓采购网/中国制造网会员登录", maxLines: 1, textAlign: TextAlign.start, style:
TextStyle(fontSize: 16, color: Colors.black54), ), ); }
下面实现关键部分,用户名和密码输入框,上代码
_buildEditWidget() { return Container( margin: EdgeInsets.only(left: 15, right:
15), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4.0), border
: Border.all( width: 1.0 / MediaQuery.of(context).devicePixelRatio, color:
Colors.grey.withOpacity(0.5)), ), ); }

flutter中控件度量的单位是逻辑像素,和iOS中点的概念一样,为了使边框是1px宽,我们必须获取1个逻辑像素代表几个物理像素(px),MediaQuery.of(context).devicePixelRatio就是我们需要的,MediaQuery.of(context)返回MediaQueryData,MediaQueryData里包含屏幕大小(逻辑大小),padding等信息。

下面我们来实现用户名的输入框
class _LoginPageState extends State<LoginPage> { TextEditingController
_pwdEditController; TextEditingController _userNameEditController; final
FocusNode _userNameFocusNode= FocusNode(); final FocusNode _pwdFocusNode =
FocusNode(); @override void initState() { super.initState(); _pwdEditController
= TextEditingController(); _userNameEditController = TextEditingController();
_pwdEditController.addListener(() => setState(() => {}));
_userNameEditController.addListener(() => setState(() => {})); } // 此处省略其他代码 }
_buildLoginNameTextField() { return TextField( controller:
_userNameEditController, focusNode: _userNameFocusNode, decoration:
InputDecoration( hintText: "登录名/邮箱/手机", border: InputBorder.none, prefixIcon:
Image.asset( "assets/login/user_name.png", fit: BoxFit.none, ), suffixIcon: (
_userNameEditController.text ?? "").isEmpty ? IconButton( icon: Image.asset(
"assets/login/qrcode_login.png", fit: BoxFit.cover, ), onPressed: () => {}, ) :
IconButton( icon: Icon( Icons.cancel, color: Colors.grey, ), onPressed: () {
_userNameEditController.clear(); _userNameFocusNode.unfocus(); setState(() {});
}) ), ); }
TextField控件功能和原生iOS控件UITextField功能类似,不过大多数属性通过设置decoration来实现,
placeholder和leftview,rightview对应hintText,prefixIcon,suffixIcon属性,为了实现输入内容不为空时输入框右边显示清空按钮,需要监听TextField的值,我们通过设置TextEditingController并监听它的值变化来实现,隐藏键盘我们通过FocusNode来设置,具体见代码,为了去除编辑框的下划线,设置
InputDecoration属性
border: InputBorder.none,
登录名的介绍已经完了,密码框的实现和登录名几乎一样,唯一的不同就是设置属性 obscureText: true, 其他的就不再多说,上代码
_buildPwdTextField() { return TextField( controller: _pwdEditController,
focusNode: _pwdFocusNode, obscureText: true, decoration: InputDecoration(
hintText: "密码", border: InputBorder.none, prefixIcon: Image.asset(
"assets/login/password.png", fit: BoxFit.none, ), suffixIcon: (
_pwdEditController.text ?? "").isEmpty ? FlatButton( child: Text("忘记密码"),
onPressed: () { _pwdFocusNode.unfocus(); _userNameFocusNode.unfocus(); }) :
IconButton( icon: Icon( Icons.cancel, color: Colors.grey, ), onPressed: () {
_pwdEditController.clear(); _pwdFocusNode.unfocus(); setState(() {}); }), )); }
两个输入框之间有一条1px分割线,分割线在Flutter中也有控件:Divider,最后编辑框组的实现如下
_buildEditWidget() { return Container( margin: EdgeInsets.only(left: 15, right:
15), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4.0), border
: Border.all( width: 1.0 / MediaQuery.of(context).devicePixelRatio, color:
Colors.grey.withOpacity(0.5)), ), child: Column( children: <Widget>[
_buildLoginNameTextField(), Divider(height: 1.0), _buildPwdTextField(), ], ), );
}
现在只剩下最后的部件:登录按钮和注册按钮
_buildLoginRegisterButton() { return Padding( padding: EdgeInsets.all(15),
child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[
Expanded( child: Container( height: 44, decoration: BoxDecoration( borderRadius:
BorderRadius.circular(4.0), color: Colors.grey.withOpacity(0.3), ), child:
FlatButton( onPressed: null, child: Text( "登录", style: TextStyle(color: Colors.
white), )), ), ), SizedBox(width: 15.0), Expanded( child: Container( height: 44,
decoration: BoxDecoration( borderRadius: BorderRadius.circular(4.0), border:
Border.all(width: 1.0, color: Colors.green), ), child: FlatButton( onPressed:
null, child: Text( "立即注册", style: TextStyle(color: Colors.green), )), )) ], ), )
; }

为了使按钮等分剩余空间,需要用到Expanded控件包裹,Expanded控件从Flexible派生,Flexible通过属性flex设置剩余空间的分配,熟悉android的一眼就看出类似于layout_weight属性

各个部件都实现好了,现在就是组装的时候了,我们的采取从上到下的线性布局,Flutter可以用Column来实现
@override Widget build(BuildContext context) { return Scaffold( body: Column(
crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
_buildTopBannerWidget(), _buildAccountLoginTip(), _buildEditWidget(),
_buildLoginRegisterButton(), ], ), ); }
运行后一切正常,但是点击输入框问题来了

具体意思是布局重叠了,这时候SingleChildScrollView就派上用场了,它能在弹出键盘时将内容向上移动使输入框不被覆盖,优化后的代码如下
@override Widget build(BuildContext context) { return Scaffold( body:
SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.
start, children: <Widget>[ _buildTopBannerWidget(), _buildAccountLoginTip(),
_buildEditWidget(), _buildLoginRegisterButton(), ], ), ), ); }
至此我们的登录页就实现完了,至于实际的登录联网就不再说了,有什么问题欢迎大家指正