概述
眾所周知,一個健全的
App,通常都會有一個SplashPage頁面,且該頁面一般用于應(yīng)用(APP)啟動時,當(dāng)然其存在的主要目的是承載:啟動頁、引導(dǎo)頁、廣告頁、等待頁等業(yè)務(wù)場景。筆者認(rèn)為,與其說是閃屏頁,倒不如叫中轉(zhuǎn)頁,怎么個中轉(zhuǎn)法,還請聽筆者一一到來...這里筆者借助以
Flutter實現(xiàn)微信App登錄的邏輯,以點帶面來講講SplashPage頁面產(chǎn)生的原因和作用,SplashPage頁面如何實現(xiàn)上面??提到的幾種常用的業(yè)務(wù)場景。希望大家能夠舉一反三,能夠更好的妙用SplashPage頁,從而更好的實現(xiàn)所需功能,以及提高用戶的體驗。源碼地址:flutter_wechat
場景
啟動頁 |
引導(dǎo)頁 |
|---|---|
![]() splash_page_0.png
|
![]() splash_page_1.png
|
廣告頁 |
主頁 |
![]() splash_page_2.png
|
![]() splash_page_3.png
|
由來
上面提到過,閃屏頁只是外界一種通俗的說法,但其本質(zhì)就是用來中轉(zhuǎn)或轉(zhuǎn)場的。比如現(xiàn)實場景中,程序一旦啟動,我們可能需要:讀取本地的用戶數(shù)據(jù),讀取文件存儲的(廣告)圖片,請求token是否失效,請求一些公有數(shù)據(jù),內(nèi)存緩存... ,眾所周知,這些操作場景都是比較耗時的,且一般我們都是異步去處理的,以及有時候我們必須等這些耗時操作返回數(shù)據(jù)后,才能進(jìn)行下一步操作。
當(dāng)然,閃屏頁就是為了解決耗時異步的場景而閃亮登場的。其目的就是:利用閃屏頁,友好的來等待異步耗時數(shù)據(jù)的返回,根據(jù)數(shù)據(jù)返回絲滑的過渡到目標(biāo)頁面,從而增大用戶體驗。
這里筆者就拿程序一旦啟動,讀取本地用戶信息(耗時),根據(jù)用戶信息有無,來顯示不同界面(主頁或登錄)的常見場景,進(jìn)一步來說明閃屏頁的妙用。
偽代碼如下:
// 獲取用戶數(shù)據(jù) 耗時操作 異步請求
await final userInfo = _fetchUserInfo();
if (userInfo != null) {
// 有用戶數(shù)據(jù),跳轉(zhuǎn)到主頁
} else {
// 沒有用戶數(shù)據(jù),跳轉(zhuǎn)到登錄頁
}
方案一:main函數(shù)處理
偽代碼如下:
void main() async {
// 獲取用戶數(shù)據(jù)
await final userInfo = _fetchUserInfo();
if (userInfo != null) {
// 有用戶數(shù)據(jù),跳轉(zhuǎn)到主頁
} else {
// 沒有用戶數(shù)據(jù),跳轉(zhuǎn)到登錄頁
}
}
優(yōu)點:無需增加閃屏頁(中轉(zhuǎn)頁),代碼邏輯比較清晰
缺點:只適合耗時比較短的異步請求(100ms之內(nèi)),否則程序一啟動,會有肉眼可見的卡頓,影響用戶體驗。
方案二:閃屏頁處理
程序一啟動,立即切換到閃屏頁,閃屏頁初始化的時候異步獲取用戶數(shù)據(jù),且閃屏頁默認(rèn)展示跟iOS或Android一致的啟動頁,從而迷惑用戶認(rèn)為App正常啟動的錯覺,從而無形之中提高了用戶的體驗。
一旦耗時的數(shù)據(jù)異步返回了,然后再去絲滑的切換頁面。
代碼實現(xiàn)如下:
// SplashPage.dart
class _SplashPageState extends State<SplashPage>{
@override
void initState() {
super.initState();
// 初始化
initAsync();
}
// 異步初始化
void initAsync() async {
// 獲取用戶數(shù)據(jù)
await final userInfo = _fetchUserInfo();
if (userInfo != null) {
// 有用戶數(shù)據(jù),跳轉(zhuǎn)到主頁
} else {
// 沒有用戶數(shù)據(jù),跳轉(zhuǎn)到登錄頁
}
}
@override
Widget build(BuildContext context) {
// 返回啟動頁
return LaunchImage()
}
}
優(yōu)點:極大的增強(qiáng)了用戶體驗,且拓展性強(qiáng),以此可以衍生出引導(dǎo)頁、廣告頁...等常見業(yè)務(wù)場景,下面會一一說到。
綜上所述,側(cè)面驗證了,閃屏頁一般是用來友好的等待異步耗時的數(shù)據(jù)返回,根據(jù)數(shù)據(jù)返回絲滑的過渡到目標(biāo)頁面,從而極大的增強(qiáng)用戶體驗而產(chǎn)生。
用途
閃屏頁在現(xiàn)實場景中,使用是非常廣泛的,筆者相信一款正常的App都會使用到閃屏頁,且大多數(shù)用于程序啟動時啟動頁、引導(dǎo)頁、廣告頁、等待頁等業(yè)務(wù)場景。
這里筆者借用實現(xiàn)微信App啟動的邏輯,來闡述一下閃屏頁的用途,希望大家能夠舉一反三,能夠?qū)⑵溆糜诂F(xiàn)實的開發(fā)場景中去。
微信啟動邏輯圖(開局一張圖,內(nèi)容全靠編...)

