本节目标

  • 静态路由
  • 带阴影的椭圆图标
  • 输入有效性校验
  • 组件抽取方法
  • 通用组件、业务组件
  • 程序目录组织
  • 抽取透明导航栏
  • toast 提示组件

视频

代码

https://github.com/ducafecat/flutter_learn_news/releases/tag/v1.0.3

1 静态路由

1.1 定义静态路由

  • 登录页 lib/pages/sign_in/sign_in.dart

  • 注册页 lib/pages/sign_up/sign_up.dart

  • 静态路由 lib/routes.dart

1
2
3
4
5
6
7
8
import 'package:flutter_ducafecat_news/pages/sign_in/sign_in.dart';
import 'package:flutter_ducafecat_news/pages/sign_up/sign_up.dart';

/// 静态路由
var staticRoutes = {
"/sign-in": (context) => SignInPage(), // 登录
"/sign-up": (context) => SignUpPage(), // 注册
};

1.2 注册静态路由

  • lib/main.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'package:flutter/material.dart';
import 'package:flutter_ducafecat_news/pages/welcome/welcome.dart';
import 'package:flutter_ducafecat_news/routes.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ducafecat.tech',
home: WelcomePage(),
routes: staticRoutes,
debugShowCheckedModeBanner: false,
);
}
}

2 登录界面

2.1 维护色彩常量

  • lib/common/values/colors.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import 'dart:ui';

class AppColors {
/// 主背景 白色
static const Color primaryBackground = Color.fromARGB(255, 255, 255, 255);

/// 主文本 灰色
static const Color primaryText = Color.fromARGB(255, 45, 45, 47);

/// 主控件-背景 蓝色
static const Color primaryElement = Color.fromARGB(255, 41, 103, 255);

/// 主控件-文本 白色
static const Color primaryElementText = Color.fromARGB(255, 255, 255, 255);

// *****************************************

/// 第二种控件-背景色 淡灰色
static const Color secondaryElement = Color.fromARGB(255, 246, 246, 246);

/// 第二种控件-文本 浅蓝色
static const Color secondaryElementText = Color.fromARGB(255, 41, 103, 255);

// *****************************************

/// 第三种控件-背景色 石墨色
static const Color thirdElement = Color.fromARGB(255, 45, 45, 47);
}

2.2 程序结构

  • lib/pages/sign_in/sign_in.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import 'package:flutter/material.dart';
import 'package:flutter_ducafecat_news/common/utils/utils.dart';
import 'package:flutter_ducafecat_news/common/values/values.dart';
import 'package:flutter_ducafecat_news/common/widgets/widgets.dart';

class SignInPage extends StatefulWidget {
SignInPage({Key key}) : super(key: key);

@override
_SignInPageState createState() => _SignInPageState();
}

class _SignInPageState extends State<SignInPage> {

// logo
Widget _buildLogo() {
return Container();
}

// 登录表单
Widget _buildInputForm() {
return Container();
}

// 第三方登录
Widget _buildThirdPartyLogin() {
return Container();
}

// 注册按钮
Widget _buildSignupButton() {
return Container();
}

@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Center(
child: Column(
children: <Widget>[
_buildLogo(),
_buildInputForm(),
Spacer(),
_buildThirdPartyLogin(),
_buildSignupButton(),
],
),
),
);
}
}

2.3 画带阴影的椭圆图标

  • lib/pages/sign_in/sign_in.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// logo
