登录页几乎是每个联网app必备的界面,下面以我工作中开发的百卓优采云进销存app软件的登录页为例使用Flutter来实现,具体效果图如下:
界面看起来很简单,但麻雀虽小五脏俱全,使用到了实际开发中所需的大多数控件,下面让我们开启实现之旅,首先我们先实现上面的banner,实现之前我们先做好准备工作,把界面中需要的图片资源导入,具体步骤如下:
准备工作做好后,我们来一步一步实现登录界面,先上骨架代码
void main() => runApp(AbizApp ());
class AbizApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Title',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home:
//AppFuncBrowse(),
LoginPage(),
);
}
}
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() {
// TODO: implement createState
return _LoginPageState();
}
}
class _LoginPageState extends State<LoginPage> {
@override
Widget build(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(),
],
),
),
);
}
至此我们的登录页就实现完了,至于实际的登录联网就不再说了,有什么问题欢迎大家指正