惊天秘密!如何在 Flutter 项目中实现操作引导

不要冒然评价我,你只知道我的名字,却不知道我的故事,你只是听闻我做了什么,却不知我经历过什么。

俗话说得好,产品有三宝,弹窗浮层加引导。

上图截图自我司 App 晓黑板中的口算模块,相信每个 App 开发在工作中都碰到这种场景,为了避免用户对新功能产生困惑,会对一些功能加一些引导操作。在原生开发中,例如 Android 开发中,我们可以使用 NewbieGuide 等开源库来实现。但是很遗憾的是,在 Dart packages 中找了一圈,一无所获。

但是我们还是很快就解决了问题,既然解决不了问题,我们就要学会让这个问题不存在,这时候开发一宝就显得尤其有用了。


本文完,大家下期再见👋

~

~

~

开个玩笑。真的猛男,敢于直面惨淡的人生,也敢于正视淋漓的鲜血,这区区需求怎么能打倒我们。接下来我们开始琢磨一下这个引导操作要怎么实现,相信各位小伙伴接到这个需求第一个想到的就是,这玩意儿不就是在整个页面上面盖一个蒙层,然后把中间再抠一块出来。最后再加一些文字和一个下一步按钮就行了嘛。你要是把这个需求想得这么简单,那你可就真是大对特对了。所以我们就按前面说的三步来实现这个东西。当然,你如果不想接着往下看的话,可以直接点击这里使用我们开发完成的引导组件库来快速在你的 Flutter 项目中接入引导功能。

盖个蒙层

那么如何在 Flutter 页面上盖一个浮层呢?翻了一下 Flutter 的 API 文档,找到了两个法宝,分别是 OverlayOverlayEntry。两者的使用方法如下。

class _MyWidgetState extends State<MyWidget> {
  OverlayEntry overlayEntry;
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          RaisedButton(
            onPressed: () {
              /// 1. 创建一个 overlayEntry 实例,builder 方法返回一个 Widget
              /// 该 Widget 会被渲染到页面顶层
              overlayEntry = OverlayEntry(
                builder: (context) => Container(
                  color: Colors.white.withOpacity(.4),
                  child: Center(
                    child: RaisedButton(
                      onPressed: () {
                        /// 3. 执行 remove 方法销毁 overlayEntry 实例
                        overlayEntry.remove();
                      },
                      child: Text('点我关闭 OverlayEntry'),
                    ),
                  ),
                ),
              );
              /// 2. 使用 OverlayState.insert 方法来显示 overlayEntry
              Overlay.of(context).insert(overlayEntry);
            },
            child: Text('点我康康 Overlay 的用法'),
          ),
        ],
      ),
    );
  }
}

如果你不嫌烦的话,还可以点击这里亲自试一试效果。

中间抠一块儿

上面解决了浮层问题,下面我们就来看一下如何让蒙层中间某一块区域亮起来的问题,这个问题很简单,为什么会亮,因为有光,怎么有光。我想到了上帝,因为上帝说要有光,于是就有了光。


成为上帝 I(找到组件所在位置)

接下来就是如何成为上帝的第一步,我们要找到需要高亮的那个组件的大小和位置,经过我缜密的调查,发现在 Flutter 中可以通过 GlobalKey 来获取一个元素的大小和位置,核心代码如下:

/// 1. 声明一个 globalKey
final globalKey = GlobalKey();

RaisedButton(
  /// 2. 将 globalKey 绑定到组件上
  key: globalKey,
  onPressed: () {
    ///
  },
  child: Text(
    '点我康康控制台输出',
  ),
);

/// 3. 通过下面的代码来获取组件的尺寸和位置
RenderBox renderBox = globalKey.currentContext.findRenderObject();
Size size = renderBox.size;
Offset offset = renderBox.localToGlobal(Offset.zero);
print(size);
print(offset);

其中 Size 中有 widthheight 属性,分别表示高亮组件的宽高属性,Offset 中有 dxdy 属性,分别表示组件左上角距离屏幕左侧和顶部的距离。相信做 Web 开发的同学对这个都很熟悉了。

如果你不嫌烦的话,还可以点击这里亲自试一试效果。

成为上帝 II(高亮组件所在区域)

