• 在Flutter中如何通过向MaterialApp下的Overlay添加“图层”来实现可共享的页面图层
  • 发布于 1周前
  • 62 热度
    0 评论
大家对于 Overlay 可能不会陌生,那么 OverlayPortal 呢?在 Flutter 中可以通过向 MaterialApp 下的  Overlay 添加“图层”,来实现比如「增加一个全局悬浮控件」或者「页面指引」之类的实现,这是因为 Overlay 在 Flutter 里类似于一个“图层管理器”,它的内部有一个 _Theater(剧院),默认情况下每个「Route 页面」都是通过  OverlayEntry 被加入到“剧院”里去展示。

例如我们常用的 Navigator 其实就是使用了 Overlay 来承载「路由页面」,每个打开的 Route 默认情况下是向 Overlay 插入 OverlayEntry 来增加“图层”,每个  OverlayEntry 在层级上互相独立,这也是买个 Route 互不影响的原因之一。

也就是说,之前我们一般都是通过  Overlay 和  OverlayEntry 来实现增加新图层的需要,那这次提到的  OverlayPortal 又是什么东西?事实上 OverlayPortal 也是用来向  Overlay 添加图层的实现,但是它和  OverlayEntry 又有很大不一样,最大的不一样在于它的「可共享页面状态」和「具有页面自限性」。

前面我们聊到,因为每个  OverlayEntry 在  Overlay 下都是平级且“互不影响”,所以当你在页面 A 内唤起一个 新的 OverlayEntry B , 那么 A 是没办法直接通过 InheritedWidget 共享各种状态,因为新的  OverlayEntry B 不属于页面 A ,而是互为平级的  OverlayEntry ,例如下方 Text('Hello') 无法共享“隔壁”的 Theme 。

那么  OverlayPortal 就不一样了,它可以做到「状态和父级相关联」,但是「在图层结构上又相互独立」,从而实现更简单的页面内「屏幕图层」操作,比如页面内的浮动窗口,弹出框等。

备注:Flutter 内置的 OverlayPortal 是受到 flutter_portal 的启发,在去年的 flutter/flutter#105335 中合并 。

举个例子,如下代码所示:
.在页面内定义了一个 DefaultTextStyle 用于往下共享修改后的全局文本样式 fontSize: 20
.增加一个 OverlayPortal 并绑定  OverlayPortalController 用于控制 show 或者 hide
.在  overlayChildBuilder 里返回一个「提示文本」,「提示文本」可以随机出现在屏幕任何位置
.添加 child 显示一个正常的 Text 文本
.点击 onPressed 通过 _tooltipController.toggle 显示和隐藏「提示文本」
class ClickableTooltipWidgetState extends State<ClickableTooltipWidget> {
  final OverlayPortalController _tooltipController = OverlayPortalController();

  final Random random = Random();
   // 堆代码 duidaima.com
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 50,
      width: 300,
      decoration: BoxDecoration(
          color: Colors.blue, borderRadius: BorderRadius.circular(10)),
      child: TextButton(
        ///点击 OverlayPortalController 实现展示和隐藏
        onPressed: _tooltipController.toggle,
        child: DefaultTextStyle(
          //// 共享了 DefaultTextStyle 的 fontSize: 20 修改
          style: DefaultTextStyle.of(context).style.copyWith(fontSize: 20),
          /// 使用了 OverlayPortal 
          child: OverlayPortal(
            controller: _tooltipController,
            /// 通过 overlayChildBuilder 增加图层
            overlayChildBuilder: (BuildContext context) {
              return Positioned(
                right: random.nextInt(200).toDouble(),
                bottom: random.nextInt(500).toDouble(),
                child: const ColoredBox(
                  color: Colors.amberAccent,
                  child: Text('Text Everyone Wants to See'),
                ),
              );
            },
            /// 页面内的 child 
            child: const Text('Press to show/hide'),
          ),
        ),
      ),
    );
  }
}
可以看到,在点击屏幕中间的按键之后, overlayChildBuilder 内的「提示文本」可以随意在屏幕任意位置出现和隐藏,也就是:
「提示文本」的布局和绘制不受页面 Container 的布局约束,因为它是被加入到  Overlay 到“独立图层”
「提示文本」的样式继承了  DefaultTextStyle 往下共享的样式,所以它的状况又可以和当前页面渲染树同步。

再举个例子,比如在  OverlayPortal 显示 「提示文本」 文本的时候,我们关掉页面,此时因为  OverlayPortal和页面是相关联的,所以它会被“直接销毁”,这也是它页面自限性的体现:

那到这里,有没有觉得「很神奇」,  OverlayPortal 是如何做到状态和父级相关联,但是在图层结构上又相互独立的呢?简单来说就是这样一张图,它通过 _RenderLayoutSurrogateProxyBox 存在页面 tree 里面,但是又通过 _RenderDeferredLayoutBox “布局和绘制” 在全局的 OverLay 里:

就是一个  OverlayPortal 内部都有这两个实现对象:
.每个页面的 OverlayEntry 都有持有一个 LinkedList<_OverlayEntryLocation> _sortedTheaterSiblings 的列表
.每个有  OverlayPortal 显示就会有一个  _OverlayEntryLocation ,它相当于是一个 slot ,  OverlayPortal#overlayChildBuilder 相当于是向当前页面的  OverlayEntry 的 _sortedTheaterSiblings 添加了一个  _OverlayEntryLocation

.最后这个 slot 会通过如  _theater._addDeferredChild(child); 触发布局更新

再稍微捋一捋,大概就是:  OverlayPortal#overlayChildBuilder 的最终布局和绘制,其实都是通过 Overlay 的内部统一的  _Theater(剧院)完成,所以它在这个层面上其实和  OverlayEntry 相似,只是 Overlay 是通过 slot 等方式 “间接” 参与,本身它还是存在于页面的 tree 下面。

而从层级上来说:
在 Overlay 中, OverlayPortal 通常位于最靠近它的 OverlayEntry(一般就是页面 Route) 之后,并在下一个 OverlayEntry 之前,所以它可以存在于当前页面任意位置,又不会遮挡到下一个页面

当 OverlayEntry 具有多个关联的 OverlayPortal 时,它们之间的绘制顺序是调用 verlayPortalController.show 的顺序

所以可以看到, OverlayPortal 主要是为我们补充了「页面内全局图层」的场景,因为它可以做到状态和父级相关联,但是在图层结构上又相互独立 ,适当使用  OverlayPortal 替代  OverlayEntry ,可以让我们更灵活搭配各种页面内的渲染场景,比如图层,指引,甚至通过局部图层来实现切换动画:

用户评论