仿学而思网校移动端app flutter

疫情导致被迫离职在家,为保证不被社会所抛弃(躲避哄娃),决定仿做一个app,来练习一下Flutter技术。

本文将仿做 学而思网校启动页。
成品效果图:


image.png

功能说明

本文将介绍如何实现仿 学而思网校移动app(android)的 启动页.

知识点

  • Stack布局
  • 自定义字体
  • 视频控件
  • 圆角矩形布局

介绍

启动页背景是一个循环播放的视频,右上角有一个直接进入的按钮;下方由 logo+登录按钮+协议组成。整个页面比较简单。

代码来了

SplashPage

创建启动屏页面,我使用的是vs IDE,新增dart文件后,输入 sta会出现自动补全代码功能,比较好用,选择statefulWidget.生成代码如下:

/**
 * 
 * 启动屏页面,main启动后进入本页面
 */
class SplashPage extends StatefulWidget {
  @override
  _SplashPageState createState() => _SplashPageState();
}

class _SplashPageState extends State {
}

视频组件

视频采用 videoplayer组件,文档看这里: https://pub.flutter-io.cn/packages/video_player
首先在pubspec.yaml 中引用videoplayer组件

pubspec.yaml 中部分代码段

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2

  # https://pub.flutter-io.cn/packages/video_player
  video_player: ^0.10.6
  • 1.在SplashPage中引入组件
import 'package:video_player/video_player.dart';
  • 2.定义video controller,用来控制视频播放组件
VideoPlayerController _controller;
// 在initState 中初始化,视频资源可以是网络资源,也可以是本地资源。
@override
  void initState() {
    print('splashPageInit');
    // TODO: implement initState
    super.initState();
    _controller = VideoPlayerController
        .network('https://mvp.yanhuisou.com/uploads/files/20200307/fb5dbe8eb6d74d7ab75ba5bd5a845baf.mp4')
        // .asset('src/start.mp4')
      ..setLooping(true)
      ..setVolume(0.3)
      ..initialize()
      .then((_) {
        // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
        _controller.play();
        setState(() {});
      });
}
// 在dispose 中释放资源
 void dispose() {
    // TODO: implement dispose

    _controller.pause();
    _controller.dispose();
    super.dispose();
  }
  • 3.布局中引入组件
    布局在后面的代码再介绍。

Stack布局

由于会出现叠放的布局情况,所以使用Stack方式进行布局。大体结构如下:

    Stack(
      children: [
//  背景视频或图片
          Container(),
// 顶部直接跳过按钮
          Positioned(),
// 底部logo+登录注册按钮+协议
          Positioned()
      ]);

详细代码:

@override
  Widget build(BuildContext context) {
    ScreenUtil.init(context, width: 750, height: 1334);

    final size = MediaQuery.of(context).size;
    Application.screenWidth = size.width;
    Application.screenHeight = size.height;
    Application.statusBarHeight = MediaQuery.of(context).padding.top;
    Application.bottomBarHeight = MediaQuery.of(context).padding.bottom;
    return Stack(children: [
      Container(
          child: _controller.value.initialized
              ? VideoPlayer(_controller)
              : Image(
                  image: AssetImage('src/bg.jpg'),
                  height: Application.screenHeight,
                )),
      Positioned(
          top: Application.statusBarHeight + 10,
          right: 20,
          child:
              // Text('sss'),
              RoundedBox(
            width: 80.0,
            height: 32.0,
            borderRadius: 16,
            child: Container(
                child: Text(
              '直接进入',
              style: TextStyle(
                  color: Colors.white,
                  fontSize: 14,
                  decoration: TextDecoration.none,
                  fontWeight: FontWeight.w100),
            )),
          )..bgColor = Colors.black26),
      Positioned(
          bottom: 30,
          width: ScreenUtil().setWidth(750),
          child: Container(
            // alignment: Alignment.center,
            // width: 750,
            // width: ,
            // color: Colors.black26,
            child: Column(children: [
              Container(
                padding: EdgeInsets.all(10),
                child: Text('学而思网校-仿',
                    style: customFont.copyWith(
                        color: Colors.white,
                        fontSize: 25,
                        fontWeight: FontWeight.w100,
                        decoration: TextDecoration.none,
                        letterSpacing: 4)
                    // TextStyle(
                    //     fontFamily: 'Ewert',
                    //     color: Colors.red,
                    //     fontSize: 25,
                    //     decoration: TextDecoration.none,
                    //     letterSpacing: 4),

                    ),
              ),
              Container(
                margin: EdgeInsets.fromLTRB(0, 20, 0, 20),
                child: RoundedBox(
                  width: 300.0,
                  height: 50.0,
                  borderRadius: 30,
                  child: Container(
                      child: Text(
                    '登录/注册',
                    style: TextStyle(
                        color: Colors.white,
                        fontSize: 25,
                        decoration: TextDecoration.none,
                        fontFamily: 'PingFang',
                        fontWeight: FontWeight.w200,
                        letterSpacing: 4),
                  )),
                )..bgColor = Colors.redAccent,
              ),
              Container(
                  padding: EdgeInsets.all(10),
                  child: RichText(
                    text: TextSpan(
                        style: TextStyle(
                          color: Colors.white70,
                        ),
                        text: "登录即代表你已阅读并同意",
                        children: [
                          TextSpan(
                              text: " 《用户协议》",
                              style: TextStyle(
                                color: Colors.redAccent,
                              ),
                              recognizer: TapGestureRecognizer()
                                ..onTap = () {
                                  toProtocolPage();
                                }),
                          TextSpan(
                              text: "《隐私协议》",
                              style: TextStyle(
                                color: Colors.redAccent,
                              ),
                              recognizer: TapGestureRecognizer()
                                ..onTap = () {
                                  toProtocolPage(isUserProtocol: false);
                                })
                        ]),
                  )),
            ]),
          ))
    ]);
  }

