笔记-Flutter应用启动闪屏到登录界面

2,352 阅读5分钟

demo传送门

闪屏

先看下闪屏动画的代码:

import 'package:flutter/material.dart';
import 'login_screen.dart';

class SplashScreen extends StatefulWidget {
  @override
  _SplashScreenState createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> with SingleTickerProviderStateMixin {

  AnimationController _controller;
  Animation _animation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(vsync: this, duration: Duration(milliseconds: 3000));
    _animation = Tween(begin: 0.0, end: 1.0).animate(_controller);
    
    /* 动画事件监听器
    监听动画的执行状态,这里监听动画结束的状态,如果结束则执行页面跳转
     */
    _animation.addStatusListener((status){
      if (status == AnimationStatus.completed) {
        Navigator.of(context).pushAndRemoveUntil(
          MaterialPageRoute(builder: (context) => LoginView()),
        (route) => route==null
        );
      }
    });
    // 播放动画
    _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return FadeTransition( // 透明动画组件
      opacity: _animation, // 执行动画
      child: Image.network(
        'http://img.ph.126.net/L-kWzHFAymXFUvgauDtB-g==/2596888135149416738.jpg',
        scale: 2.0, // 缩放
        fit: BoxFit.cover,  // 充满容器
      ),
    );
  }
}

AnimationContrllerAnimation的一个子类,可以用它来控制动画,比如执行时间。 设置好动画,就设置一个动画事件的监听器animation.addStatusListener,它可以监听到动画的执行状态,这里监听到动画结束,执行页面跳转动作。

关于图片的设置,以及路由的跳转,在后面都有详细的解释。

登录界面

这是一个很简单的登录界面,先看下页面里主要都有什么。

  • 图片logo
  • 上下两个输入框
  • 一个登录按钮

这边了解过Flutter的小伙伴们,看到了这个布局一下子就能够想得到Column,下面看主体代码

import 'package:flutter/material.dart';
import 'home_page.dart';

class LoginView extends StatefulWidget {
  @override
  _LoginViewState createState() => _LoginViewState();
}

class _LoginViewState extends State<LoginView> {

  var _photoController = TextEditingController();
  var _passController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(10.0),
              child: Image.asset('images/logo.jpg'),
            ),
            AccountTextField(),
            PasswordTextField(),
            LoginButton(),
          ],
        ),
      ),
    );
  }
  
  // 账号TextField
  Widget AccountTextField() {
    return Padding(
        padding: const EdgeInsets.fromLTRB(15.0, 30.0, 15.0, 0.0),
        child: TextField(
          controller: _photoController,
          onChanged: (text) { // 内容变化
            print('输入了$text');
          },
          decoration: InputDecoration(
              prefixIcon: Icon(Icons.phone_iphone),
              fillColor: Colors.white70,
              filled: true,
              labelText: '请输入手机号'
          ),
        )
    );
  }
  
  // 密码TextField
  Widget PasswordTextField() {
    return Padding(
        padding: const EdgeInsets.fromLTRB(15.0, 10.0, 15.0, 0.0),
        child: TextField(
          controller: _passController,
          decoration: InputDecoration(
              prefixIcon: Icon(Icons.email),
              fillColor: Colors.white70,
              filled: true,
              labelText: '请输入密码'
          ),
          obscureText: true,
        )
    );
  }
  
  // 登录按钮
  Widget LoginButton() {
    return GestureDetector(
      child: Container(
        margin: const EdgeInsets.fromLTRB(0.0, 30.0, 0.0, 0.0),
        width: MediaQuery.of(context).size.width - 30.0,
        height: 50.0,
        child: Card(
          color: Colors.lightBlue,
          elevation: 6.0,
          child: Center(
            child: Text('登录', style: TextStyle(fontSize: 18.0, color: Colors.white)),
          )
        )
      ),
      onTap: (){
        Navigator.of(context).pushAndRemoveUntil(
            MaterialPageRoute(builder: (context) => HomePage()),
                (route) => route==null
        );
      },
    );
  }
  
}

说明一下布局:

  • 主体引入一个Column Widget
  • 第一个子Widget是一个logo图片,它被Padding Widget包裹着,我们可以通过Paddingpadding属性,轻松的控制这个Widget位置和边距
  • 第二个和第三个子Widget是一个TextField Widget,和上面的图片一样,通过Padding Widget包裹着,很容易控制它们的边距
  • 第四个子Widget用的是一个Container,然后里面通过GestureDetector添加了点击事件,其实这里的登录按钮,完全可以通过button性质的Widget来做,这里只是想记录一下,给控件添加手势的功能

下面简单介绍一下几个简单的Widget

图片Widget

本地图片通过 pubspec.yaml 文件(位于项目根目录),来识别应用程序所用的图片

