用 Flutter 写一个精美的登录页面(最新版)

打印 上一主题 下一主题

主题 796|帖子 796|积分 2388

用 Flutter 写一个精美的登录页面(最新版)



   参考了博客:用flutter写一个精美的登录页面。但是那篇文章是 18 年的,较多 API 已经更新,本篇博文在其基础上使用最新版本 Dart 和 Flutter 开发。
  完整源码在文章最后,有需要可以直接看源码。
效果图:


主体结构

该页面的主体布局如下:


  • 整体基于一个 Scaffold,没有 AppBar
  • 由于要对输入框进行表单校验,使用了 Form
  • 使用 ListView 作为外层控件
  1. return Scaffold(
  2.   body: Form(
  3.     key: _formKey,
  4.     autovalidateMode: AutovalidateMode.onUserInteraction,
  5.     child: ListView(
  6.       padding: const EdgeInsets.symmetric(horizontal: 20),
  7.       children: [
  8.         const SizedBox(height: kToolbarHeight), // 距离顶部一个工具栏的高度
  9.         buildTitle(), // Login
  10.         buildTitleLine(), // 标题下面的下滑线
  11.         const SizedBox(height: 50),
  12.         buildEmailTextField(), // 输入邮箱
  13.         const SizedBox(height: 30),
  14.         buildPasswordTextField(context), // 输入密码
  15.         buildForgetPasswordText(context), // 忘记密码
  16.         const SizedBox(height: 50),
  17.         buildLoginButton(context), // 登录按钮
  18.         const SizedBox(height: 30),
  19.         buildOtherLoginText(), // 其他账号登录
  20.         buildOtherMethod(context), // 其他登录方式
  21.         buildRegisterText(context), // 注册
  22.       ],
  23.     ),
  24.   ),
  25. );
复制代码
标题


大标题 Titile 比较简单,就是设置了 边距 和 文字大小:


  • Padding 组件:设置边距
  • Text 组件:显示文本及控制样式
  1. Widget buildTitle() {
  2.   return const Padding( // 设置边距
  3.       padding: EdgeInsets.all(8),
  4.       child: Text(
  5.         'Login',
  6.         style: TextStyle(fontSize: 42),
  7.       ));
  8. }
复制代码
Title 下面的下划线其实就是个设置了宽高的 Container:


  • Align 组件:用于控制子组件的对齐方式
  • Container 组件:一个包含绘画、定位、大小的组件
  1. Widget buildTitleLine() {
  2.   return Padding(
  3.       padding: const EdgeInsets.only(left: 12.0, top: 4.0),
  4.       child: Align(
  5.         alignment: Alignment.bottomLeft,
  6.         child: Container(
  7.           color: Colors.black,
  8.           width: 40,
  9.           height: 2,
  10.         ),
  11.       ));
  12. }
复制代码
输入框


邮箱输入框:使用正则对输入进行校验


  • TextFormField 组件:用于 Form 中的文本输入框
  • validator 属性用于对输入框进行表单校验
  • onSave 属性当表单校验通过后执行一些操作
   何时进行表单校验是 From 组件中的一个属性
  1. Widget buildEmailTextField() {
  2.   return TextFormField(
  3.     decoration: const InputDecoration(labelText: 'Email Address'),
  4.     validator: (v) {
  5.       var emailReg = RegExp(
  6.           r"[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?");
  7.       if (!emailReg.hasMatch(v!)) {
  8.         return '请输入正确的邮箱地址';
  9.       }
  10.     },
  11.     onSaved: (v) => _email = v!,
  12.   );
  13. }
复制代码
密码输入框:


  • obscureText 属性控制表单文字的显示与隐藏,我们使用自定义变量来实现点击按钮隐藏或显示密码功能
  • suffixIcon 属性是在输入框后面加一个图标,给它一个点击方法是改变是否显示密码,并更改图标的颜色
  • setState 用于通知 Flutter 框架重绘界面,此处我们利用变量 _isObscure 控制输入框密码的可见性,涉及到界面显示的更新,因此要更新 _isObscure 的代码放在 setState 中。
  1. Widget buildPasswordTextField(BuildContext context) {
  2.   return TextFormField(
  3.       obscureText: _isObscure, // 是否显示文字
  4.       onSaved: (v) => _password = v!,
  5.       validator: (v) {
  6.         if (v!.isEmpty) {
  7.           return '请输入密码';
  8.         }
  9.       },
  10.       decoration: InputDecoration(
  11.           labelText: "Password",
  12.           suffixIcon: IconButton(
  13.             icon: Icon(
  14.               Icons.remove_red_eye,
  15.               color: _eyeColor,
  16.             ),
  17.             onPressed: () {
  18.               // 修改 state 内部变量, 且需要界面内容更新, 需要使用 setState()
  19.               setState(() {
  20.                 _isObscure = !_isObscure;
  21.                 _eyeColor = (_isObscure
  22.                     ? Colors.grey
  23.                     : Theme.of(context).iconTheme.color)!;
  24.               });
  25.             },
  26.           )));
  27. }
