在APP启动之前,一般都会SplashPage页面,这页面包含闪屏启动页(启动APP的过渡页面)、引导页(APP简介说明)、广告页(点击在浏览器中打开H5页面、或者直接下载APP文件)。
实现思路
APP的第一个页面就是闪屏启动页面,然后再处理跳转H5广告页面,还是跳转到首页的逻辑。SplashPage分为四层,默认启动图,引导图,广告图,倒计时跳过 使用status来控制页面显示状态,status=0显示启动图,status=1显示广告图和倒计时跳过,status=2显示引导图。
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(),
);
}
}
依赖的第三方库
- fluro Flutter路由管理
- shared_preferences 简单数据的持久化存储
- cached_network_image 显示网络图片并保存到cache中
- url_launcher 打开URL。包括打电话、发短信、在浏览器中打开地址等URL
- flutter_inappbrowser 内联webview或应用程序内浏览器窗口
- flukit Flutter 常用库集合
以上的库都是比较常用的,当然也可以自行选择其他库或者不使用。
实现步骤
一、解决Android启动白屏
在Android原生的mipmap目录下放闪屏背景图,可以根据不同分辨率放入。
然后修改Android原生的drawable目录下的launch_background.xml
<?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中定义
// 构建闪屏背景
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
_timer = Timer.periodic(new Duration(seconds: 1), (timer) {
setState(() {
if (_count <= 1) {
_timer.cancel();
_timer = null;
_goMain();
} else {
_count = _count - 1;
}
});
});
判断是否已经加载过引导图
SharedPreferences prefs = await SharedPreferences.getInstance();
bool isGuide = prefs.getBool(Constant.key_guide) ?? true;
// 是否已经加载过引导图
if (isGuide) {
_initGuideBanner();
} else {
_initSplash();
}
完整代码
splash_page.dart
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
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();
}
}
本文参考wanandroid,感谢鸿洋大神