以上这部分代码我觉得有几个地方需要说明一下:

  • 1、视频背景是在视频初始化完成后进行展示,视频是否完成初始化根据_controller.value.initialized来确定,如果未完成初始化则显示本地图片。
  • 2、RoundedBox是我自定义的一个圆角矩形组件,在顶部的直接进入与下方的登录均使用到了该组件,这里先不介绍。
  • 3、协议部分采用了 RichText的方式,实现协议内容样式与正文内容样式不同。
  • 4、logo是使用文本字体实现,这里涉及到了自定义字体的方式。
# pubspec.yaml 中部分代码段
  fonts:
    - family: MaoTi                # 字体别名
      fonts:
        - asset: fonts/mao_ti.ttf  # 字体文件目录
          weight: 700              # 权重 700表示粗体,相当于bold 
// SplashPage中部分代码
// 自定义字体,使用 customFont.copyWith来重新定义该属性。
  var customFont = TextStyle(
    fontFamily: "MaoTi", // 指定该Text的字体。
  );

   Text('学而思网校-仿',
           style: customFont.copyWith(
           color: Colors.white,
           fontSize: 25,
           fontWeight: FontWeight.w100,
           decoration: TextDecoration.none,
  ),

圆角矩形组件

上文有提到自定义的圆角组件,由于每次绘制圆角矩形效果比较麻烦,想着自定义一个组件会简单一些。直接上代码:

import 'package:flutter/material.dart';

/**
 * 
 * 圆角容器
 * 
 */
class RoundedBox extends StatelessWidget {
  Widget child;
  final double width;
  final double height;
  // 圆角半径
  final double borderRadius;

  Color bgColor = Colors.black26;
  Color borderColor = Colors.white;
  double borderWidth = 1;

  RoundedBox(
      {Key key,
      this.child,
      this.borderColor,
      this.width,
      this.height,
      this.borderRadius})
      : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Container(
      width: this.width,
      height: this.height,
      child: this.child,
      alignment: Alignment.center,
      decoration: BoxDecoration(
          color: this.bgColor,
          borderRadius: BorderRadius.circular(this.borderRadius),
          shape: BoxShape.rectangle
          //   boxShadow: [
          //     // // 阴影
          //     // BoxShadow(
          //     //   color: Colors.grey[50],
          //     //   // 偏移位置,以中心点为参考
          //     //   offset: Offset(0, 0),
          //     //   // 凝炼程度(密度)越大越散
          //     //   blurRadius: 40.0,
          //     //   // 扩散程度(远近)
          //     //   spreadRadius: 10.0,
          //     // )
          //   ],
          ),
    );
  }
}

总结

总的来说功能很简单,但在做的过程中还是遇到了很多问题。在大多时候还是一看就会,一做就废,还是需要勤动手。

本次开发过程中遇到了, assets file not found的问题,在经过若干次尝试,最终发现是pubspec.yaml中编写 assets部分存在问题,猜测是某个地方多了一个空格导致(编到后来无法复现了),还奇怪的以为是videoplayer的bug嘞,这个锅没甩出去。

启动页完成了,接下来找时间将主页、协议页编写完成,给自己立个flag。

你可能感兴趣的:(仿学而思网校移动端app flutter)