Flutter实战之闪屏启动页

在APP启动之前,一般都会SplashPage页面,这页面包含闪屏启动页(启动APP的过渡页面)、引导页(APP简介说明)、广告页(点击在浏览器中打开H5页面、或者直接下载APP文件)。

实现思路

APP的第一个页面就是闪屏启动页面,然后再处理跳转H5广告页面,还是跳转到首页的逻辑。SplashPage分为四层,默认启动图,引导图,广告图,倒计时跳过 使用status来控制页面显示状态,status=0显示启动图,status=1显示广告图和倒计时跳过,status=2显示引导图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyApp extends StatelessWidget {

@override
Widget build(BuildContext context) {

final router = Router();
Routers.configureRouters(router);
Application.router = router;

return MaterialApp(
title: 'Flutter Demo',
onGenerateRoute: Application.router.generator,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SplashPage(),
);
}
}

依赖的第三方库

  1. fluro Flutter路由管理
  2. shared_preferences 简单数据的持久化存储
  3. cached_network_image 显示网络图片并保存到cache中
  4. url_launcher 打开URL。包括打电话、发短信、在浏览器中打开地址等URL
  5. flutter_inappbrowser 内联webview或应用程序内浏览器窗口
  6. flukit Flutter 常用库集合

以上的库都是比较常用的,当然也可以自行选择其他库或者不使用。

实现步骤

一、解决Android启动白屏

在Android原生的mipmap目录下放闪屏背景图,可以根据不同分辨率放入。
然后修改Android原生的drawable目录下的launch_background.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />

<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<!-- 撑满屏幕、可能会有拉伸 -->
<item android:drawable="@mipmap/splash_bg"></item>
</layer-list>

二、构建基础页面

根据status,使用Stack + Offset来显示不同的页面。使用Stack + Align来绝对定位部分子组件。注意:必须先把图片放到assets/images文件夹下,然后在pubspec.yaml中定义

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
// 构建闪屏背景
Widget _buildSplashBg() {
return new Image.asset(
'assets/images/splash_bg.png',
width: double.infinity,
height: double.infinity,
fit: BoxFit.fill,
);
}

// 构建广告
Widget _buildAdWidget() {
return new InkWell(
onTap: () {
print('点击了广告图');
},
child: new Container(
alignment: Alignment.center,
child: new CachedNetworkImage(
width: double.infinity,
height: double.infinity,
fit: BoxFit.fill,
imageUrl: _splashModel.imgUrl,
placeholder: (context, url) => _buildSplashBg(),
errorWidget: (context, url, error) => _buildSplashBg(),
),
),
),
);
}

// 构建引导页
_initGuideBanner() {
setState(() {
_status = 2;
});
for (int i = 0, length = _guideList.length; i < length; i++) {
if (i == length - 1) {
_bannerList.add(new Stack(
children: <Widget>[
new Image.asset(
_guideList[i],
fit: BoxFit.fill,
width: double.infinity,
height: double.infinity,
),
new Align(
alignment: Alignment.bottomCenter,
child: new Container(
margin: EdgeInsets.only(bottom: 160.0),
child: new InkWell(
onTap: () {
_goMain();
},
child: new CircleAvatar(
radius: 48.0,
backgroundColor: Colors.indigoAccent,
child: new Padding(
padding: EdgeInsets.all(2.0),
child: new Text(
'立即体验',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 16.0),
),
),
),
),
),
),
],
));
} else {
// print(_guideList[i]);
_bannerList.add(new Image.asset(
_guideList[i],
fit: BoxFit.fill,
width: double.infinity,
height: double.infinity,
));
}
}
}

