疫情导致被迫离职在家,为保证不被社会所抛弃(躲避哄娃),决定仿做一个app,来练习一下Flutter技术。
本文将仿做 学而思网校启动页。
成品效果图:
功能说明
本文将介绍如何实现仿 学而思网校移动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。