← 返回索引

范围:下拉、右键菜单、对话框与提示气泡的推入、浮层对鼠标的遮挡与外部点击关闭判定、overlays().push 各入口的默认值、悬停扫过浮层外部不关闭的原因。

前置:快速开始;Screen 需持有 interactionManager 字段。


覆盖层的定义

覆盖层(Overlay)是推入一棵独立浮层树的元素,与屏幕的根元素树并列存在:根树是 FluentScreen 子类创建并持有的 LayoutElement,浮层树由 Screen 持有的 OverlayManager 封装(InteractionManager 只借用引用做路由,不拥有它)。推入浮层不改变元素在根树中的位置,因为 Tooltip、ContextMenu、Dialog 这类元素本就不是根树的子节点。

浮层树是多根森林(OverlayTree):每个根是一个独立浮层,后推入的根渲染在上层。视觉堆叠与输入优先是同一机制的两个方面:

  • 渲染按加入顺序执行:根先绘制、后加入的根覆盖在先加入的根之上,每个根内部父节点先绘制、子节点后绘制;
  • 输入先经 topmostAnywhere 取最上层命中,再沿父链向上分发:最后推入的根最先获得事件。

这两个方向互逆:渲染自下而上叠加,输入先取最上层命中再沿父链向上分发(OverlayManager.renderOverlay 的实现注释)。浮层树的结构与遍历细节见覆盖层系统。


浮层推入 API

元素进入浮层树的入口是 push 与 remove。组件内部通常经 InteractionManager.active.overlays() 取得借用引用,再调用 OverlayManager 的 push/remove。所有权与访问路径如下:

组件 (Tooltip / SelectDropdown / AutoCompleteBox / ContextMenu / MessageFlyout)
  │  经 InteractionManager.active.overlays() 取借用引用
  ▼
OverlayManager          ← 所有权:Screen;增删入口 push/remove;渲染入口 renderOverlay
  ▼
 OverlayTree             ← 纯数据结构:多根森林、索引、遍历算法

InteractionManager      ← 仅输入路由:分发 / 清扫 / 命中(overlays.tree().…)
                           不提供浮层推入/移除转发

overlays() 返回 Screen 持有的浮层管理器(借用引用):浮层的推入与移除经此访问,InteractionManager 本身不持有该管理器。Screen 与 OverlayManager 的构造归属见根树与层级系统。

手动推入

典型推入方式如下;每种方式得到的标志(dismissOnOutsideClick)在代码后说明:

OverlayManager overlays = InteractionManager.active.overlays();

// ① 单参:最常用的推法
overlays.push(menu);

// ② 双参:显式指定外部关闭
overlays.push(menu, /* dismissOnOutsideClick */ false);

// ③ 带父浮层的子浮层:parent 指向直系父元素,留空等价于不传入
overlays.push(parentMenu, subMenu);
overlays.push(parentMenu, subMenu, /* dismissOnOutsideClick */ true);

// 移除:连子树一起摘掉
overlays.remove(menu);

单参 push(el) 的默认值是 dismissOnOutsideClick=true,结果为外部点击关闭。双参 push(el, dismissOnOutsideClick) 显式给出标志。remove(el) 移除该浮层及其整棵子树。

带父浮层的推入统一写作 push(parent, child) 或 push(parent, child, dismissOnOutsideClick):parent 只保存直系父元素,不记录最外层父元素。子浮层沿父指针向上或向下遍历父链,parent 为空表示该浮层为顶层浮层。OverlayTree 不维护 children 列表;关闭与豁免判定时按父指针反复扫描层列表,层数通常只有十几个,扫描成本明显低于另外建表的维护成本。

Manager 与树侧的默认值对照

OverlayManager 的单参与双参 push 转发到 OverlayTree 的对应重载,两边的默认值以源码为准,对照如下:

入口签名dismissOnOutsideClick
Manager 单参overlays.push(el)true
Manager 双参overlays.push(el, dismiss)传入值
树双参tree.push(child, dismiss)传入值
树带父tree.push(parent, child, dismiss)传入值
Manager 带父overlays.push(parent, child)true
Manager 带父overlays.push(parent, child, dismiss)传入值

Manager 单参的默认值为外部点击关闭。树侧第二参是 dismissOnOutsideClick(类型 boolean),读签名即可分辨。

组件的自挂载

浮层组件在自身的 show() 中经 overlays() 推入。各组件的实际取值:Tooltip 经 overlays().push(current, false) 推入(不外关,通过悬停丢失关闭);ContextMenu 用 overlays().push(this)(外关);MessageFlyout 用 overlays().push(this)(外关);SelectDropdown、AutoCompleteBox 经 overlays().push(...) 单参推入(外关)。

子浮层的父链与级联关闭

子浮层不是独立概念,而是同一个 OverlayTree 中的一条记录,其 parent 字段指向直系父元素。推入子浮层时使用带父参数的入口:

// 父菜单已打开,再弹出子菜单
overlays.push(parentMenu, childMenu);