Widget _buildLogo() {
return Container(
width: duSetWidth(110),
margin: EdgeInsets.only(top: duSetHeight(40 + 44.0)), // 顶部系统栏 44px
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
height: duSetWidth(76),
width: duSetWidth(76),
margin: EdgeInsets.symmetric(horizontal: duSetWidth(15)),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
left: 0,
top: 0,
right: 0,
child: Container(
height: duSetWidth(76),
decoration: BoxDecoration(
color: AppColors.primaryBackground,
boxShadow: [
Shadows.primaryShadow,
],
borderRadius: BorderRadius.all(
Radius.circular(duSetWidth(76 * 0.5))), // 父容器的50%
),
child: Container(),
),
),
Positioned(
top: duSetWidth(13),
child: Image.asset(
"assets/images/logo.png",
fit: BoxFit.none,
),
),
],
),
),
Container(
margin: EdgeInsets.only(top: duSetHeight(15)),
child: Text(
"SECTOR",
textAlign: TextAlign.center,
style: TextStyle(
color: AppColors.primaryText,
fontFamily: "Montserrat",
fontWeight: FontWeight.w600,
fontSize: duSetFontSize(24),
height: 1,
),
),
),
Text(
"news",
textAlign: TextAlign.center,
style: TextStyle(
color: AppColors.primaryText,
fontFamily: "Avenir",
fontWeight: FontWeight.w400,
fontSize: duSetFontSize(16),
height: 1,
),
),
],
),
);
}

2.4 抽取输入框

  • lib/common/widgets/input.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/// 输入框
Widget inputTextEdit({
@required TextEditingController controller,
TextInputType keyboardType = TextInputType.text,
String hintText,
bool isPassword = false,
double marginTop = 15,
}) {
return Container(
height: duSetHeight(44),
margin: EdgeInsets.only(top: duSetHeight(marginTop)),
decoration: BoxDecoration(
color: AppColors.secondaryElement,
borderRadius: Radii.k6pxRadius,
),
child: TextField(
controller: controller,
keyboardType: keyboardType,
decoration: InputDecoration(
hintText: hintText,
contentPadding: EdgeInsets.fromLTRB(20, 10, 0, 9),
border: InputBorder.none,
),
style: TextStyle(
color: AppColors.primaryText,
fontFamily: "Avenir",
fontWeight: FontWeight.w400,
fontSize: duSetFontSize(18),
),
maxLines: 1,
autocorrect: false, // 自动纠正
obscureText: isPassword, // 隐藏输入内容, 密码框
),
);
}

2.5 抽取扁平按钮

  • lib/common/widgets/button.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/// 扁平圆角按钮
Widget btnFlatButtonWidget({
@required VoidCallback onPressed,
double width = 140,
double height = 44,
Color gbColor = AppColors.primaryElement,
String title = "button",
Color fontColor = AppColors.primaryElementText,
double fontSize = 18,
String fontName = "Montserrat",
FontWeight fontWeight = FontWeight.w400,
}) {
return Container(
width: duSetWidth(width),
height: duSetHeight(height),
child: FlatButton(
onPressed: onPressed,
color: gbColor,
shape: RoundedRectangleBorder(
borderRadius: Radii.k6pxRadius,
),
child: Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
color: fontColor,
fontFamily: fontName,
fontWeight: fontWeight,
fontSize: duSetFontSize(fontSize),
height: 1,
),
),
),
);
}

2.6 抽取社交登录按钮

  • lib/common/widgets/button.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// 第三方按钮
Widget btnFlatButtonBorderOnlyWidget({
@required VoidCallback onPressed,
double width = 88,
double height = 44,
String iconFileName,
}) {
return Container(
width: duSetWidth(width),
height: duSetHeight(height),
child: FlatButton(
onPressed: onPressed,
shape: RoundedRectangleBorder(
side: Borders.primaryBorder,
borderRadius: Radii.k6pxRadius,
),
child: Image.asset(
"assets/images/icons-$iconFileName.png",
),
),
);
}

2.7 封装 toast 提示框

  • lib/common/widgets/toast.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Future<bool> toastInfo({
@required String msg,
Color backgroundColor = Colors.black,
Color textColor = Colors.white,
}) async {
return await Fluttertoast.showToast(
msg: msg,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
timeInSecForIos: 1,
backgroundColor: backgroundColor,
textColor: textColor,
fontSize: duSetFontSize(16),
);
}

2.8 数据有效性检验

  • lib/pages/sign_in/sign_in.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
