Flutter 玩轉(zhuǎn)微信——閃屏頁妙用

概述

  • 眾所周知,一個健全的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)容全靠編...)

微信登錄.png

上面就是筆者整理的微信登陸邏輯,大家可以打開你手機(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ù)場景。

期待

  1. 文章若對您有些許幫助,請給個喜歡??,畢竟碼字不易;若對您沒啥幫助,請給點建議??,切記學(xué)無止境。
  2. 針對文章所述內(nèi)容,閱讀期間任何疑問;請在文章底部評論指出,我會火速解決和修正問題。
  3. GitHub地址:https://github.com/CoderMikeHe
  4. 源碼地址:flutter_wechat

參考鏈接

拓展

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容