上面就是筆者整理的微信登陸邏輯,大家可以打開你手機(jī)上的微信App,逐個驗證各個邏輯。當(dāng)然微信是沒有廣告頁的,筆者這里增加廣告邏輯只是為了滿足業(yè)界通用App的邏輯罷了,微信的做法是:v1 == v2 => 根據(jù)account和userInfo判斷 => 切換頁面,可見,微信的登陸比上面邏輯圖更簡單,這里筆者就不一一贅述了。
其次,筆者相信,上面的邏輯圖,應(yīng)該能滿足業(yè)界80%以上的app啟動邏輯,大家如果有任何疑問或者有更好的解決方案,歡迎留言交流,謝謝。
最后,相信有了邏輯圖,大家寫起代碼也比較胸有成竹了,也希望大家在寫代碼之前,先寫好流程圖,避免像無頭蒼蠅一樣,毫無目標(biāo)性。記住:在錯誤的道路上,停止就是前進(jìn)!
代碼
/// 閃屏跳轉(zhuǎn)模式
enum MHSplashSkipMode {
newFeature, // 新特性(引導(dǎo)頁)
login, // 登陸
currentLogin, // 賬號登陸
homePage, // 主頁
ad, // 廣告頁
}
/// 閃屏界面主要用來中轉(zhuǎn)(新特性界面、登陸界面、主頁面)
class SplashPage extends StatefulWidget {
SplashPage({Key key}) : super(key: key);
_SplashPageState createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> {
/// 跳轉(zhuǎn)方式
MHSplashSkipMode _skipMode;
/// 定時器相關(guān)
TimerUtil _timerUtil;
/// 計數(shù)
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();
// 監(jiān)聽部件渲染完
/// widget渲染監(jiān)聽。
WidgetUtil widgetUtil = new WidgetUtil();
widgetUtil.asyncPrepares(true, (_) async {
// widget渲染完成。
// App啟動時讀取Sp數(shù)據(jù),需要異步等待Sp初始化完成。必須保證它 優(yōu)先初始化。
await SpUtil.getInstance();
// 獲取一下通訊錄數(shù)據(jù),理論上是在跳轉(zhuǎn)到主頁時去請求
ContactsService.sharedInstance;
// 讀取一下全球手機(jī)區(qū)號編碼
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;
// 跳轉(zhuǎn)路徑
String skipPath;
// 跳轉(zhuǎn)模式
MHSplashSkipMode skipMode;
if (Util.isNotEmptyString(rawLogin) && currentUser != null) {
// 有登陸賬號 + 有用戶數(shù)據(jù) 跳轉(zhuǎn)到 主頁
skipMode = MHSplashSkipMode.homePage;
skipPath = Routers.homePage;
} else if (currentUser != null) {
// 沒有登陸賬號 + 有用戶數(shù)據(jù) 跳轉(zhuǎn)到當(dāng)前登陸
skipMode = MHSplashSkipMode.currentLogin;
skipPath = LoginRouter.currentLoginPage;
} else {
// 沒有登陸賬號 + 沒有用戶數(shù)據(jù) 跳轉(zhuǎn)到登陸
skipMode = MHSplashSkipMode.login;
skipPath = LoginRouter.loginPage;
}
// 這里無需更新 頁面 直接跳轉(zhuǎn)即可
_skipMode = skipMode;
// 跳轉(zhuǎn)對應(yīng)的主頁
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 設(shè)計稿的寬度和高度(單位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)
// 配置設(shè)計圖尺寸,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) {
// 引導(dǎo)頁
child = _buildNewFeatureWidget();
} else if (_skipMode == MHSplashSkipMode.ad) {
// 廣告頁
child = _buildAdWidget();
} else {
// 啟動頁
child = _buildDefaultLaunchImage();
}
return Material(child: child);
}
/// 默認(rèn)情況是一個啟動頁 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(
// 這里設(shè)置顏色 跟啟動頁一致的背景色,以免發(fā)生白屏閃爍
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(
// 這里設(shè)置顏色 跟背景一致的背景色,以免發(fā)生白屏閃爍
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');
// 跳轉(zhuǎn)到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),
),
),
),
)
],
);
}
}
總結(jié)
閃屏頁的功能雖然很簡單,但是作用卻非常大,希望大家通過閱讀本篇文章,能夠更好地認(rèn)識閃屏頁,了解他的由來,知道他的用途,并將其優(yōu)點運(yùn)用到實際開發(fā)中去,能夠?qū)崒嵲谠诘慕鉀Q現(xiàn)實中的問題。
- 由來:用來友好的等待異步耗時的數(shù)據(jù)返回,再根據(jù)數(shù)據(jù)返回絲滑的過渡到目標(biāo)頁面,從而極大的增強(qiáng)用戶體驗而產(chǎn)生。
- 用途:一般是用在App啟動時,承載啟動頁、引導(dǎo)頁、廣告頁、等待頁等業(yè)務(wù)場景。
期待
- 文章若對您有些許幫助,請給個喜歡??,畢竟碼字不易;若對您沒啥幫助,請給點建議??,切記學(xué)無止境。
- 針對文章所述內(nèi)容,閱讀期間任何疑問;請在文章底部評論指出,我會火速解決和修正問題。
- GitHub地址:https://github.com/CoderMikeHe
- 源碼地址:flutter_wechat



