范围:输入路由的修改与排查。所有鼠标、键盘、焦点与覆盖层事件都从
InteractionManager这一个入口进入框架。内容包括:一次点击按优先级依次经过的捕获、挂起、外部关闭、覆盖层与根树;命中检测 H0/H1/L1/H3/L3 各层的职责与覆写选择;焦点与鼠标捕获状态的持有者与清除时机。
InteractionManager
整个框架的单一事件入口点。Screen 仅转发事件;所有路由逻辑都在该类中。
public class InteractionManager {
private final LayoutElement<?> root; // 根元素树
private final OverlayManager overlays; // 覆盖层系统(借用引用)
private Focusable focusedTarget; // 当前焦点(单一目标)
private Element<?> pressOwner; // 当前按下目标
private Element<?> mouseCapture; // 拖拽捕获所有者
private int suspendLevel; // 全局挂起计数器
public static InteractionManager active; // 全局单例
}
初始化
在 Screen 子类中:
@Override protected void init() {
overlays = new OverlayManager();
interactionManager = new InteractionManager(root, overlays);
}
// 将所有输入事件转发给 interactionManager
@Override public boolean mousePressed(double mx, double my, int btn) {
return interactionManager != null && interactionManager.mousePressed(new MousePressedEvent(mx, my, btn, Modifiers.NONE));
}
碰撞检测层级
碰撞检测按由简至繁的层次划分:
| 层级 | 方法 | 描述 |
|---|---|---|
| H0 | Element.rectHit(el, mx, my) | 纯几何矩形命中。不可覆盖。 |
| H1 | el.shapeHit(mx, my) | 有效形状命中。唯一可覆盖的点。 默认 = H0。用于动画几何形状。 |
| L1 | Element.isHit(el, mx, my) | 形状命中 ∩ 祖先裁剪边界。统一谓词,用于分发门控、外部关闭和块查询。 |
| H3 | Hit.hitOrder/hitPath/topmost | 单树搜索。hitOrder = 广度优先(所有命中,自上而下)。hitPath = 深度优先(根→最深)。topmost = 路径终点。 |
| L3 | topmostAnywhere(mx, my) | 全 UI 搜索:先覆盖层树(topmostInTree),再根树。 |
关键不变量: isHit = shapeHit(mx, my) && isMouseWithinClipBounds(mx, my)。裁剪边界检查沿父元素链向上查找 HIDDEN 或 SCROLL 容器,并验证点在其边界内。
鼠标分发优先级
mousePressed 按固定优先级顺序处理(第一个 true 获胜):
0. BLOCK_RIGHT 守卫 — 如果 pressOwner 有 BLOCK_RIGHT,右键事件不再派发
1. mouseCapture — 被捕获元素获得所有事件(忽略碰撞检测)
2. suspendLevel > 0 — 跳过所有内容,返回 false
3. dismissOverlaysOutside — 在分发前通知覆盖层外部交互
4. L3 最高命中 — 整个 UI 的单次碰撞检测(工具提示关闭遍历)
5. 焦点转移 — 命中焦点元素 → 获取焦点;未命中焦点族 → 取消焦点
6. 按下链 — 从命中元素沿父链向上,消费者记为 pressOwner
7. 捕获簿记 — pendingCapture 激活为 mouseCapture
mouseReleased / mouseDragged 遵循相同的主干: 捕获 → 挂起 → 关闭 → 枝叶→主干通知式分发。关键区别: 松开与拖拽会先检查 pressOwner 的 CAPTURE_RELEASE / CAPTURE_DRAG 位,命中则路由给 pressOwner。松开到 pressOwner 与捕获所有者时调用 pressReleased(e, hit),hit 是管理器当场按完整悬停仲裁算出的命中;广播路径调用 mouseReleased(e),不带命中信息。
mouseScrolled 与按下同构: 捕获 → 挂起 → 关闭 → 命中元素优先、沿父链回退、第一个消费滚轮的即截断。起点是 topmostAnywhere(...);它为 null 时(点在容器空白处)改用 Hit.deepestLayoutAt(root, mx, my),取矩形含点的最深布局容器。
焦点系统
public interface Focusable {
boolean isFocused();
void defocus(); // "放弃与位置无关的事件接收权"
}
- 单一目标: 任何时候最多一个
focusedTarget - 点击外部取消焦点: 点击焦点族外部触发
defocus() - 焦点族:
FocusableElement.focusFamily是一个Set<Element<?>>(基于标识)。族内的元素被点击时不会触发取消焦点。用于: TextBox 的上下文菜单、AutoCompleteBox 的下拉菜单。 - 键盘路由:
keyPressed/keyReleased/charTyped/preeditUpdated经dispatchKeyboard发送给resolveKeyboardReceiver()解析出的接收者,主路径是focusedTarget(须在焦点上且是KeyboardReceiver);否则从焦点目标(无则从根)沿父链向上找第一个仍在焦点上的KeyboardReceiver,只取沿途第一个 - 无注册表: 焦点通过
setFocusTarget/clearFocusTarget设置/清除,使用标识比较
鼠标捕获
拖拽操作的两阶段激活:
// 阶段 1: 请求(在 mousePressed 中)
InteractionManager.active.requestMouseCapture(this); // pendingCapture = this
// 阶段 2: 激活(在下一次 mousePressed 命中时)
// mouseCapture 变为激活状态,捕获坐标被记录
两阶段是硬约束,不是约定:requestMouseCapture 只写 pendingCapture,真正被提升为 mouseCapture 只发生在 mousePressed 的按下链末尾。所以在悬停回调(mouseEntered)里调它不会生效,即 pendingCapture 只会等待下一次任意按下才激活,此时还会将捕获错误地交给当时已不在悬停的元素。需要「按下落住」的独占(拖动)必须在 mousePressed 中申请。
releaseMouseCapture() 除了把 mouseCapture 置空,还会清零 captureButton、captureStartX/Y、capturePrevX/Y、captureLastX/Y 这七个字段,不清零会使 captureDX() 之类访问器读到上一次手势的残留值。
捕获期间:
mousePressed,mouseDragged,mouseScrolled直接发送给捕获所有者(无碰撞检测)pressReleased(e, hit)发送给捕获所有者然后自动释放- 根据光标是否位于捕获所有者边界内,合成
mouseEntered/mouseExited事件
查询 API: isMouseCaptured(), mouseCaptureOwner(), captureStartX/Y(), captureDeltaX/Y(), captureDX/DY()。
分发数据系统
事件对象
- 鼠标链路使用
MousePressedEventMouseReleasedEventMouseDraggedEventMouseScrolledEvent; - 键盘链路使用
KeyPressedEventKeyReleasedEventCharTypedEventPreeditUpdatedEvent; - 悬停链路使用
HoverEvent。
各输入回调接收对应事件对象,从事件对象字段读取坐标、按钮、修饰键与滚轮偏移。
命中数据与路径
Hit 是命中实现,定义子节点候选、同层命中顺序、最深路径与路径终点。核心方法如下:
| 方法 | 作用 |
|---|---|
Hit.candidates(LayoutElement<?>) | 返回可见且可布局的直接子元素候选。 |
Hit.hitOrder(LayoutElement<?>, mx, my) | 广度优先:当前层所有命中的自上而下顺序。 |
Hit.hitPath(LayoutElement<?>, mx, my) | 深度优先:根到最深元素的路径。 |
Hit.topmost(LayoutElement<?>, mx, my) | 路径终点元素,无则 null。 |
InteractionManager 持有根树与浮层管理器两个字段:root 与 overlays。
浮层树在 OverlayTree 内放入 Layer 记录列表,推入、移除与查询都直接作用于列表。
全 UI 命中
InteractionManager.topmostAnywhere(mx, my) 是唯一的全 UI 命中入口。
命中顺序为先询问 overlays.tree().topmostInTree(mx, my),未命中时再计算 Hit.topmost(root, mx, my)。
分发遍历
事件遍历的核心依据是命中元素与其父链,而非容器内部递归。规则如下:
LayoutElement只负责绘制顺序与布局,不保存输入分发状态。InteractionManager的四条鼠标分发链路先依次执行捕获、挂起、外部关闭、全 UI 命中、焦点转移,再按命中链通知元素。dispatchOverlays对每个可见且命中的层根调用元素的事件回调;返回true即不可继续分发。
外部点击关闭
浮层树使用 OverlayTree.Layer 记录。每条记录只包含该层根元素与 dismissOnOutsideClick 标志。
InteractionManager.dismissOverlaysOutside 会遍历层列表,依次判断是否需要发送外部交互通知。
tick 链
tick 与输入是两条互不交叉的链:ui.tick 包下的 UiTickDriver 与 TickWalker 把渲染帧的真实耗时按固定步长累加,每一步遍历根树与浮层树并调用 Element.tick(),不经过上述任何一条输入链路。InteractionManager 没有每帧入口,原先那个按 20 TPS 转发同一条遍历的旧方法已删除(删除记录见 改动记录),驱动器只借用本类读取两棵树。步长取值、遍历口径与不变量以 生命周期系统 为准。
相关文档
- 元素系统:pressFlags 位标志定义
- 覆盖层系统:覆盖层分发
- 根树与层级系统:输入流水线
- 生命周期系统:tick 链的步长、遍历口径与不变量
- Developer: 交互与事件:Screen 输入转发的使用侧口径