我们已经在代码层面定位到这个组件的位置了,接下来就是对该区域进行精准打击,让这块区域不被浮层的颜色所覆盖,请看成为上帝的第二步。

先看结果,可以看到中间的组件没有被遮罩层遮住,但是有眼睛的同学可能会发现,为啥上下还会各有一段也没被遮住,那是因为 RaisedButton 上下自带一个 margin,所以代码获取 RaisedButton 的实际占位比看起来要大,有兴趣的同学可以去研究一下怎么去掉这个 margin。又有同学会说了,为啥这个截图是移动端的截图,不是 Web 浏览器上的截图。这个问题问得好,请看下面核心代码。

OverlayEntry(
  builder: (context) => Stack(
    children: [
      /// 我们使用了 ColorFiltered 来实现这个功能
      ColorFiltered(
        colorFilter: ColorFilter.mode(
          /// 遮罩层颜色
          Colors.red.withOpacity(.4),
          BlendMode.srcOut,
        ),
        child: Stack(
          children: [
            Container(
              decoration: BoxDecoration(
                /// 任何颜色均可
                color: Colors.white,
                backgroundBlendMode: BlendMode.dstOut,
              ),
            ),
            Positioned(
              /// 和需要高亮组件的大小和位置均一致
              child: Container(
                /// 任何颜色均可
                color: Colors.white,
                width: size.width,
                height: size.height,
              ),
              left: offset.dx,
              top: offset.dy,
            ),
          ],
        ),
      ),
    ],
  ),
);

上面可以看到,我们使用了 StackPositioned 来实现将组件放到我们想要的位置,然后实现高亮的核心组件是 ColorFilteredColorFiltered 可太好了,我可太喜欢这个组件了,它能做的事情也很有趣,后面我们还再出一篇文章去单独地介绍它。敬请期待叭~

然后说一说为啥我们的截图是移动端,不再是 Web 端,因为 ColorFiltered 这哥们太强大,以至于 Flutter 团队在 Flutter Web 上还没有完全实现它。你可以在 Flutter 仓库里面的随便找到不少关于 ColorFiltered 在 Web 上表现异常的 Issue

当然如果你不嫌烦的话,而且也愿意在 Web 上试一下没有效果的效果,我也很贴心的为你准备了在线链接

成为上帝 III(加上下一步按钮和文字)

终于到了最后一步,加上下一步按钮和文字,这就不用说了,创建 overlayEntry 的时候你愿意在 builder 方法里面返回啥都行。

那么我还漏掉了什么没说呢?认真思考的同学可能想到,我要怎么更新 overlayEntry 呢,引导页一般有多个呀,我不能每次都 remove 掉当前的,然后再 insert 一个新的吧,那样页面肯定会有闪烁。其实如果认真看了OverlayEntry文档的话肯定不会错过这个 markNeedsBuild 方法。这里就不再举个例子了,我太懒了。总之就是如果 builder 的内容有变化,你对 overlayEntry 执行一次 overlayEntry.markNeedsBuild() 就可以了,Flutter 就会重新渲染一次 builder 返回的内容。以此来做到无闪烁切换引导页。

相信完成了上面三步,我们即使没有成为上帝,也能做到有光,让指定区域高亮起来了。一句话总结:

我们使用了 Overlay 和 ColorFiltered 即完成了引导页的制作

我看完了

耐心看完了的小伙伴肯定都觉得作者诚不欺我,确实很简单,这两个组件我都用过。但是:

So,我们贴心地开发了一个小的库,并借鉴了 Web 端的知名引导库 Intro.js 的名字,给它取名为 flutter_intro

少废话,先看东西。

上图即为使用 flutter_intro 的默认主题可以快速实现的引导效果。那么我要怎么使用呢?首先在项目依赖文件 pubspec.yaml 中引入 flutter_intro点击这里查看最新版本。

引入 Intro 并实例化一个对象

import 'package:flutter_intro/flutter_intro.dart';