// 父菜单关闭时,会级联移除所有子层
overlays.remove(parentMenu);

parent 只保存直系父元素,为空表示顶层浮层。该字段提供两项行为:一、父层被 remove 时,其下属子层一并移除;二、本次点击命中子浮层时,父浮层不被外部关闭流程误删。实现细节见覆盖层系统。


外部关闭开关 dismissOnOutsideClick

浮层推入后的外部关闭行为由 dismissOnOutsideClick 决定:

  • true(默认):在浮层子树之外产生的明确输入(点击、松开、拖拽、滚轮)会使其关闭。典型对象为下拉面板、自动补全、消息条、右键菜单,即点击其他位置时自动关闭。
  • false:浮层保持显示,除非由代码显式移除。典型对象为 Tooltip(通过悬停丢失关闭,不通过点击)与不响应外部点击关闭的被动浮层。

默认值固定为 true,单参推入均使用该值;需要保持显示时必须显式传入 false。


外部关闭的触发条件

dismissOnOutsideClick=true 的浮层显示期间,在其子树之外产生的明确输入(点击、松开、拖拽、滚轮)会使其关闭。InteractionManager 的四个明确输入分发器,即按下(mousePressed)、释放(mouseReleased)、拖拽(mouseDragged)、滚轮(mouseScrolled),在分发之前调用同一个私有方法 dismissOverlaysOutside。该方法对每棵根浮层各检查一次:跳过不可见的节点;若光标落在该层根元素自身的命中区域内则跳过(subtreeHit);若该节点的 dismissOnOutsideClick 为 false 也跳过;其余节点收到一次 onOutsideInteraction() 调用,Element 的默认实现为空,浮层组件覆写为 close()。清扫在分发之前执行,因此分发过程中新建的浮层在清扫时尚不存在,不受本次外部关闭影响。清扫粒度为“该层根元素是否在光标下“:仅当光标位于根元素自身命中区域内才跳过,不会因子浮层的悬停误关。

父层豁免:本次点击命中某一层浮层时,其直系父层及所有祖先层在本轮外部关闭中均被豁免。豁免判定按层表向外推导:先收集命中层,再沿该层 parent 链逐层向上标记,链上的父层均不进入 onOutsideInteraction() 路径。具体判定顺序见交互流程的外关流程。

悬停不会触发外部关闭。 鼠标移入、移出或移动扫过浮层外部时,浮层不会关闭,这是刻意设计。Element.onOutsideInteraction 的 javadoc 写明它是“明确输入专属,悬停不触发“,只响应点击、松开、拖拽、滚轮这四类明确输入。因此 Tooltip 需要另一套机制,由组件在悬停丢失时自行移除;仅依靠悬停进出关闭的浮层不会关闭。确认方式为检查 InteractionManager 中 dismissOverlaysOutside 的调用点:它们分别位于四个明确输入分发器内,而 updateHover(悬停路径)中没有任何调用。


可自动关闭的下拉面板示例

示例为继承 Element 的面板,以默认外部关闭语义推入,关闭时从树中移除。

public class MyPanel extends Element<MyPanel> {
    public void show() {
        InteractionManager.active.overlays().push(this);
    }
    public void close() {
        visible(false);
        InteractionManager.active.overlays().remove(this);
    }
}

该浮层使用外部关闭语义:点击浮层外部会触发 onOutsideInteraction → close() 关闭自身。若要使浮层在点击外部时不关闭,推入时传入 push(this, false)。remove 必须调用,否则 close() 仅隐藏节点,节点仍留在树中(隐藏节点不参与命中,但仍接收 tick 调用)。


不变量

  1. 外部关闭只响应明确输入。 按下、松开、拖拽、滚轮四类输入会触发外部关闭清扫;悬停(移入/移出/移动)永远不触发。改动 dismissOverlaysOutside 的调用点或给悬停路径加调用,都直接违反这条不变量。
  2. 默认组合为外部关闭。 overlays().push 单参将 dismissOnOutsideClick 固定为 true;任何源码版本中出现“默认不外关“的推入入口均视为回归。
  3. 浮层树所有权在 Screen。 OverlayManager 由 Screen 构造并持有,InteractionManager 只借用引用做路由(分发/清扫/命中遍历),不管理生命周期;overlays() 提供借用引用而非所有权,推入与移除均在 Manager 上执行。所有权的完整论证在根树与层级系统。

改动波及

  • 改 push 重载的默认值(任何一个少参重载里写死的 true/false):波及全库所有按旧默认推入的弹层,下拉、补全、消息条、右键菜单的行为同时翻转,且多数调用方无法察觉自身依赖默认值。修改前须核对取值表与全部调用点。
  • 改 dismissOverlaysOutside 的调用点(增删那四行、或把它挪到分发之后):波及所有可外关浮层,要么全部无法关闭,要么在分发过程中新建的浮层被随后的外部关闭判定立即关闭;悬停路径若被加入调用点,违反不变量 1。

相关文档