UI命中:布局与输入必须使用同一坐标快照

自动关联目录:UI命中:布局与输入必须使用同一坐标快照

核心判断:渲染位置与命中位置必须由同一个 Viewport/Safe Area 快照推导。布局晚一帧可以接受,用新坐标命中旧布局不可以。

一、业务冲突:布局适合批处理,输入却要求当场正确

自定义 UI 支持比例加偏移的布局、旋转、裁剪、滚动与安全区。属性连续变化时,每次都重排整树成本过高,适合在帧末合并;但 Begin 到达时必须立即知道当前最上层可交互节点,不能命中上一帧的位置。

移动端还同时存在设备安全区与核心 UI 安全区。Screen 根可以选择忽略 Inset,若布局和输入各自读取当前窗口数据,同一个点会在两套矩形之间漂移。

二、实际实现:Dirty 合并,命中前同步 Flush

工程用 Dirty 标记与帧末 Ticker 合并布局,递归计算绝对位置、尺寸与旋转,再把结果同步回脚本可见属性。指针到达时调用 EnsureLayoutFlushed,保证 Hit Test 不会消费旧布局。

布局和输入共享同一份 Inset Snapshot。Screen 根明确选择 DeviceSafe、CoreUISafe 或忽略 Inset,输入样本携带 Viewport Identity、Generation 和 Inset Revision;修订不匹配时拒绝命中,而不是用当前 Inset 重算旧事件。

flowchart LR
    C[UI 属性变更] --> D[Dirty Roots]
    D --> F[帧末批量 Layout]
    P[Pointer Begin] --> V{Layout Revision 已追平?}
    V -- 否 --> S[同步 Flush 必要根]
    V -- 是 --> H[Hit Graph]
    S --> H
    I[同一 Inset Snapshot] --> F
    I --> H

三、值得保留:正确性屏障只放在真正需要的位置

普通属性变化延迟到帧末,避免同一帧重复重排;只有命中、脚本读取绝对布局或必须提交显示时才同步追平。这比“所有变化立刻布局”和“永远下一帧生效”都更符合业务。

指针捕获以 Source + PointerId 为键。Begin 选中对象后,Move/End 继续发给该对象,即使它移出命中区域;终止事件释放捕获。命中拓扑与输入生命周期因此能够闭合。

四、结构性缺陷:单一 Dirty 位无法描述成本与依赖

全局 Dirty 只能回答“有变化”,不能回答哪些根节点、哪类依赖需要重算。文本测量改变尺寸、父节点裁剪改变命中、图片晚到改变 DesiredSize,它们影响范围不同;全部同步 Flush 会把一次点击变成不可预测的主线程尖峰。

布局结果回写脚本属性也可能形成反馈环:引擎计算 AbsoluteSize,脚本监听变化后修改源属性,再次失效布局。若没有 SourceRevision 与只读派生字段,批处理会掩盖循环,而不会消除循环。

五、目标设计:增量 Layout Graph 与版本化 Hit Graph

为每个节点记录 Measure、Arrange、Clip、Paint、Hit 五类 Revision 及反向依赖。变更只传播到必要祖先或后代;Hit Graph 引用已提交 LayoutRevision。输入到达时,只 Flush 与候选屏幕根相关的脏子图,并设置同步工作预算;超过预算则推迟 Begin 或降级到最近已提交一致快照,绝不混用版本。

绝对位置和尺寸作为只读派生值发布,携带 LayoutRevision;脚本写入只能修改源约束。安全区变化创建新 CoordinateSnapshot,旧指针会话继续使用旧快照直至 End/Cancel,新 Begin 才进入新版本。

HitResult HitTest(const PointerSample& p)
{
    if (!coordinateSnapshots.Contains(p.insetRevision))
        return HitResult::Reject(StaleCoordinateDomain);
    layout.FlushRequiredRoots(p.viewportId, SyncBudget);
    HitGraph& graph = hitGraphs.Require(layout.committedRevision);
    return graph.Query(p.uiSafePosition);
}

六、预算与排查:测同步追平,不只测平均布局时间

指标定义用途
DirtyRoots每帧被失效的独立根数判断失效是否扩散
SyncFlushCost输入触发的同步布局耗时解释点击卡顿
NodesRecomputedMeasure/Arrange 实际节点数验证增量效果
HitRevisionLagHit 与最新文档 Revision 差证明一致性
CaptureWithoutTerminal未收到 End/Cancel 的捕获数查按钮卡死
InsetMismatchRejects坐标快照不匹配次数查旋转/缩放竞态

跨平台矩阵至少覆盖鼠标悬停、触摸多指、控制器焦点、窗口缩放、刘海安全区、分屏 Viewport 和高 DPI。目标不是所有平台走相同事件,而是最终命中使用相同版本化几何语义。