复制代码
密码输入框下面还有一行 “忘记密码?” 的文本,利用 Align 组件将其靠右对齐
  1. Widget buildForgetPasswordText(BuildContext context) {
  2.   return Padding(
  3.     padding: const EdgeInsets.only(top: 8),
  4.     child: Align(
  5.       alignment: Alignment.centerRight,
  6.       child: TextButton(
  7.         onPressed: () {
  8.           // Navigator.pop(context);
  9.           print("忘记密码");
  10.         },
  11.         child: const Text("忘记密码?",
  12.             style: TextStyle(fontSize: 14, color: Colors.grey)),
  13.       ),
  14.     ),
  15.   );
  16. }
复制代码
登录按钮


登录按钮:


  • ElevatedButton 组件是一个常见的,点击后有波纹效果的按钮
  • 点击登录按钮后,进行表单校验
  1. Widget buildLoginButton(BuildContext context) {
  2.   return Align(
  3.     child: SizedBox(
  4.       height: 45,
  5.       width: 270,
  6.       child: ElevatedButton(
  7.         style: ButtonStyle(
  8.             // 设置圆角
  9.             shape: MaterialStateProperty.all(const StadiumBorder(
  10.                 side: BorderSide(style: BorderStyle.none)))),
  11.         child: Text('Login',
  12.             style: Theme.of(context).primaryTextTheme.headline5),
  13.         onPressed: () {
  14.           // 表单校验通过才会继续执行
  15.           if ((_formKey.currentState as FormState).validate()) {
  16.             (_formKey.currentState as FormState).save();
  17.             //TODO 执行登录方法
  18.             print('email: $_email, password: $_password');
  19.           }
  20.         },
  21.       ),
  22.     ),
  23.   );
  24. }
复制代码
其他登录方式


其他登录方式:


  • ButtonBar 组件用于构建多个按钮的排列(方向可控)
  • map 是个高阶函数,可以对 数组的每个元素进行某种操作,最后再归约成数组
  • SnackBar 是一种从底部出现的轻量级弹窗
  1. final List _loginMethod = [
  2.   {
  3.     "title": "facebook",
  4.     "icon": Icons.facebook,
  5.   },
  6.   {
  7.     "title": "google",
  8.     "icon": Icons.fiber_dvr,
  9.   },
  10.   {
  11.     "title": "twitter",
  12.     "icon": Icons.account_balance,
  13.   },
  14. ];
  15. Widget buildOtherMethod(context) {
  16.   return ButtonBar(
  17.     alignment: MainAxisAlignment.center,
  18.     children: _loginMethod
  19.         .map((item) => Builder(builder: (context) {
  20.               return IconButton(
  21.                   icon: Icon(item['icon'],
  22.                       color: Theme.of(context).iconTheme.color),
  23.                   onPressed: () {
  24.                     //TODO: 第三方登录方法
  25.                     ScaffoldMessenger.of(context).showSnackBar(
  26.                       SnackBar(
  27.                           content: Text('${item['title']}登录'),
  28.                           action: SnackBarAction(
  29.                             label: '取消',
  30.                             onPressed: () {},
  31.                           )),
  32.                     );
  33.                   });
  34.             }))
  35.         .toList(),
  36.   );
  37. }
复制代码
注册按钮

