Skip to content

登录页面

起始页面

git checkout chapter-8

添加依赖

pubspec.yaml 依赖中添加 flutter_localizationsflutter_cupertino_localizations

1
2
3
4
5
6
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  flutter_cupertino_localizations: ^1.0.1

修改并保存之后会自动下载依赖

colors.dart

lib/config/colors.dart:

1
2
3
4
5
6
import 'package:flutter/material.dart';

const Color PRIMARY_COLOR = Color(0xFF50D2C2);
const Color ACCENT_COLOR = Color(0xFF50D2C2);
const Color INDICATOR_COLOR = Colors.white;
const Color BACKGROUND_COLOR = Colors.white;

main.dart

lib/main.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
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:todo/config/colors.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: PRIMARY_COLOR,
        indicatorColor: ACCENT_COLOR,
        accentColor: PRIMARY_COLOR,
      ),
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale('en'),
        const Locale('zh', 'CN'),
      ],
      home: const MyHomePage(title: 'Todo List'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(child: Text('这即将是一个 Todo App')),
    );
  }
}

搭建 UI 框架

创建登录页面文件

首先创建 lib/pages/login.dart 文件,并写入基本的文件内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import 'package:flutter/material.dart';

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('登录页面'),
      ),
    );
  }
}

然后修改 main.dart 文件中 App 的 home 属性,让 App 启动后直接展示登录页面

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import 'package:todo/pages/login.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      home: LoginPage(),
    );
  }
}

保存该文件。得益于 Flutter 的 HotReload 功能,我们无须重新编译就可以看到登录页面发生了变化,如下图所示:

搭建整体结构

登录页面的上半部分由一张图片填充,下半部分又分为三部分: 用于输入邮箱和密码的文本框、登录按钮、注册提示文字

可以利用 Column 来完成需求。我们首先利用 Column 组件将登录页面一分为二,然后利用 Expanded 组件让分割后的两部分能够铺开整个页面

 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
