← 返回索引

范围:悬停查询、回调覆写、路由冻结、弹出菜单的外关豁免,以及七个输入回调的默认返回值、suspend() 的语义、focusFamily 的作用与错误覆写的症状。

前置:快速开始(八条输入转发方法)与核心概念。


转发与响应

转发由 Screen 层完成:FluentScreen 基类只自动处理 mouseMoved(鼠标移动即刷新悬停归属),其余八条输入方法必须由 Screen 子类转发给 InteractionManager。这八条的完整清单与逐条写法见快速开始的「八个输入转发方法」小节。漏转发任何一个的症状是对应输入无法进入树(滚动无响应、键盘不进入输入框);核对方式为清点 Screen 覆写的方法数量。Screen 的方法体不加入自定义逻辑,路由统一由 InteractionManager 处理。

响应由组件实现,包括覆写元素自身的回调、查询悬停与管理焦点。


悬停检测

组件判断“鼠标是否位于自身之上“的唯一入口是静态方法 InteractionManager.hovering(el, mx, my),源码中没有第二个悬停查询入口。

// 在渲染或点击回调里
boolean hovered = InteractionManager.hovering(this, mx, my);

hovering 在有活动管理器时执行全 UI 最上层仲裁(浮层优先,可见性与裁剪均参与),保证“被其他浮层覆盖时不视为悬停“;无活动管理器(如单测)时回退到裸矩形,不抛出异常。rectHit 不可用于该判断,因为裸矩形不考虑遮挡与浮层优先级,下拉面板打开时下层按钮会被判定为悬停。核对处为 hovering 的实现:有活动管理器才进入 isHover 仲裁,active == null 时才回退裸矩形;仲裁细节见交互引擎。

库内组件的悬停分支均调用该方法:Button、ItemView、SelectBox、Slider、ListView、TextBox 等的悬停判断都是 InteractionManager.hovering(this, mx, my)(各组件渲染或回调中的原句)。


Element 回调清单

组件响应输入的回调全部定义在 Element,默认行为如下;覆写需要消费的回调,不改动其他回调:

回调签名要点默认行为
mousePressed(MousePressedEvent)返回 true 表示消费返回 false(不消费)
mouseReleased(MouseReleasedEvent)返回 true 表示消费;广播通知式松开返回 false
pressReleased(MouseReleasedEvent, boolean)按下会话收尾时由管理器调用,hit 表示松开位置是否命中本元素转发到 mouseReleased(e)
mouseDragged(MouseDraggedEvent)返回 true 表示消费返回 false
mouseScrolled(MouseScrolledEvent)返回 true 表示消费返回 false
mouseEntered(HoverEvent)悬停进入空
mouseMoved(HoverEvent)悬停内移动空
mouseExited(HoverEvent)悬停离开空
onOutsideInteraction()明确输入落在浮层外时由管理器调用;点击命中某层浮层时,它的父层与所有祖先层在本轮外关里被豁免(判定见外关流程的父层豁免)空(浮层组件覆写为 close(),见覆盖层与浮动元素)

消费语义:从命中元素沿父链向上逐个尝试,第一个使 mousePressed 返回 true 的元素消费事件,事件不再下放;全部返回 false 时才传递给更下层。因此“返回 true“不只是记录状态,它会阻断后续祖先的消费。

三个悬停回调(Entered/Moved/Exited)是空方法而非布尔返回值,由管理器在悬停归属翻转时主动调用(updateHover 驱动);覆写时只需实现动作,不涉及消费语义。


路由冻结 suspend() 与 resume()

InteractionManager.suspend() 将挂起计数加一,resume() 减一(不低于零),isSuspended() 报告计数是否大于零。挂起计数大于零时,输入分发的各个入口整体短路:点击、释放、拖拽、滚轮、键盘均立即返回,不进入树、不触发外部关闭清扫、不改变焦点。

suspend() 提供全局屏蔽输入的开关,适用于打开全屏模态确认框、播放接管输入的过场,或排查根因时临时屏蔽全部 UI 输入:调用 suspend() 进入屏蔽,调用 resume() 退出屏蔽。调用必须成对:resume() 不会将计数减为负值,但缺少一次 resume() 会使分发永久挂起;排查时先用 isSuspended() 查看计数。

挂起期间悬停归属被清空:updateHover 在 suspendLevel > 0 时将下一悬停归属置 null,暂停范围不限于“明确输入“路径。核对点有两处:四个鼠标分发器开头的 if (suspendLevel > 0) return ... 短路语句,以及 updateHover 中的挂起分支。


focusFamily 的焦点豁免

焦点的取得与失去由点击链的焦点转移算法统一裁定:点中可聚焦元素则获得焦点,点击聚焦元素(及其家族)之外的区域则失去焦点。输入框弹出的右键菜单、候选下拉属于“点中也不算失焦“的特例,因为它们命中的不是输入框本身,按裸规则处理会使正在编辑的输入框失去焦点。focusFamily 是登记这类关联元素的集合:

