概述
众所周知,一个健全的
App
,通常都会有一个SplashPage
页面,且该页面一般用于应用(APP)启动时,当然其存在的主要目的是承载:启动页
、引导页
、广告页
、等待页
等业务场景。笔者认为,与其说是闪屏页
,倒不如叫中转页
,怎么个中转
法,还请听笔者一一到来...这里笔者借助以
Flutter
实现微信App
登录的逻辑,以点带面来讲讲SplashPage
页面产生的原因和作用,SplashPage
页面如何实现上面👆提到的几种常用的业务场景。希望大家能够举一反三,能够更好的妙用SplashPage
页,从而更好的实现所需功能,以及提高用户的体验。源码地址:flutter_wechat
场景
启动页 |
引导页 |
---|---|
splash_page_0.png
|
splash_page_1.png
|
| 广告页
| 主页
|
|
splash_page_2.png
|
splash_page_3.png
|
由来
上面提到过,闪屏页
只是外界一种通俗的说法,但其本质就是用来中转
或转场
的。比如现实场景中,程序一旦启动,我们可能需要:读取本地的用户数据
,读取文件存储的(广告)图片
,请求token是否失效
,请求一些公有数据,内存缓存
... ,众所周知,这些操作场景都是比较耗时
的,且一般我们都是异步
去处理的,以及有时候我们必须等这些耗时操作返回数据后,才能进行下一步操作。
当然,闪屏页
就是为了解决耗时异步
的场景而闪亮登场的。其目的就是:利用闪屏页,友好的来等待异步耗时数据的返回,根据数据返回丝滑的过渡到目标页面,从而增大用户体验。
这里笔者就拿程序一旦启动,读取本地用户信息(耗时),根据用户信息有无,来显示不同界面(主页或登录)
的常见场景,进一步来说明闪屏页
的妙用。
伪代码如下:
// 获取用户数据 耗时操作 异步请求
await final userInfo = _fetchUserInfo();
if (userInfo != null) {
// 有用户数据,跳转到主页
} else {
// 没有用户数据,跳转到登录页
}
方案一:main函数处理
伪代码如下:
void main() async {
// 获取用户数据
await final userInfo = _fetchUserInfo();
if (userInfo != null) {
// 有用户数据,跳转到主页
} else {
// 没有用户数据,跳转到登录页
}
}
优点:无需增加闪屏页(中转页),代码逻辑比较清晰
缺点:只适合耗时比较短的异步请求(100ms之内),否则程序一启动,会有肉眼可见的卡顿,影响用户体验。
方案二:闪屏页处理
程序一启动,立即切换到闪屏页,闪屏页初始化的时候异步获取用户数据,且闪屏页默认展示跟iOS或Android一致的启动页,从而迷惑用户认为App正常启动的错觉,从而无形之中提高了用户的体验。
一旦耗时的数据异步返回了,然后再去丝滑的切换页面。
代码实现如下:
// SplashPage.dart
class _SplashPageState extends State<SplashPage>{
@override
void initState() {
super.initState();
// 初始化
initAsync();
}
// 异步初始化
void initAsync() async {
// 获取用户数据
await final userInfo = _fetchUserInfo();
if (userInfo != null) {
// 有用户数据,跳转到主页
} else {
// 没有用户数据,跳转到登录页
}
}
@override
Widget build(BuildContext context) {
// 返回启动页
return LaunchImage()
}
}
优点:极大的增强了用户体验,且拓展性强,以此可以衍生出引导页
、广告页
...等常见业务场景,下面会一一说到。
综上所述,侧面验证了,闪屏页
一般是用来友好的等待异步耗时的数据返回,根据数据返回丝滑的过渡到目标页面,从而极大的增强用户体验而产生。
用途
闪屏页
在现实场景中,使用是非常广泛的,笔者相信一款正常的App
都会使用到闪屏页
,且大多数用于程序启动时启动页
、引导页
、广告页
、等待页
等业务场景。
这里笔者借用实现微信App启动的逻辑,来阐述一下闪屏页的用途,希望大家能够举一反三,能够将其用于现实的开发场景中去。
微信启动逻辑图(开局一张图,内容全靠编...)
微信登录.png
上面就是笔者整理的微信登陆逻辑,大家可以打开你手机上的微信App,逐个验证各个逻辑。当然微信是没有广告页的,笔者这里增加广告逻辑只是为了满足业界通用App的逻辑罢了,微信的做法是:v1 == v2
=> 根据account和userInfo判断
=> 切换页面
,可见,微信的登陆比上面逻辑图更简单,这里笔者就不一一赘述了。
其次,笔者相信,上面的逻辑图,应该能满足业界80%以上的app启动逻辑,大家如果有任何疑问或者有更好的解决方案,欢迎留言交流,谢谢。
最后,相信有了逻辑图,大家写起代码也比较胸有成竹了,也希望大家在写代码之前,先写好流程图,避免像无头苍蝇一样,毫无目标性。记住:在错误的道路上,停止就是前进!
代码
/// 闪屏跳转模式
enum MHSplashSkipMode {
newFeature, // 新特性(引导页)
login, // 登陆
currentLogin, // 账号登陆
homePage, // 主页
ad, // 广告页
}
/// 闪屏界面主要用来中转(新特性界面、登陆界面、主页面)
class SplashPage extends StatefulWidget {
SplashPage({Key key}) : super(key: key);
_SplashPageState createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> {
/// 跳转方式
MHSplashSkipMode _skipMode;
/// 定时器相关
TimerUtil _timerUtil;
/// 计数
int _count = 5;
/// 点击是否高亮
bool _highlight = false;
@override
void dispose() {
super.dispose();
print('🔥 Splash Page is Over 👉');
// 记得中dispose里面把timer cancel。
if (_timerUtil != null) _timerUtil.cancel();
}
@override
void initState() {
super.initState();
// 监听部件渲染完
/// widget渲染监听。
WidgetUtil widgetUtil = new WidgetUtil();
widgetUtil.asyncPrepares(true, (_) async {
// widget渲染完成。
// App启动时读取Sp数据,需要异步等待Sp初始化完成。必须保证它 优先初始化。
await SpUtil.getInstance();
// 获取一下通讯录数据,理论上是在跳转到主页时去请求
ContactsService.sharedInstance;
// 读取一下全球手机区号编码
ZoneCodeService.sharedInstance;
/// 获取App信息
PackageInfo packageInfo = await PackageInfo.fromPlatform();
// String appName = packageInfo.appName;
// String packageName = packageInfo.packageName;
String version = packageInfo.version;
String buildNumber = packageInfo.buildNumber;
// 拼接app version
final String appVersion = version + '+' + buildNumber;
// 获取缓存的版本号
final String cacheVersion = SpUtil.getString(CacheKey.appVersionKey);
// 获取用户信息
if (appVersion != cacheVersion) {
// 保存版本
SpUtil.putString(CacheKey.appVersionKey, appVersion);
// 更新页面,切换为新特性页面
setState(() {
_skipMode = MHSplashSkipMode.newFeature;
});
} else {
// _switchRootView();
setState(() {
_skipMode = MHSplashSkipMode.ad;
});
// 配置定时器
_configureCountDown();
}
});
}
// 切换rootView
void _switchRootView() {
// 取出登陆账号
final String rawLogin = AccountService.sharedInstance.rawLogin;
// 取出用户
final User currentUser = AccountService.sharedInstance.currentUser;
// 跳转路径
String skipPath;
// 跳转模式
MHSplashSkipMode skipMode;
if (Util.isNotEmptyString(rawLogin) && currentUser != null) {
// 有登陆账号 + 有用户数据 跳转到 主页
skipMode = MHSplashSkipMode.homePage;
skipPath = Routers.homePage;
} else if (currentUser != null) {
// 没有登陆账号 + 有用户数据 跳转到当前登陆
skipMode = MHSplashSkipMode.currentLogin;
skipPath = LoginRouter.currentLoginPage;
} else {
// 没有登陆账号 + 没有用户数据 跳转到登陆
skipMode = MHSplashSkipMode.login;
skipPath = LoginRouter.loginPage;
}
// 这里无需更新 页面 直接跳转即可
_skipMode = skipMode;
// 跳转对应的主页
NavigatorUtils.push(context, skipPath,
clearStack: true, transition: TransitionType.fadeIn);
}
/// 配置倒计时
void _configureCountDown() {
_timerUtil = TimerUtil(mTotalTime: 5000);
_timerUtil.setOnTimerTickCallback((int tick) {
double _tick = tick / 1000;
if (_tick == 0) {
// 切换到主页面
_switchRootView();
} else {
setState(() {
_count = _tick.toInt();
});
}
});
_timerUtil.startCountDown();
}
@override
Widget build(BuildContext context) {
/// 配置屏幕适配的 flutter_screenutil 和 flustars 设计稿的宽度和高度(单位px)
/// Set the fit size (fill in the screen size of the device in the design) If the design is based on the size of the iPhone6 (iPhone6 750*1334)
// 配置设计图尺寸,iphone 7 plus 1242.0 x 2208.0
final double designW = 1242.0;
final double designH = 2208.0;
FlutterScreenUtil.ScreenUtil.instance =
FlutterScreenUtil.ScreenUtil(width: designW, height: designH)
..init(context);
setDesignWHD(designW, designH, density: 3);
/// If you use a dependent context-free method to obtain screen parameters and adaptions, you need to call this method.
MediaQuery.of(context);
Widget child;
if (_skipMode == MHSplashSkipMode.newFeature) {
// 引导页
child = _buildNewFeatureWidget();
} else if (_skipMode == MHSplashSkipMode.ad) {
// 广告页
child = _buildAdWidget();
} else {
// 启动页
child = _buildDefaultLaunchImage();
}
return Material(child: child);
}
/// 默认情况是一个启动页 1200x530
/// https://game.gtimg.cn/images/yxzj/img201606/heroimg/121/121-bigskin-4.jpg
Widget _buildDefaultLaunchImage() {
return Container(
width: double.maxFinite,
height: double.maxFinite,
decoration: BoxDecoration(
// 这里设置颜色 跟启动页一致的背景色,以免发生白屏闪烁
color: Color.fromRGBO(0, 10, 24, 1),
image: DecorationImage(
// 注意:启动页 别搞太大 以免加载慢
image: AssetImage(Constant.assetsImages + 'LaunchImage.png'),
fit: BoxFit.cover,
),
),
);
}
/// 新特性界面
Widget _buildNewFeatureWidget() {
return Swiper(
itemCount: 3,
loop: false,
itemBuilder: (_, index) {
final String name =
Constant.assetsImagesNewFeature + 'intro_page_${index + 1}.png';
Widget widget = Image.asset(
name,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
);
if (index == 2) {
return Stack(
children: <Widget>[
widget,
Positioned(
child: InkWell(
child: Image.asset(
Constant.assetsImagesNewFeature + 'skip_btn.png',
width: 175.0,
height: 55.0,
),
onTap: _switchRootView,
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
focusColor: Colors.transparent,
),
left: (ScreenUtil.getInstance().screenWidth - 175) * 0.5,
bottom: 55.0,
width: 175.0,
height: 55.0,
),
],
);
} else {
return widget;
}
},
);
}
/// 广告页
Widget _buildAdWidget() {
return Container(
child: _buildAdChildWidget(),
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
// 这里设置颜色 跟背景一致的背景色,以免发生白屏闪烁
color: Color.fromRGBO(21, 5, 27, 1),
image: DecorationImage(
image: AssetImage(Constant.assetsImagesBg + 'SkyBg01_320x490.png'),
fit: BoxFit.cover,
),
),
);
}
Widget _buildAdChildWidget() {
final double horizontal =
FlutterScreenUtil.ScreenUtil.getInstance().setWidth(30.0);
final double vertical =
FlutterScreenUtil.ScreenUtil.getInstance().setHeight(9.0);
final double fontSize =
FlutterScreenUtil.ScreenUtil.getInstance().setSp(42.0);
final lineHeight =
FlutterScreenUtil.ScreenUtil.getInstance().setHeight(20.0 * 3 / 14.0);
final radius = FlutterScreenUtil.ScreenUtil.getInstance().setWidth(108.0);
return Stack(
children: <Widget>[
Swiper(
onTap: (idx) {
print('onTap $idx');
// 跳转到Web
},
itemCount: 4,
autoplayDelay: 1500,
loop: true,
autoplay: true,
itemBuilder: (_, index) {
return Center(
child: Image.asset(
Constant.assetsImagesAds + '121-bigskin-${index + 1}.jpg',
fit: BoxFit.cover,
),
);
},
),
Positioned(
top: FlutterScreenUtil.ScreenUtil.getInstance().setWidth(60.0),
right: FlutterScreenUtil.ScreenUtil.getInstance().setWidth(60.0),
child: InkWell(
onTap: () {
if (_timerUtil != null) {
_timerUtil.cancel();
}
_switchRootView();
},
onHighlightChanged: (highlight) {
setState(() {
_highlight = highlight;
});
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: horizontal, vertical: vertical),
alignment: Alignment.center,
decoration: BoxDecoration(
color: _highlight ? Colors.white30 : Colors.white10,
border: Border.all(color: Colors.white, width: 0),
borderRadius: BorderRadius.all(Radius.circular(radius)),
),
child: Text(
'跳过 $_count',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: fontSize,
height: lineHeight),
),
),
),
)
],
);
}
}
总结
闪屏页
的功能虽然很简单,但是作用却非常大,希望大家通过阅读本篇文章,能够更好地认识闪屏页
,了解他的由来,知道他的用途,并将其优点运用到实际开发中去,能够实实在在的解决现实中的问题。
- 由来:用来友好的等待异步耗时的数据返回,再根据数据返回丝滑的过渡到目标页面,从而极大的增强用户体验而产生。
- 用途:一般是用在App启动时,承载启动页、引导页、广告页、等待页等业务场景。
期待
- 文章若对您有些许帮助,请给个喜欢❤️,毕竟码字不易;若对您没啥帮助,请给点建议💗,切记学无止境。
- 针对文章所述内容,阅读期间任何疑问;请在文章底部评论指出,我会火速解决和修正问题。
- GitHub地址:https://github.com/CoderMikeHe
- 源码地址:flutter_wechat
参考链接
拓展
本文转自 https://www.jianshu.com/p/e2dcd0e8e04d,如有侵权,请联系删除。