/// 1. 引入 Intro,实例化一个对象,传入必传的 stepCount 参数
/// 和 widgetBuilder 参数,其中 widgetBuilder 可以使用库内置的
/// 方法,这样就只需传入需要显示的文本即可。
/// 当然如果你不嫌烦的话,也可以自己实现 widgetBuilder 方法
Intro intro = Intro(
  /// 总共的引导页数量,必传
  stepCount: 4,

  /// 高亮区域与 widget 的内边距
  padding: EdgeInsets.all(8),

  /// 高亮区域的圆角半径
  borderRadius: BorderRadius.all(Radius.circular(4)),

  /// 使用库默认提供的 useDefaultTheme 可以快速构建引导页
  /// 需要自定义引导页样式和内容,需要自己实现 widgetBuilder 方法
  widgetBuilder: StepWidgetBuilder.useDefaultTheme(
    /// 提示文本
    texts: [
      '你好呀,我是 Flutter Intro。',
      '我可以帮你在 Flutter 项目中快读实现 Step By Step 引导。',
      '我的用法也十分简单,你可以通过 example 和 api 文档快速掌握和使用。',
      '为了快速实现引导,我也默认提供了一套样式,开箱即用,祝大家使用愉快,再见!',
    ],
    /// 按钮文字
    btnLabel: '我知道了',
    /// 是否在按钮后显示当前步骤
    showStepLabel: true,
  ),
);

下图为 flutter_intro 支持的一些参数配置所对应的位置介绍:

给需要加引导的的 Widget 绑定 globalKey

好熟悉的操作,和上面介绍的一模一样。

当然,这里为了方便大家使用,库的内部为大家创建好了 globalKey,使用的时候只需要通过 intro.keys[下标] 获取就行了。

Placeholder(
  /// 2. 第一个引导页即绑定 keys 中的第一项,以此内推
  key: intro.keys[0]
)

真是太方便了!

全军出击

好了,我们已经做好全部的准备了。输入以下指令,点击运行。

intro.start(context);

没了,是不是很简单。

你这个太丑了

如果你嫌我的默认主题丑,想要自己实现 widgetBuilder 方法,我也可以接受。

final Widget Function(StepWidgetParams params) widgetBuilder;

该方法会在引导页出现时由 flutter_intro 内部调用,并会将当前页面上的一些数据通过参数的形式 StepWidgetParams 传进来,最终渲染在屏幕上的为此方法返回的组件。

class StepWidgetParams {
  /// 返回前一个引导页方法,如果没有,则为 null
  final VoidCallback onPrev;

  /// 进入下一个引导页方法,如果没有,则为 null
  final VoidCallback onNext;

  /// 结束所有引导页方法
  final VoidCallback onFinish;

  /// 当前执行到第几个引导页,从 0 开始
  final int currentStepIndex;

  /// 引导页的总数
  final int stepCount;

  /// 屏幕的宽高
  final Size screenSize;

  /// 高亮组件的的宽高
  final Size size;

  /// 高亮组件左上角坐标
  final Offset offset;
}

StepWidgetParams 提供了生成引导页所需要的所有参数,默认提供的主题也是基于此参数生成引导页。

尾声

天下没有不散的筵席,但是如果你请客,我可以多陪你吃一会儿。

这篇文章主要介绍了如何在 Flutter 中实现操作引导,并且我们基于此封装了一个我们眼里东半球最好用的 flutter_intro


对 Electron 感兴趣?请关注我们的开源项目 Electron Playground,带你极速上手 Electron。

我们每周五会精选一些有意思的文章和消息和大家分享,来掘金关注我们的 晓前端周刊


我们是好未来 · 晓黑板前端技术团队。
我们会经常与大家分享最新最酷的行业技术知识。
欢迎来 知乎掘金SegmentfaultCSDN简书开源中国博客园 关注我们。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341

推荐阅读更多精彩内容

  • Flutter项目目录结构介绍 以我新创建的myflutter项目为例,用vscode打开是这样: Flutter...
    星辰大海_王阅读 1,538评论 0 2
  • 通过实际案列理解 Flutter 中 Key 在其渲染机制中起到的作用,从而达到能在合理的时间和地点使用合理的 K...
    stefanJi阅读 13,698评论 13 50
  • 邂逅FLutter 万物皆是Widget 一般缩进2个空格 文字居中 Widget Center() Materi...
    JackLeeVip阅读 3,088评论 0 4
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,464评论 16 22
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,547评论 0 11