// 输入框弹出菜单时
inputBox.addFocusFamily(contextMenu);      // 登记:点它,我仍算境内
// 菜单关闭时对称摘除,避免泄漏已关闭浮层的引用
inputBox.removeFocusFamily(contextMenu);

判定入口是 isInFocusFamily(hit):命中自身或集合中任一成员均视为家族内,点击空白区域恒为家族外;集合按同一性(identity)比较,登记对象为实例本身。焦点转移读取 L3 全 UI 命中结果(管理器计算一次并向下复用,见架构原理),因此菜单打开时点击菜单,命中落在家族内,输入框不失焦。isInFocusFamily 的规则仅为 hit == this || focusFamily.contains(hit),没有其他分支。

外关流程的父层豁免

点击、松开、拖拽、滚轮四类明确输入分发之前,InteractionManager 均执行一次 dismissOverlaysOutside。原有规则只检查“该层自身是否被命中“:被命中则不关闭,未被命中且标志为真则关闭。新增豁免规则为:某次输入命中某一层浮层时,其直系父层及所有祖先层在本轮外关判定中全部豁免。判定顺序为“先收集命中层,再沿父链逐层向上标记“;父子关系定义见覆盖层与浮动元素。


错误覆写的症状与确认

症状 A:将逻辑写入 shapeHit 并处理树或裁剪。 shapeHit(H1)是唯一可覆写点,但它只判定该点是否落在自身形状内;裁剪由 L1 isHit 统一合取,树搜索由 Hit 完成。在 shapeHit 中遍历祖先或查询浮层,症状为:滚动容器裁剪外的区域误命中(绕过裁剪合取),或浮层覆盖时下层元素仍报命中(绕过 L1 统一谓词)。核对处为 shapeHit 的 javadoc:其中限定覆写只涉及形状、不涉及裁剪与树。删除覆写体中的 getParent、isMouseWithinClipBounds、OverlayTree 引用,仅保留几何判断,再复测症状。

症状 B:以自缓存的悬停状态代替 hovering。 在 mousePressed 中设置 hovered = true、在其他位置设置 false,会脱离管理器的仲裁:浮层覆盖时旧状态残留(按钮持续显示高亮),或 mouseExited 漏调后状态无法恢复。自查方式为全局搜索 boolean hovered 字段,将手工维护的状态替换为每帧 InteractionManager.hovering(this, mx, my) 的即时查询;库内组件(Button、ItemView 等)均采用该写法。


焦点与键盘

焦点由框架自动管理:点中 FocusableElement 获得焦点,点击家族外元素失去焦点,键盘与 IME 分发给当前焦点目标。判断“是否有文本框正在接收输入“(全局热键抑制常用)的代码如下:

boolean typing = InteractionManager.active != null
        && InteractionManager.active.hasFocusedTextInput();

FluentScreen.isTextInputFocused() 封装了同一判断。键盘回调 keyPressed / charTyped / preeditUpdated 的默认返回 false,输入框类组件覆写这些回调以消费按键;转发仍由 Screen 层的八条方法负责(见快速开始)。


不变量

  1. 悬停查询只有一个入口:组件侧使用 InteractionManager.hovering,源码中没有第二个查询入口。手维护悬停状态或用裸矩形自判,都破坏“管理器统一仲裁遮挡与浮层优先“的性质。
  2. shapeHit 只处理形状:裁剪、树、浮层一概不处理,由 L1 与树搜索统一合成;违反则分发门控、外部关闭、阻断判定三个读取同一谓词的地方同时出错。
  3. 挂起计数必须成对:suspend() 与 resume() 配对,计数归零前所有明确输入入口短路,悬停归属在挂起期间被清空。
  4. 分发与命中链算法以交互引擎为准:标签表(H0/H1/L1/H3/L3 等)与优先级的权威在该文档,表述冲突时以该文档为准。
  5. 转发清单归属于快速开始:Screen 层的转发清单仅在该文档维护。

改动波及

  • 覆写 shapeHit 时处理树或裁剪:波及分发门控、外部关闭、阻断判定,三处共用 L1 谓词,一处被污染则三处全部出错;症状与确认方式见症状 A。
  • 手缓存悬停代替 hovering:波及一切遮挡场景(浮层覆盖、裁剪区外),症状见症状 B;确认方式为将字段查询替换为即时 hovering 后复测覆盖场景。
  • 漏覆写某个输入回调(默认 false 不消费):波及该输入的下层兄弟,事件被放行给更下层,表现为点击上层元素却命中下层元素;对照快速开始的转发方法与回调清单核对。
  • suspend() 漏 resume():波及整个屏幕的输入(全部入口短路),排查先看 isSuspended()。
  • focusFamily 只加不摘:泄漏已关闭浮层的引用,且旧菜单实例仍在集合中,新实例点中时命中不匹配,输入框异常失焦;确认方式是在关闭路径检查 removeFocusFamily 是否对称存在。

相关文档