作者:何凯俊github 不要冒然评价我,你只知道我的名字,却不知道我的故事,你只是听闻我做了什么,却不知我经历过什么。
俗话说得好,产品有三宝,弹窗浮层加引导。
上图截图自我司 App 晓黑板中的口算模块,相信每个 App 开发在工作中都碰到这种场景,为了避免用户对新功能产生困惑,会对一些功能加一些引导操作。在原生开发中,例如 Android 开发中,我们可以使用 NewbieGuide 等开源库来实现。但是很遗憾的是,在 Dart packages 中找了一圈,一无所获。
但是我们还是很快就解决了问题,既然解决不了问题,我们就要学会让这个问题不存在,这时候开发一宝就显得尤其有用了。
本文完,大家下期再见👋
~
~
~
开个玩笑。真的猛男,敢于直面惨淡的人生,也敢于正视淋漓的鲜血,这区区需求怎么能打倒我们。接下来我们开始琢磨一下这个引导操作要怎么实现,相信各位小伙伴接到这个需求第一个想到的就是,这玩意儿不就是在整个页面上面盖一个蒙层,然后把中间再抠一块出来。最后再加一些文字和一个下一步按钮就行了嘛。你要是把这个需求想得这么简单,那你可就真是大对特对了。所以我们就按前面说的三步来实现这个东西。当然,你如果不想接着往下看的话,可以直接点击这里使用我们开发完成的引导组件库来快速在你的 Flutter 项目中接入引导功能。
那么如何在 Flutter 页面上盖一个浮层呢?翻了一下 Flutter 的 API 文档,找到了两个法宝,分别是 Overlay 和 OverlayEntry。两者的使用方法如下。
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 的用法'), ), ], ), ); } } 复制代码如果你不嫌烦的话,还可以点击这里亲自试一试效果。
上面解决了浮层问题,下面我们就来看一下如何让蒙层中间某一块区域亮起来的问题,这个问题很简单,为什么会亮,因为有光,怎么有光。我想到了上帝,因为上帝说要有光,于是就有了光。
接下来就是如何成为上帝的第一步,我们要找到需要高亮的那个组件的大小和位置,经过我缜密的调查,发现在 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 中有 width 和 height 属性,分别表示高亮组件的宽高属性,Offset 中有 dx 和 dy 属性,分别表示组件左上角距离屏幕左侧和顶部的距离。相信做 Web 开发的同学对这个都很熟悉了。
如果你不嫌烦的话,还可以点击这里亲自试一试效果。
我们已经在代码层面定位到这个组件的位置了,接下来就是对该区域进行精准打击,让这块区域不被浮层的颜色所覆盖,请看成为上帝的第二步。
先看结果,可以看到中间的组件没有被遮罩层遮住,但是有眼睛的同学可能会发现,为啥上下还会各有一段也没被遮住,那是因为 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, ), ], ), ), ], ), ); 复制代码上面可以看到,我们使用了 Stack 和 Positioned 来实现将组件放到我们想要的位置,然后实现高亮的核心组件是 ColorFiltered,ColorFiltered 可太好了,我可太喜欢这个组件了,它能做的事情也很有趣,后面我们还再出一篇文章去单独地介绍它。敬请期待叭~
然后说一说为啥我们的截图是移动端,不再是 Web 端,因为 ColorFiltered 这哥们太强大,以至于 Flutter 团队在 Flutter Web 上还没有完全实现它。你可以在 Flutter 仓库里面的随便找到不少关于 ColorFiltered 在 Web 上表现异常的 Issue。
当然如果你不嫌烦的话,而且也愿意在 Web 上试一下没有效果的效果,我也很贴心的为你准备了在线链接。
终于到了最后一步,加上下一步按钮和文字,这就不用说了,创建 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。点击这里查看最新版本。
下图为 flutter_intro 支持的一些参数配置所对应的位置介绍:
好熟悉的操作,和上面介绍的一模一样。
当然,这里为了方便大家使用,库的内部为大家创建好了 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。
而且我又欠了你们一篇介绍 ColorFiltered 的文章。
我们是好未来·晓黑板 Flutter 团队,期待下次再见👋。
作者:晓黑板前端技术 链接:https://juejin.im/post/6877732301896155143 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。