// Stack + Offset。Swiper来自于flukit
@override
Widget build(BuildContext context) {
return new Material(
child: new Stack(
children: <Widget>[
new Offstage(
offstage: !(_status == 0),
child: _buildSplashBg(),
),
new Offstage(
offstage: !(_status == 2),
child: _bannerList.isEmpty
? new Container()
: new Swiper(
autoStart: false,
circular: false,
indicator: CircleSwiperIndicator(
radius: 4.0,
padding: EdgeInsets.only(bottom: 30.0),
itemColor: Colors.black26,
),
children: _bannerList),
),
_buildAdWidget(),
new Offstage(
offstage: !(_status == 1),
child: new Container(
alignment: Alignment.bottomRight,
margin: EdgeInsets.all(20.0),
child: InkWell(
onTap: () {
_goMain();
},
child: new Container(
padding: EdgeInsets.all(12.0),
child: new Text(
'$_count 跳转',
style: new TextStyle(fontSize: 14.0, color: Colors.white),
),
decoration: new BoxDecoration(
color: Color(0x66000000),
borderRadius: BorderRadius.all(Radius.circular(4.0)),
border: new Border.all(
width: 0.33, color: Colors.grey))),
),
),
)
],
),
);

三、实现闪屏相关逻辑

倒计时。使用Timer.periodic

1
2
3
4
5
6
7
8
9
10
11
_timer = Timer.periodic(new Duration(seconds: 1), (timer) {
setState(() {
if (_count <= 1) {
_timer.cancel();
_timer = null;
_goMain();
} else {
_count = _count - 1;
}
});
});

判断是否已经加载过引导图

1
2
3
4
5
6
7
8
SharedPreferences prefs = await SharedPreferences.getInstance();
bool isGuide = prefs.getBool(Constant.key_guide) ?? true;
// 是否已经加载过引导图
if (isGuide) {
_initGuideBanner();
} else {
_initSplash();
}

完整代码

splash_page.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
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:music_app/router/routers.dart';
import 'package:quiver/strings.dart';
import 'dart:convert';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flukit/flukit.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:music_app/utils/http_utils.dart';
import 'package:music_app/common/common.dart';
import 'package:music_app/models/splash_model.dart';
import 'package:music_app/router/application.dart';

class SplashPage extends StatefulWidget {
@override
_SplashPageState createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {

List<String> _guideList = [
'assets/images/guide1.png',
'assets/images/guide2.png',
'assets/images/guide3.png',
'assets/images/guide4.png'
];

List<Widget> _bannerList = new List();

Timer _timer;
int _status = 0; // 0-启动图,1-广告图和倒计时跳过,2-引导图。
int _count = 3; // 倒计时秒数

SplashModel _splashModel;

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

void _initAsync() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
// 获取上次存储的闪屏页面
String jsonStr = prefs.getString(Constant.key_splash_model);
if (isNotEmpty(jsonStr)) {
Map<String, dynamic> splash = json.decode(jsonStr);
_splashModel = SplashModel.fromJson(splash);
}

HttpUtils httpUtils = new HttpUtils();
// 获取闪屏的广告数据
httpUtils.getSplash().then((model) {
if (isNotEmpty(model.imgUrl)) {
if (_splashModel == null || (_splashModel.imgUrl != model.imgUrl)) {
prefs.setString(Constant.key_splash_model, model.toString());
setState(() {
_splashModel = model;
});
}
} else {
prefs.setString(Constant.key_splash_model, null);
}
});

bool isGuide = prefs.getBool(Constant.key_guide) ?? true;
// 是否已经加载过引导图
if (isGuide) {
_initGuideBanner();
} else {
_initSplash();
}
}

_initGuideBanner() {
setState(() {
_status = 2;
});
for (int i = 0, length = _guideList.length; i < length; i++) {
if (i == length - 1) {
_bannerList.add(new Stack(
children: <Widget>[
new Image.asset(
_guideList[i],
fit: BoxFit.fill,
width: double.infinity,
height: double.infinity,
),
new Align(
alignment: Alignment.bottomCenter,
child: new Container(
margin: EdgeInsets.only(bottom: 160.0),
child: new InkWell(
onTap: () {
_goMain();
},
child: new CircleAvatar(
radius: 48.0,
backgroundColor: Colors.indigoAccent,
child: new Padding(
padding: EdgeInsets.all(2.0),
child: new Text(
'立即体验',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 16.0),
),
),
),
),
),
),
],
));
} else {
// print(_guideList[i]);
_bannerList.add(new Image.asset(
_guideList[i],
fit: BoxFit.fill,
width: double.infinity,
height: double.infinity,
));
}
}
}

_initSplash() {
if (_splashModel == null) {
_goMain();
return;
}
setState(() {
_status = 1;
});

// 倒计时
_timer = Timer.periodic(new Duration(seconds: 1), (timer) {
setState(() {
if (_count <= 1) {
_timer.cancel();
_timer = null;
_goMain();
} else {
_count = _count - 1;
}
});
});
}

// 跳转主页
void _goMain() {
Application.router.navigateTo(context, Routers.mainPage, replace: true, transition: TransitionType.fadeIn);
}

// 构建闪屏背景
Widget _buildSplashBg() {
return new Image.asset(
'assets/images/splash_bg.png',
width: double.infinity,
fit: BoxFit.fill,
height: double.infinity,
);
}

// 构建广告
Widget _buildAdWidget() {
if (_splashModel == null) {
return new Container(
height: 0.0,
);
}
return new Offstage(
offstage: !(_status == 1),
child: new InkWell(
onTap: () async {
String url = _splashModel.url;
if (isEmpty(url)) return;
_goMain();
if (url.endsWith(".apk")) {
// 打开浏览器下载APK
if (await canLaunch(url)) {
await launch(url, forceSafariVC: false, forceWebView: false);
}
} else {
// 在浏览器中打开url
Application.router.navigateTo(context, Routers.adPage + '?title=${Uri.encodeComponent(_splashModel.title)}&url=${Uri.encodeComponent(_splashModel.url)}', transition: TransitionType.fadeIn);
}
},
child: new Container(
alignment: Alignment.center,
child: new CachedNetworkImage(
width: double.infinity,
height: double.infinity,
fit: BoxFit.fill,
imageUrl: _splashModel.imgUrl,
placeholder: (context, url) => _buildSplashBg(),
errorWidget: (context, url, error) => _buildSplashBg(),
),
),
),
);
}

@override
void dispose() {
_timer?.cancel();
_timer = null;
super.dispose();
}

@override
Widget build(BuildContext context) {
return new Material(
child: new Stack(
children: <Widget>[
new Offstage(
offstage: !(_status == 0),
child: _buildSplashBg(),
),
new Offstage(
offstage: !(_status == 2),
child: _bannerList.isEmpty
? new Container()
: new Swiper(
autoStart: false,
circular: false,
indicator: CircleSwiperIndicator(
radius: 4.0,
padding: EdgeInsets.only(bottom: 30.0),
itemColor: Colors.black26,
),
children: _bannerList),
),
_buildAdWidget(),
new Offstage(
offstage: !(_status == 1),
child: new Container(
alignment: Alignment.bottomRight,
margin: EdgeInsets.all(20.0),
child: InkWell(
onTap: () {
_goMain();
},
child: new Container(
padding: EdgeInsets.all(12.0),
child: new Text(
'$_count 跳转',
style: new TextStyle(fontSize: 14.0, color: Colors.white),
),
decoration: new BoxDecoration(
color: Color(0x66000000),
borderRadius: BorderRadius.all(Radius.circular(4.0)),
border: new Border.all(
width: 0.33, color: Colors.grey))),
),
),
)
],
),
);
}
}

splash_model.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
class SplashModel {
String title;
String content;
String url;
String imgUrl;

SplashModel({this.title, this.content, this.url, this.imgUrl});

SplashModel.fromJson(Map<String, dynamic> json)
: title = json['title'],
content = json['content'],
url = json['url'],
imgUrl = json['imgUrl'];

Map<String, dynamic> toJson() => {
'title': title,
'content': content,
'url': url,
'imgUrl': imgUrl,
};

@override
String toString() {
StringBuffer sb = new StringBuffer('{');
sb.write("\"title\":\"$title\"");
sb.write(",\"content\":\"$content\"");
sb.write(",\"url\":\"$url\"");
sb.write(",\"imgUrl\":\"$imgUrl\"");
sb.write('}');
return sb.toString();
}
}

坚持原创技术分享,您的支持将鼓励我继续创作!