注册按钮:


  • GestureDetector 是手势检测器,用它包裹组件后可以实现对该组件的各种手势的监听,例如:“单击”、“双击”、“长按” 等。
  1. Widget buildRegisterText(context) {
  2.   return Center(
  3.     child: Padding(
  4.       padding: const EdgeInsets.only(top: 10),
  5.       child: Row(
  6.         mainAxisAlignment: MainAxisAlignment.center,
  7.         children: [
  8.           const Text('没有账号?'),
  9.           GestureDetector(
  10.             child: const Text('点击注册', style: TextStyle(color: Colors.green)),
  11.             onTap: () {
  12.               print("点击注册");
  13.             },
  14.           )
  15.         ],
  16.       ),
  17.     ),
  18.   );
  19. }
复制代码
完整源码

新建一个 Flutter 项目,替换其中 main.dart 的内容,即可运行起来。
  1. import 'package:flutter/material.dart';
  2. void main() => runApp(const MyApp());
  3. class MyApp extends StatelessWidget {
  4.   const MyApp({Key? key}) : super(key: key);
  5.   @override
  6.   Widget build(BuildContext context) {
  7.     return MaterialApp(
  8.         debugShowCheckedModeBanner: false, // 不显示右上角的 debug
  9.         title: 'Flutter Demo',
  10.         theme: ThemeData(
  11.           primarySwatch: Colors.blue,
  12.         ),
  13.         // 注册路由表
  14.         routes: {
  15.           "/": (context) => const HomePage(title: "登录"), // 首页路由
  16.         });
  17.   }
  18. }
  19. class HomePage extends StatefulWidget {
  20.   const HomePage({Key? key, required this.title}) : super(key: key);
  21.   final String title;
  22.   @override
  23.   _HomePageState createState() => _HomePageState();
  24. }
  25. class _HomePageState extends State<HomePage> {
  26.   final GlobalKey _formKey = GlobalKey<FormState>();
  27.   late String _email, _password;
  28.   bool _isObscure = true;
  29.   Color _eyeColor = Colors.grey;
  30.   final List _loginMethod = [
  31.     {
  32.       "title": "facebook",
  33.       "icon": Icons.facebook,
  34.     },
  35.     {
  36.       "title": "google",
  37.       "icon": Icons.fiber_dvr,
  38.     },
  39.     {
  40.       "title": "twitter",
  41.       "icon": Icons.account_balance,
  42.     },
  43.   ];
  44.   @override
  45.   Widget build(BuildContext context) {
  46.     return Scaffold(
  47.       body: Form(
  48.         key: _formKey, // 设置globalKey,用于后面获取FormStat
  49.         autovalidateMode: AutovalidateMode.onUserInteraction,
  50.         child: ListView(
  51.           padding: const EdgeInsets.symmetric(horizontal: 20),
  52.           children: [
  53.             const SizedBox(height: kToolbarHeight), // 距离顶部一个工具栏的高度
  54.             buildTitle(), // Login
  55.             buildTitleLine(), // Login下面的下划线
  56.             const SizedBox(height: 60),
  57.             buildEmailTextField(), // 输入邮箱
  58.             const SizedBox(height: 30),
  59.             buildPasswordTextField(context), // 输入密码
  60.             buildForgetPasswordText(context), // 忘记密码
  61.             const SizedBox(height: 60),
  62.             buildLoginButton(context), // 登录按钮
  63.             const SizedBox(height: 40),
  64.             buildOtherLoginText(), // 其他账号登录
  65.             buildOtherMethod(context), // 其他登录方式
  66.             buildRegisterText(context), // 注册
  67.           ],
  68.         ),
  69.       ),
  70.     );
  71.   }
  72.   Widget buildRegisterText(context) {
  73.     return Center(
  74.       child: Padding(
  75.         padding: const EdgeInsets.only(top: 10),
  76.         child: Row(
  77.           mainAxisAlignment: MainAxisAlignment.center,
  78.           children: [
  79.             const Text('没有账号?'),
  80.             GestureDetector(
  81.               child: const Text('点击注册', style: TextStyle(color: Colors.green)),
  82.               onTap: () {
  83.                 print("点击注册");
  84.               },
  85.             )
  86.           ],
  87.         ),
  88.       ),
  89.     );
  90.   }
  91.   Widget buildOtherMethod(context) {
  92.     return ButtonBar(
  93.       alignment: MainAxisAlignment.center,
  94.       children: _loginMethod
  95.           .map((item) => Builder(builder: (context) {
  96.                 return IconButton(
  97.                     icon: Icon(item['icon'],
  98.                         color: Theme.of(context).iconTheme.color),
  99.                     onPressed: () {
  100.                       //TODO: 第三方登录方法
  101.                       ScaffoldMessenger.of(context).showSnackBar(
  102.                         SnackBar(
  103.                             content: Text('${item['title']}登录'),
  104.                             action: SnackBarAction(
  105.                               label: '取消',
  106.                               onPressed: () {},
  107.                             )),
  108.                       );
  109.                     });
  110.               }))
  111.           .toList(),
  112.     );
  113.   }
  114.   Widget buildOtherLoginText() {
  115.     return const Center(
  116.       child: Text(
  117.         '其他账号登录',
  118.         style: TextStyle(color: Colors.grey, fontSize: 14),
  119.       ),
  120.     );
  121.   }
  122.   Widget buildLoginButton(BuildContext context) {
  123.     return Align(
  124.       child: SizedBox(
  125.         height: 45,
  126.         width: 270,
  127.         child: ElevatedButton(
  128.           style: ButtonStyle(
  129.               // 设置圆角
  130.               shape: MaterialStateProperty.all(const StadiumBorder(
  131.                   side: BorderSide(style: BorderStyle.none)))),
  132.           child: Text('Login',
  133.               style: Theme.of(context).primaryTextTheme.headline5),
  134.           onPressed: () {
  135.             // 表单校验通过才会继续执行
  136.             if ((_formKey.currentState as FormState).validate()) {
  137.               (_formKey.currentState as FormState).save();
  138.               //TODO 执行登录方法
  139.               print('email: $_email, password: $_password');
  140.             }
  141.           },
  142.         ),
  143.       ),
  144.     );
  145.   }
  146.   Widget buildForgetPasswordText(BuildContext context) {
  147.     return Padding(
  148.       padding: const EdgeInsets.only(top: 8),
  149.       child: Align(
  150.         alignment: Alignment.centerRight,
  151.         child: TextButton(
  152.           onPressed: () {
  153.             // Navigator.pop(context);
  154.             print("忘记密码");
  155.           },
  156.           child: const Text("忘记密码?",
  157.               style: TextStyle(fontSize: 14, color: Colors.grey)),
  158.         ),
  159.       ),
  160.     );
  161.   }
  162.   Widget buildPasswordTextField(BuildContext context) {
  163.     return TextFormField(
  164.         obscureText: _isObscure, // 是否显示文字
  165.         onSaved: (v) => _password = v!,
  166.         validator: (v) {
  167.           if (v!.isEmpty) {
  168.             return '请输入密码';
  169.           }
  170.         },
  171.         decoration: InputDecoration(
  172.             labelText: "Password",
  173.             suffixIcon: IconButton(
  174.               icon: Icon(
  175.                 Icons.remove_red_eye,
  176.                 color: _eyeColor,
  177.               ),
  178.               onPressed: () {
  179.                 // 修改 state 内部变量, 且需要界面内容更新, 需要使用 setState()
  180.                 setState(() {
  181.                   _isObscure = !_isObscure;
  182.                   _eyeColor = (_isObscure
  183.                       ? Colors.grey
  184.                       : Theme.of(context).iconTheme.color)!;
  185.                 });
  186.               },
  187.             )));
  188.   }
  189.   Widget buildEmailTextField() {
  190.     return TextFormField(
  191.       decoration: const InputDecoration(labelText: 'Email Address'),
  192.       validator: (v) {
  193.         var emailReg = RegExp(
  194.             r"[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?");
  195.         if (!emailReg.hasMatch(v!)) {
  196.           return '请输入正确的邮箱地址';
  197.         }
  198.       },
  199.       onSaved: (v) => _email = v!,
  200.     );
  201.   }
  202.   Widget buildTitleLine() {
  203.     return Padding(
  204.         padding: const EdgeInsets.only(left: 12.0, top: 4.0),
  205.         child: Align(
  206.           alignment: Alignment.bottomLeft,
  207.           child: Container(
  208.             color: Colors.black,
  209.             width: 40,
  210.             height: 2,
  211.           ),
  212.         ));
  213.   }
  214.   Widget buildTitle() {
  215.     return const Padding(
  216.         padding: EdgeInsets.all(8),
  217.         child: Text(
  218.           'Login',
  219.           style: TextStyle(fontSize: 42),
  220.         ));
  221.   }
  222. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

民工心事

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表