源码地址:flutter_wechat
场景:
启动页
引导页
广告页
主页
上面提到过,闪屏页
只是外界一种通俗的说法,但其本质就是用来中转或转场的。比如现实场景中,程序一旦启动,我们可能需要:读取本地的用户数据,读取文件存储的(广告)图片,请求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{
@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启动的逻辑,来阐述一下闪屏页的用途,希望大家能够举一反三,能够将其用于现实的开发场景中去。
微信启动逻辑图(开局一张图,内容全靠编…)
上面就是笔者整理的微信登陆逻辑,大家可以打开你手机上的微信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 {
/// 跳转方式
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,
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,
最后
小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
资料⬅专栏获取
ll(
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,
最后
小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
[外链图片转存中…(img-7mkXCKAs-1719053731575)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
资料⬅专栏获取