...
class _SignInPageState extends State<SignInPage> {

//email的控制器
final TextEditingController _emailController = TextEditingController();
//密码的控制器
final TextEditingController _passController = TextEditingController();
...

// 执行登录操作
_handleSignIn() {
if (!duIsEmail(_emailController.value.text)) {
toastInfo(msg: '请正确输入邮件');
return;
}
if (!duCheckStringLength(_passController.value.text, 6)) {
toastInfo(msg: '密码不能小于6位');
return;
}
}

...

// 登录表单
Widget _buildInputForm() {
return Container(
width: duSetWidth(295),
// height: 204,
margin: EdgeInsets.only(top: duSetHeight(49)),
child: Column(
children: [
// email input
inputTextEdit(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
hintText: "Email",
marginTop: 0,
),
// password input
inputTextEdit(
controller: _passController,
keyboardType: TextInputType.visiblePassword,
hintText: "Password",
isPassword: true,
),

// 注册、登录 横向布局
Container(
height: duSetHeight(44),
margin: EdgeInsets.only(top: duSetHeight(15)),
child: Row(
children: [
// 注册
btnFlatButtonWidget(
onPressed: _handleNavSignUp,
gbColor: AppColors.thirdElement,
title: "Sign up",
),
Spacer(),
// 登录
btnFlatButtonWidget(
onPressed: _handleSignIn,
gbColor: AppColors.primaryElement,
title: "Sign in",
),
],
),
),
// Spacer(),

// Fogot password
Container(
height: duSetHeight(22),
margin: EdgeInsets.only(top: duSetHeight(20)),
child: FlatButton(
onPressed: () => {},
child: Text(
"Fogot password?",
textAlign: TextAlign.center,
style: TextStyle(
color: AppColors.secondaryElementText,
fontFamily: "Avenir",
fontWeight: FontWeight.w400,
fontSize: duSetFontSize(16),
height: 1, // 设置下行高,否则字体下沉
),
),
),
),
],
),
);
}

3 注册界面

3.1 程序结构

  • lib/pages/sign_up/sign_up.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import 'package:flutter/material.dart';
import 'package:flutter_ducafecat_news/common/utils/utils.dart';
import 'package:flutter_ducafecat_news/common/values/values.dart';
import 'package:flutter_ducafecat_news/common/widgets/widgets.dart';

class SignUpPage extends StatefulWidget {
SignUpPage({Key key}) : super(key: key);

@override
_SignUpPageState createState() => _SignUpPageState();
}

class _SignUpPageState extends State<SignUpPage> {

// logo
Widget _buildLogo() {
return Container();
}

// 注册表单
Widget _buildInputForm() {
return Container();
}

// 第三方
Widget _buildThirdPartyLogin() {
return Container();
}

// 有账号
Widget _buildHaveAccountButton() {
return Container();
}

@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
...,
body: Center(
child: Column(
children: <Widget>[
Divider(height: 1),
_buildLogo(),
_buildInputForm(),
Spacer(),
_buildThirdPartyLogin(),
_buildHaveAccountButton(),
],
),
),
);
}
}

3.2 透明导航栏

  • lib/common/widgets/app.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// 透明背景 AppBar
Widget transparentAppBar({
@required BuildContext context,
List<Widget> actions,
}) {
return AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text(''),
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: AppColors.primaryText,
),
onPressed: () {
Navigator.pop(context);
},
),
actions: actions,
);
}
  • lib/pages/sign_up/sign_up.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: transparentAppBar(
context: context,
actions: <Widget>[
IconButton(
icon: Icon(
Icons.info_outline,
color: AppColors.primaryText,
),
onPressed: () {
toastInfo(msg: '这是注册界面');
},
)
],
),

3.2 注册表单

  • lib/pages/sign_up/sign_up.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