class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            // 利用 Expanded 组件让两个子组件都能占满整个屏幕
            Expanded(
              child: Container(
                color: Colors.blue,
                child: Center(
                  child: Text('top'),
                ),
              ),
            ),
            Expanded(
              child: Container(
                color: Colors.red,
                child: Center(
                  child: Text('bottom'),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

这里我们暂时利用容器组件来表示即将填充的内容,并用不同的颜色区分页面的不同部分。
保存 login.dart 文件,可以看到页面变成了下图这个样子:

布局文本框组件

现在我们需要为文本框等组件找到合适的布局手段
下半部分包含的三个组件在下半部分也是依次向下排布的,因此可以继续嵌套使用 Column 组件来布局这三个组件:

 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
class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            // 利用 Expanded 组件让两个子组件都能占满整个屏幕
            Expanded(
              child: Container(
                color: Colors.blue,
                child: Center(
                  child: Text('top'),
                ),
              ),
            ),
            Expanded(
              child: Container(
                color: Colors.red,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    Column(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: <Widget>[
                        Container(
                          child: Text('邮箱'),
                          color: Colors.brown,
                        ),
                        Container(
                          child: Text('密码'),
                          color: Colors.brown,
                        ),
                      ],
                    ),
                    Container(
                      child: Text('登录按钮'),
                      color: Colors.brown,
                    ),
                    Container(
                      child: Text('注册提示'),
                      color: Colors.brown,
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

当 Column 组件的 crossAxisAlignment 属性取 stretch 时,Column 自身才会尽可能地占据父组件提供的最大宽度,并且要求其所有子组件的宽度也要与自身的宽度相同。
对于其他属性值,Column 会首先要求子组件按照不超过父组件提供的最大宽度进行布局,然后自身宽度则由宽度最大的子组件决定

继续完善细节

到这里,大体的页面结构就已经设计完成了,再来关注一些细节。
我们可以利用 Padding 组件为这些组件增加边距,修改代码如下:

在 vacode 中,可以选中某个组件,例如这里的 Column 组件,然后单击左侧的小灯泡,选择 Wrap With Padding, 就可以非常方便地添加新组件

 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
class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            // 利用 Expanded 组件让两个子组件都能占满整个屏幕
            Expanded(
              child: Container(
                color: Colors.blue,
                child: Center(
                  child: Text('top'),
                ),
              ),
            ),
            Expanded(
              child: Container(
                color: Colors.red,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    Padding(
                      padding: const EdgeInsets.only(
                          left: 24, right: 24, bottom: 12),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: <Widget>[
                          Container(
                            child: Text('邮箱'),
                            color: Colors.brown,
                          ),
                          Container(
                            child: Text('密码'),
                            color: Colors.brown,
                          ),
                        ],
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.only(
                          left: 24, right: 24, bottom: 12),
                      child: Container(
                        child: Text('登录按钮'),
                        color: Colors.brown,
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.only(
                          left: 24, right: 24, bottom: 12),
                      child: Container(
                        child: Text('注册提示'),
                        color: Colors.brown,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

完成上面的修改后,登录页面中的各个组件与屏幕边缘就有了合适的距离,同时组件之间也有了合适的距离

接下来将 "注册提示" 改为 "没有账号?立即注册",这其实是由两个组件组成的,这里我们可以使用 Row 组件来对这两个组件进行布局:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Padding(
    padding: const EdgeInsets.only(
        left: 24, right: 24, bottom: 12),
    child: Container(
    child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
            Text('没有账号? '),
            Text('立即注册'),
        ],
    ),
    color: Colors.brown,
    ),
),

此时的页面如图所示:

填充组件

填充图片组件

图片资源在 assets/images 目录下。
但是此时,在我们的待办事项应用中还无法获取这张图片。原因在于我们并没有在配置文件 pubspec.yaml 中声明要将这个文件集成到我们的 App 中

打开 pubspec.yml 文件,按照如下形式添加声明:

1
2
3
4
5
6
7
8
9
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  assets:
    - assets/images/

保存文件,可以看到已经自动执行了 flutter pub get 命令。
然后我们将之前添加的文字替换为图片组件 Image,并将背景色调整为默认的白色

1
2
3
4
5
6
7
Expanded(
    child: Container(
        child: Center(
            child: Image.asset('assets/images/mark.png'),
        ),
    ),
),

由于我们此次的改动新增加了图片,因此需要执行一次 Hot Restart 让此修改生效。
如果你使用的是 VS Code,那么请打开命令界面 (View -> Command Palette...),然后在弹出的输入框中输入 hot restart 来执行 HotRestart

执行 HotRestart 后,我们发现图片看起来比我们预想的要大不少

这是为什么呢? 因为在默认情况下,Image 组件展示图片时会使用输入图片的实际大小。
由于我们这里设置的图片分辨率比较高,因此图片基本占据了整个页面的上半部分。遇到这种问题,我们需要给 Image 组件指定一个宽高

指定宽高有两种方式,一种是通过 Image 组件的 width 和 height 属性强行指定宽高的值,这种方式虽然简单粗暴,但是需要我们不断地尝试,去寻找一个合适的宽高大小,并且对于不同大小的屏幕没有良好的适配性。

另一种方式是我们给这个组件的外层包裹一个容器,这个容器能够约束这个图片组件的布局方式,让这个图片以我们约束的布局方式布局。

所幸,Flutter 为我们提供了 FractionallySizedBox 组件,让我们能够按照百分比来布局组件。
通过传入的 widthFactor 和 heightFactor 参数,我们可以让 Image 组件按照父容器(这里是 Center 组件)的大小来布局。
下面是实现过程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Expanded(
    child: Container(
        color: Colors.white,
        child: Center(
            child: FractionallySizedBox(
                child: Image.asset('assets/images/mark.png'),
                widthFactor: 0.4,
                heightFactor: 0.4,
            ),
        ),
    ),
),

此时的页面如图所示:

填充邮箱和密码输入框

接下来,继续填充我们的页面。首先将原本的文本框替换成最基本的 TextField,并为 TextField 增加装饰器,补充上作为提示的 hintText:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Padding(
    padding: const EdgeInsets.only(
        left: 24, right: 24, bottom: 12),
    child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
            TextField(
                decoration: InputDecoration(
                    hintText: '请输入邮箱',
                    labelText: '邮箱',
                ),
            ),
            TextField(
                decoration: InputDecoration(
                    hintText: '请输入六位以上的密码',
                    labelText: '密码',
                ),
            ),
        ],
    ),
),

除此之外,由于第二个输入框是密码框,因此还需要将这个输入框的输入内容设置为不展示:

1
2
3
4
5
6
7
TextField(
    decoration: InputDecoration(
        hintText: '请输入六位以上的密码',
        labelText: '密码',
    ),
    obscureText: true,
)

如此一来,输入密码框的文本就会被 *** 代替 。

登录按钮与注册提示按钮

我们来继续完善页面下方的登录按钮。
登录按钮比较简单,只需要为 Text 外部包裹上一个合适样式的 TextButton 即可

首先在登录文本的外部增加一个 TextButton,并为其设置对应的颜色,其中 R 代表红色值、G 代表绿色值、B 代表蓝色值,这三个参数的取值范围都是 0 到 255;
O 代表透明度,取值为 0 标识完全透明,为 1 表示完全不透明。
与此同时,我们也把我们页面下半部分的红色背景色移除,让它能够展示默认的背景色:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Padding(
    padding: const EdgeInsets.only(
        left: 24, right: 24, bottom: 12),
    child: TextButton(
        child:
            Text('登录按钮', style: TextStyle(color: Colors.white)),
        onPressed: () {},
        style: TextButton.styleFrom(
            backgroundColor: Color.fromRGBO(69, 202, 181, 1),
        ),
    ),
),

此时页面看起来就是这样的:

最后,我们再为下方的 "立即注册" 文本包裹一个 InkWell(TextButton 默认有内边距,无法消除)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
child: Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
        Text('没有账号? '),
        InkWell(
            child: Text('立即注册'),
            onTap: () {},
        ),
    ],
),

为页面增加本地逻辑

处理焦点

一般而言,我们使用的 App 都是在单击键盘以外的空白区域后,就会让键盘收回去,而我们此时的待办事项应用并没有这个功能,必须要单击键盘下方的按键才能让键盘收回去。
下面基于此,继续优化我们的应用。

在 Flutter 中,TextField 的焦点事件可以通过 FocusNode 控制。
使用 FocusNode 之前,首先需要将 "登录" 页面转为 StatefulWidget 类型,转换方式也很简单,依旧是利用我们之前提过的小灯泡,
选中 LoginPage,然后单击小灯泡中的 Cover Widget to StatefulWidget,IDE 就会自动帮我们生成好所有需要的代码

1
2
3
4
5
6
7
8
9
class LoginPage extends StatefulWidget {
  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  @override
  Widget build(BuildContext context) { ... }
}

然后就可以通过 FocusNode 控制键盘的展示和隐藏了:

  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
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
class _LoginPageState extends State<LoginPage> {
  FocusNode emailFocusNode = FocusNode();
  FocusNode passowrdFocusNode = FocusNode();

  @override
  void initState() {
    super.initState();
    emailFocusNode = FocusNode();
    passowrdFocusNode = FocusNode();
  }

  @override
  void dispose() {
    super.dispose();
    emailFocusNode.dispose();
    passowrdFocusNode.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        emailFocusNode.unfocus();
        passowrdFocusNode.unfocus();
        FocusManager.instance.primaryFocus?.unfocus();
      },
      child: Scaffold(
        body: Center(
          child: Column(
            children: <Widget>[
              // 利用 Expanded 组件让两个子组件都能占满整个屏幕
              Expanded(
                child: Container(
                  color: Colors.white,
                  child: Center(
                    child: FractionallySizedBox(
                      child: Image.asset('assets/images/mark.png'),
                      widthFactor: 0.4,
                      heightFactor: 0.4,
                    ),
                  ),
                ),
              ),
              Expanded(
                child: Center(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      Padding(
                        padding: const EdgeInsets.only(
                            left: 24, right: 24, bottom: 12),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.stretch,
                          children: <Widget>[
                            TextField(
                              decoration: InputDecoration(
                                hintText: '请输入邮箱',
                                labelText: '邮箱',
                              ),
                              focusNode: emailFocusNode,
                              textInputAction: TextInputAction.next,
                            ),
                            TextField(
                              decoration: InputDecoration(
                                hintText: '请输入六位以上的密码',
                                labelText: '密码',
                              ),
                              focusNode: passowrdFocusNode,
                              obscureText: true,
                            ),
                          ],
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(
                            left: 24, right: 24, bottom: 12),
                        child: TextButton(
                          child: Text('登录按钮',
                              style: TextStyle(color: Colors.white)),
                          onPressed: () {},
                          style: TextButton.styleFrom(
                            backgroundColor: Color.fromRGBO(69, 202, 181, 1),
                          ),
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(
                            left: 24, right: 24, bottom: 12),
                        child: Container(
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: <Widget>[
                              Text('没有账号? '),
                              InkWell(
                                child: Text('立即注册'),
                                onTap: () {},
                              ),
                            ],
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

当有多个 TextField 时,我们也可以通过给 TextField 设置 textInputAction 的方式来控制手机键盘右下角按钮的类型,
当 textInputAction 的取值为 next 时,单击键盘右下角的按钮就会将焦点转移到下一个 TextField

为文本输入框增加校验逻辑

现在我们还需要为输入框增加校验逻辑。只有当输入的邮箱和密码都通过输入条件以后,登录按钮才可以被单击。
在这里,我们希望邮箱文本框中的输入内容包含 @ 字符,同时密码输入框的输入内容长度需要大于六位。
最简单的实现方法就是直接在 TextField 的回调方法中进行判断

接下来,我们在 _LoginPageState 中定义一个布尔变量,我们会使用这个布尔变量来表示当前是否可以让登录页面中的登录按钮可以被单击。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Padding(
    padding: const EdgeInsets.only(
        left: 24, right: 24, bottom: 12),
    child: TextButton(
        child:
            Text('登录', style: TextStyle(color: Colors.white)),
        onPressed: canLogin ? () {} : null,
        style: TextButton.styleFrom(
        backgroundColor: Color.fromRGBO(69, 202, 181, 1),
        disabledBackgroundColor:
            Color.fromRGBO(69, 202, 160, 0.5),
        ),
    ),
),

此时我们已经可以用一个变量来控制登录按钮是否可以被单击了。之后我们还要更改这个变量。
使用 TextField 的文本监听回调方法,每当输入框的文本发生变化时,就能获取到当前两个输入框中的文本,然后按照我们之前提到的规则进行校验。
同时,我们可以使用 TextEditingController 来获取两个输入框中当前包含的文本内容。具体代码如下:

 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
class _LoginPageState extends State<LoginPage> {
  FocusNode emailFocusNode = FocusNode();
  FocusNode passowrdFocusNode = FocusNode();
  bool canLogin = false;

  TextEditingController _emailController = TextEditingController();
  TextEditingController _passwordController = TextEditingController();

  void _checkInputValid(String _) {
    // 这里的参数写成 _ 表示在这里我们没有使用这个参数,这是一种比较约定俗成的写法
    bool isInputValid = _emailController.text.contains('@') &&
        _passwordController.text.length >= 6;
    if (isInputValid == canLogin) {
      return;
    }
    setState(() {
      canLogin = isInputValid;
    });
  }
  ...

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      ...
      child: Scaffold(
        body: Center(
          child: Column(
            children: <Widget>[
              // 利用 Expanded 组件让两个子组件都能占满整个屏幕
              ...
              Expanded(
                child: Center(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      Padding(
                        padding: const EdgeInsets.only(
                            left: 24, right: 24, bottom: 12),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.stretch,
                          children: <Widget>[
                            TextField(
                              decoration: InputDecoration(
                                hintText: '请输入邮箱',
                                labelText: '邮箱',
                              ),
                              focusNode: emailFocusNode,
                              textInputAction: TextInputAction.next,
                              onChanged: _checkInputValid,
                              controller: _emailController,
                            ),
                            TextField(
                              decoration: InputDecoration(
                                hintText: '请输入六位以上的密码',
                                labelText: '密码',
                              ),
                              focusNode: passowrdFocusNode,
                              obscureText: true,
                              onChanged: _checkInputValid,
                              controller: _passwordController,
                            ),
                          ],
                        ),
                      ),
                      ...,
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

将此代码保存,我们就可以看到一开始的登录按钮是不能被单击的,只有在输入框中输入符合规则的内容后,登录按钮才可以被单击

小结

最后代码:

  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
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import 'package:flutter/material.dart';

class LoginPage extends StatefulWidget {
  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  FocusNode emailFocusNode = FocusNode();
  FocusNode passowrdFocusNode = FocusNode();
  bool canLogin = false;

  TextEditingController _emailController = TextEditingController();
  TextEditingController _passwordController = TextEditingController();

  void _checkInputValid(String _) {
    // 这里的参数写成 _ 表示在这里我们没有使用这个参数,这是一种比较约定俗成的写法
    bool isInputValid = _emailController.text.contains('@') &&
        _passwordController.text.length >= 6;
    if (isInputValid == canLogin) {
      return;
    }
    setState(() {
      canLogin = isInputValid;
    });
  }

  @override
  void initState() {
    super.initState();
    canLogin = false;
    emailFocusNode = FocusNode();
    passowrdFocusNode = FocusNode();
  }

  @override
  void dispose() {
    super.dispose();
    emailFocusNode.dispose();
    passowrdFocusNode.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        emailFocusNode.unfocus();
        passowrdFocusNode.unfocus();
        FocusManager.instance.primaryFocus?.unfocus();
      },
      child: Scaffold(
        body: Center(
          child: Column(
            children: <Widget>[
              // 利用 Expanded 组件让两个子组件都能占满整个屏幕
              Expanded(
                child: Container(
                  color: Colors.white,
                  child: Center(
                    child: FractionallySizedBox(
                      child: Image.asset('assets/images/mark.png'),
                      widthFactor: 0.4,
                      heightFactor: 0.4,
                    ),
                  ),
                ),
              ),
              Expanded(
                child: Center(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      Padding(
                        padding: const EdgeInsets.only(
                            left: 24, right: 24, bottom: 12),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.stretch,
                          children: <Widget>[
                            TextField(
                              decoration: InputDecoration(
                                hintText: '请输入邮箱',
                                labelText: '邮箱',
                              ),
                              focusNode: emailFocusNode,
                              textInputAction: TextInputAction.next,
                              onChanged: _checkInputValid,
                              controller: _emailController,
                            ),
                            TextField(
                              decoration: InputDecoration(
                                hintText: '请输入六位以上的密码',
                                labelText: '密码',
                              ),
                              focusNode: passowrdFocusNode,
                              obscureText: true,
                              onChanged: _checkInputValid,
                              controller: _passwordController,
                            ),
                          ],
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(
                            left: 24, right: 24, bottom: 12),
                        child: TextButton(
                          child:
                              Text('登录', style: TextStyle(color: Colors.white)),
                          onPressed: canLogin ? () {} : null,
                          style: TextButton.styleFrom(
                            backgroundColor: Color.fromRGBO(69, 202, 181, 1),
                            disabledBackgroundColor:
                                Color.fromRGBO(69, 202, 160, 0.5),
                          ),
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(
                            left: 24, right: 24, bottom: 12),
                        child: Container(
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: <Widget>[
                              Text('没有账号? '),
                              InkWell(
                                child: Text('立即注册'),
                                onTap: () {},
                              ),
                            ],
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}