Flutter:使用Overlay展示浮动的Widget

想象一下:你编写出的迷人表单页面

你把它发给产品经理,他看了一眼说:“我一定要完整的输入国家名称吗,当我输入文字时难道你就不能给我展示些建议吗?”,你想了想:“好吧,他是对的”,因此,你决定开发一个‘自动补全‘的’预先输入’功能,随便你怎么称呼它:一个文本展示框 TextField ,当用户输入文字的时候展示一些建议选项。开始工作了..你知道怎么拿到建议数据,你知道怎么写逻辑,你知道所有要做的事情..除了不知道怎么将建议选项浮动展示在 Widgets 之上。

你想了想:打算重新设计代码结构,为了实现悬浮效果,决定将整个页面包装进一个 Stack 组件中,你需要准确的计算每个 Widget 显示的位置,非常侵入性、必须要严谨、容易出错,并且直觉告诉你这么做可能是错误的,有其他的实现方式吗?

方案就是:你可以使用 Flutter 已经提供好的 StackOverlay

在这片文章中,我将会介绍如何使用 Overlay ,来创建悬浮在其他 Widget 之上的 Widgets,并且并不需要重构你的整个页面。

你可以使用 Overlay 来展示自动匹配的建议选项,小提示,或着基本上所有的浮动的东西。

Overlay是什么?

官方文档这样定义

Stack of entries that can be managed independently.    // 一个可以独立管理的Stack子类

Overlays let independent child widgets “float” visual elements on top of other widgets by inserting them into the overlay’s Stack.    // 通过将可独立管理的子节点widgets加入到overlay的栈中,Overlays可以将这些widgets浮动展示到显现的elements节点的顶部

这看起来就是我们正在寻找的内容,当我们创建 MaterialApp 的时候,它会自动创建一个NavigatorNavigator 则又会创建一个 Overlay : 一个 Navigator 用来管理所展示的 Views 视图的 Stack 组件。

接下来,让我们一起看看怎么使用 Overlay 来解决我们的问题吧。

注意:这篇文章的核心是介绍如何显示浮动 Widgets,因此不会涉及太多如何实现自动补全文本输入框 TextField 的细节,如果你对一个编写良好、可高度自定义的自动补全 Widget 感兴趣的话,可以参考 flutter_typeahead

初始代码

我们以最初的代码开始吧:

*一个简单页面,包含了三个文本输入框:country、city、address。

然后我们就以 country 文本输入框为例吧,将它封装成一个我们自己的 StatefulWidget,命名为 CountriesField

接下来我们将要做的是,每次当选中文本输入框获取焦点 Focus 的时候,将一个浮动的 List 列表展示出来。当失去焦点 Focus 的时候,再将它隐藏起来,当然你可以按照自己需求来决定如何实现,你可能需要在用户输入了一些文字后展示它,或者当用户点击 Enter 按钮的时候再隐藏。无论怎样,让我们先看看如何展示这个悬浮的 Widget吧:

  • 我们给 TextFormField 绑定了一个 FocusNode,并且在 initState 里面给 FocusNode 添加了一个监听事件,通过监听事件来获取什么时候获得/失去焦点。
  • 每次当我们获取焦点 (_focusNode.hasFocus == true) 的时候, 我们通过 _createOverlayEntry 创建一个OverlayEntry 实例对象,然后通过使用 Overlay.of(context).insert,将它插入到最邻近的 Overlay Widget 中去。
  • 每次当我们失去焦点 (_focusNode.hasFocus == false) 的时候,我们通过使用 _overlayEntry.remove 来移除刚才添加的 Overlay 实例。
  • _createOverlayEntry 通过使用 context.findRenderObject 来获取我们的 Widget 所在的渲染对象 RenderBox ,渲染对象里包含位置 Position、大小 Size、和一些其他关于渲染的信息,有了这些信息能够帮助我们计算在哪里展示我们的悬浮列表。
  • _createOverlayEntry 通过渲染信息来获取当前 Widget 的大小,也可以使用 renderBox.localToGlobal 来获取当前 Widget 在屏幕上的坐标。我们将 localToGlobal 设置为 Offset.zero 这意味着我们将在渲染对象中使用(0,0)坐标,并且将他们转换为屏幕上相对应的坐标。
  • 接着我们创建了 OverlayEntry,这时一个用来将 Widgets 展示到 Overlay 中的 Widget
  • 当前创建的 OverlayEntry 的是一个 Positioned Widget。请牢记 Positioned Widgets 只能被插入到 Stack 中,当然 Overlay 其实也是一个 Stack
  • 我们设置 Positioned Widget 的坐标,给它和 TextField 相同的 X 轴坐标,相同的宽度,相同的 Y 轴坐标,当然为了不遮挡到 TextField,在底部进行了一些偏移。
  • Positioned 内部,我们设置了一个展示建议选项的 ListView(里面默写了例子中的一些国家)。注意到我把所有的内容都包在了 Material 中,关于这样写有两个原因: Overlay 默认不包含 Material Widget,并且很多 Widgets 如果没有有 Material 祖先节点的话不能展示,除此之外 Material 还提供了 elevation 属性,可以让我们给 Widget 设置阴影效果,看起来就像真正浮在上面一样。

以上,我们的建议选择项可以浮在所有 Widget 之上了!

彩蛋:跟随 Widget 滑动!

在我们离开之前,让我们在多学喜一点吧!假如我们的页面是可以滚动,我们可能注意到如下现象:

建议选择列表固定在了屏幕上!在某些场景下你可能的确需要固定的,但是在当前场景中,我们不想要它固定,我们想要它跟随我们的 TextField 一起滚动!

关键词滚动,Flutter给我们提供了两个widget:CompositedTransformFollowerCompositedTransformTarget,简单介绍就是,如果我们关联起一个 follower 和一个 target ,那么无论 target 滚动到哪里,这个 follower 将跟随它一起滚动!为了关联起一个 follower 和一个target,我们需要给他们设置相同的LayerLink.

因此我们需要将建议选择列表用 CompositedTransformFollower 包起来,将 TextFieldCompositedTransformTarget 包起来。然后我们将他们使用想用的 LayerLink 关联起来,这样就可以是建议选择列表跟随 TextField 一起滑动了:

  • 我们将 Material WidgetCompositedTransformFollower 包裹进 OverlayEntry 中,将TextFormField 包裹进 CompositedTransformTarget 中。
  • 我们使用相同的 LayerLink 示例,来关联 followertarget,这样 followertarget 将会在相同的坐标系中,高效的跟随 target 而动。
  • Positioned Widget 中移除了 topleft 属性,因为默认 followertarget 有相同的坐标,因此不在需要。然而我们保留了 width 属性,因为如果不设置的话,follower 将会无限的延伸。
  • 为了不遮挡 TextFormField ,我们给 CompositedTransformFollower 设置了一个offset(和之前一样的原因)。
  • 最后,我们将 showWhenUnlinked 属性设置为 false,当 TextFormField 在屏幕上不可见时,用来隐藏OverlayEntry(比如我们滑动出屏幕底部很远的时候)。

经过这些操作,我们的 OverlayEntry 现在可以跟随我们的 TextField 一起滚动啦!

重要提示:CompositedTransformFollower 仍然有一点问题,当 target 不可见时,即使 follower 已经从屏幕上隐藏了,这个 follower 仍然会响应点击事件,我已经给 Flutter Team 提了 issue ,此 issue 已经关闭,标记为解决。

Overlay 是一个强大的 Widget ,它给我们提供了一个简单易用的的用来展示浮动 Widget 的的 Stack 组件。

参考链接


发布者

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注