Flutter 玩转微信——闪屏页妙用

顺心 等级 146 0 0

概述

  • 众所周知,一个健全的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启动时,承载启动页、引导页、广告页、等待页等业务场景。

期待

  1. 文章若对您有些许帮助,请给个喜欢❤️,毕竟码字不易;若对您没啥帮助,请给点建议💗,切记学无止境。
  2. 针对文章所述内容,阅读期间任何疑问;请在文章底部评论指出,我会火速解决和修正问题。
  3. GitHub地址:https://github.com/CoderMikeHe
  4. 源码地址:flutter_wechat

参考链接

拓展

本文转自 https://www.jianshu.com/p/e2dcd0e8e04d,如有侵权,请联系删除。

预览图
收藏
评论区