在根目录下新建一个images文件夹,存放图片使用

assets:
    - images/logo.png
    - images/background.png

代码中可以直接引用,和iOS开发中的使用类似:Image.asset('images/logo.jpg')

网络图片的加载,看一下加载网络图片的方法Image.network('图片链接'),也很简单。

TextField Widget

最简单的输入框TextField(),只要这一句话,输入框其实就有了

通过项目里的代码可以看到,TextField里有个controller属性,通过这个来获取TextField的值:controller.text

做iOS的小伙伴们都知道iOS中的TextField属性中有个placeholder属性,那么这里可以通过decoration里的labelText设置这个值。

一般的密码输入框,我们都是隐式显示,也就是▪️▪️▪️▪️来表示。obscureTexts设置为true即可。

还可以在左侧添加一个icon,代码中也可以看到用法。

有兴趣的小伙伴可以通过源码里的属性值,来了解这个Widget

本人项目中使用了很多圆角矩形的边框,这里有点坑,下面看下代码:

import 'package:flutter/material.dart';

class TextFieldView extends StatelessWidget {
  Widget borderTextField() {
    return TextField(
      decoration: InputDecoration(
        contentPadding: EdgeInsets.all(15.0),
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(8.0),
          borderSide: BorderSide(color: Colors.red, width: 10.0, style: BorderStyle.solid)
        )
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('TextField')),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(15.0),
          child: borderTextField(),
        ),
      ),
    );
  }
}

效果图:

可能,细心的小伙伴们已经发现了问题
borderSide: BorderSide(color: Colors.red, width: 10.0, style: BorderStyle.solid),这句代码对边框的设置毫无作用,这里的输入框,并不是我们想要的

好在坑已被前人填满,:边框颜色解决方案

所以正确的代码姿势应该是:

Widget borderTextField() {
    return Theme(
      data: ThemeData(primaryColor: Colors.red, hintColor: Colors.blue),
      child: TextField(
        decoration: InputDecoration(
            contentPadding: EdgeInsets.all(15.0),
            border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8.0),
                // 下面方法无效
//                borderSide: BorderSide(color: Colors.red, width: 10.0, style: BorderStyle.solid)
            )
        ),
      ),
    );
  }

效果图:

再者就是边框的粗细改变,暂时没有找到合适的方法,但是可以通过重构的方式,请看代码:

import 'package:flutter/material.dart';

class TextFieldView extends StatelessWidget {

  Widget borderTextField() {
    return Container(
      padding: const EdgeInsets.all(5.0),
      height: 40.0,
      decoration: BoxDecoration(
        color: Colors.white70,
        border: Border.all(color: Colors.yellow, width: 5.0),
        borderRadius: BorderRadius.circular(8.0)
      ),
      child: TextFormField(
        decoration: InputDecoration.collapsed(hintText: '请输入内容'),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('TextField')),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(15.0),
          child: borderTextField(),
        ),
      ),
    );
  }
}

效果图:

页面跳转及路由

Navigator

Navigator继承StatefulWidget,它也是小组件,有很多相关静态函数,可以帮助我们达到页面跳转和数据交互的功能:

  • push 将设置的route信息推送到Navigator上,实现页面跳转
  • of 主要是获取Navigator最近实力的好状态
  • pop 返回到上个页面
  • canPop 判断是否可以导航到新页面
  • popAndPushNamed 指定一个路由路径,并导航到新页面
  • popUntil 反复执行pop知道该函数的参数predicate返回true为止
  • pushAndRemoveUntil 将给定路由推送到Navigator,删除先前的路由,直到该函数的参数predicate返回true为止
  • pushNamed 将命名路由推送到Navigator
  • pushNamedAndRemoveUntil 将命名路由推送到Navigator,删除先前的路由,直到该函数的参数predicate返回true为止。
  • pushReplacement 路由替换。
  • pushReplacementNamed 这个也是替换路由操作。推送一个命名路由到Navigator,新路由完成动画之后处理上一个路由。
  • removeRoute 从Navigator中删除路由,同时执行Route.dispose操作。
  • removeRouteBelow 从Navigator中删除路由,同时执行Route.dispose操作,要替换的路由是传入参数anchorRoute里面的路由。
  • replace 将Navigator中的路由替换成一个新路由。
  • replaceRouteBelow 将Navigator中的路由替换成一个新路由,要替换的路由是是传入参数anchorRoute里面的路由。

路由的操作方式

使用Navigator.push实现发送路由,Navigator.pop返回上一个页面。

push函数的有两个:上下文context、Route。这里使用的是MaterialPageRoute,该类必须要传入一个闭包函数WidgetBuilder,参数是 BuildContext对象,我们使用的是匿名函数的形式,加上箭头符号:builder: (context) => HomePage()