// 注册表单
Widget _buildInputForm() {
return Container(
width: duSetWidth(295),
// height: 204,
margin: EdgeInsets.only(top: duSetHeight(49)),
child: Column(
children: [
// fullName input
inputTextEdit(
controller: _fullnameController,
keyboardType: TextInputType.text,
hintText: "Full name",
marginTop: 0,
),
// email input
inputTextEdit(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
hintText: "Email",
),
// password input
inputTextEdit(
controller: _passController,
keyboardType: TextInputType.visiblePassword,
hintText: "Password",
isPassword: true,
),

// 创建
Container(
height: duSetHeight(44),
margin: EdgeInsets.only(top: duSetHeight(15)),
child: btnFlatButtonWidget(
onPressed: () {
if (!duCheckStringLength(_fullnameController.value.text, 5)) {
toastInfo(msg: '用户名不能小于5位');
return;
}
if (!duIsEmail(_emailController.value.text)) {
toastInfo(msg: '请正确输入邮件');
return;
}
if (!duCheckStringLength(_passController.value.text, 6)) {
toastInfo(msg: '密码不能小于6位');
return;
}
Navigator.pop(context);
},
width: 295,
fontWeight: FontWeight.w600,
title: "Create an account",
),
),
// Spacer(),

// Fogot password
Container(
height: duSetHeight(22),
margin: EdgeInsets.only(top: duSetHeight(20)),
child: FlatButton(
onPressed: _handleSignUp,
child: Text(
"Fogot password?",
textAlign: TextAlign.center,
style: TextStyle(
color: AppColors.secondaryElementText,
fontFamily: "Avenir",
fontWeight: FontWeight.w400,
fontSize: duSetFontSize(16),
height: 1, // 设置下行高,否则字体下沉
),
),
),
),
],
),
);
}

3.3 检验有效性

  • lib/pages/sign_up/sign_in.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 执行注册操作
_handleSignUp() {
if (!duCheckStringLength(_fullnameController.value.text, 5)) {
toastInfo(msg: '用户名不能小于5位');
return;
}
if (!duIsEmail(_emailController.value.text)) {
toastInfo(msg: '请正确输入邮件');
return;
}
if (!duCheckStringLength(_passController.value.text, 6)) {
toastInfo(msg: '密码不能小于6位');
return;
}
Navigator.pop(context);
}

3.4 社交按钮

  • lib/pages/sign_up/sign_up.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 第三方
Widget _buildThirdPartyLogin() {
return Container(
width: duSetWidth(295),
margin: EdgeInsets.only(bottom: duSetHeight(40)),
child: Column(
children: <Widget>[
// title
Text(
"Or sign in with social networks",
textAlign: TextAlign.center,
style: TextStyle(
color: AppColors.primaryText,
fontFamily: "Avenir",
fontWeight: FontWeight.w400,
fontSize: duSetFontSize(16),
),
),
// 按钮
Padding(
padding: EdgeInsets.only(top: duSetHeight(20)),
child: Row(
children: <Widget>[
btnFlatButtonBorderOnlyWidget(
onPressed: () {},
width: 88,
iconFileName: "twitter",
),
Spacer(),
btnFlatButtonBorderOnlyWidget(
onPressed: () {},
width: 88,
iconFileName: "google",
),
Spacer(),
btnFlatButtonBorderOnlyWidget(
onPressed: () {},
width: 88,
iconFileName: "facebook",
),
],
),
),
],
),
);
}

3.5 返回按钮

  • lib/pages/sign_up/sign_up.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 返回上一页
_handleNavPop() {
Navigator.pop(context);
}

Widget _buildHaveAccountButton() {
return Container(
margin: EdgeInsets.only(bottom: duSetHeight(20)),
child: btnFlatButtonWidget(
onPressed: _handleNavPop,
width: 294,
gbColor: AppColors.secondaryElement,
fontColor: AppColors.primaryText,
title: "I have an account",
fontWeight: FontWeight.w500,
fontSize: 16,
),
);
}

蓝湖设计稿

https://lanhuapp.com/url/lYuz1
密码: gSKl

蓝湖现在收费了,所以查看标记还请自己上传 xd 设计稿
商业设计稿文件不好直接分享, 可以加微信联系 ducafecat

参考

视频


© 猫哥

https://ducafecat.tech