范围:元素基类行为、按下位标志(pressFlags)与容器事件分发。
前置:项目概述(类层次图)。
Element 基类
Element<T extends Element<T>> 是所有 UI 对象的根类。自界定泛型使每个链式 setter 返回具体子类型,因此 .width(80).height(20).margin(m) 可以连续调用。所有组件与容器最终都继承这一个类,其字段与方法构成全框架的公共接口。
核心字段
字段声明按源码顺序排列,注释说明各自的默认值与职责。dirty 默认为 true,新元素创建时即视为需要布局:
protected int x, y, width, height; // 几何信息(相对于父元素)
protected Margin margin; // 外边距(参与父元素的布局计算,Margin.NONE)
protected Element<?> parent; // 父元素引用(由 LayoutElement.addChild 设置)
protected boolean visible = true; // 可见性(隐藏 = 不渲染、不命中)
protected boolean dirty = true; // 惰性布局标志(出生即脏)
protected Font font; // 文本渲染字体(null = 多数组件不渲染文本)
dirty 默认为 true 的原因是构造期通常尚未设置尺寸,第一次 layout() 必须完整执行一次 measure() 与 arrange(),之后由 clearDirty() 复位。
关键方法
方法表列出各方法的签名与调用效果;layout() 是布局流水线的唯一入口,完整机制以 布局系统 为准:
| 方法 | 用途 |
|---|---|
font(Font) | 设置字体(链式 API)。注意它不触发 markDirty,因为换字体不改变几何。 |
size(w, h) / width(w) / height(h) | 设置尺寸并调用 markDirty() |
margin(Margin) | 设置外边距并调用 markDirty() |
visible(boolean) | 显示/隐藏整个子树,并调用 markDirty() |
markDirty() | 置 dirty = true 后沿父链向上冒泡,父容器也随之变脏 |
layout() | 当 dirty 为真时依次执行 measure()、arrange()、clearDirty() |
measure() | 抽象方法:测量自身所需尺寸 |
arrange() | 默认空实现:安排子元素位置 |
extractRenderState(...) | 抽象方法:绘制此元素 |
mousePressed/Released/Dragged/Scrolled(...) | 输入回调,默认返回 false,返回 true 表示消费 |
pressReleased(MouseReleasedEvent, boolean) | 按下会话收尾钩子:管理器在把松开派发给 pressOwner 或捕获者时调用,hit 是当场算出的命中;默认转发到 mouseReleased(e) |
mouseEntered/mouseExited/mouseMoved(...) | 悬停回调 |
tick() | 每帧钩子,默认空实现 |
除了方法,还有两组关键的静态判定。rectHit 是不可覆写的矩形命中,命中链的 H0 层建立在它之上;真正允许子类定制形状的唯一覆写点是 shapeHit,最终的 isHit 等于 shapeHit 的结果再与裁剪区判定 isMouseWithinClipBounds 相与。命中链的完整层级归 交互引擎。
removeFromParent 与 setParent 的区别
把元素从布局树迁往覆盖层时必须调用 removeFromParent(),使旧宿主停止绘制,再由 OverlayTree.push 接管归属。setParent(...) 只改写 parent 这个反向引用,不会改变原容器的 children 列表;removeFromParent() 则在父容器是 LayoutElement 时把它从 children 列表里真正摘除,再把 parent 置空。典型问题是元素迁入覆盖层树之后旧容器仍在绘制它,原因正是只调用了 setParent:旧容器仍持有该子元素,渲染时照常绘制。
UIPoint 锚点定位
Element 上定义了 record UIPoint(int x, int y),并提供 tl()、tr()、bl()、br()、center() 五个便捷方法返回左上、右上、左下、右下、中心锚点,供需要相对某个矩形取参考点的调用方(例如 Canvas 的锚点摆放)使用。
pressFlags 位标志
每个元素可以用一组位标志声明按下期间输入事件的去向,InteractionManager 据此路由。这组位定义在 Element,语义如下:
public static final int CAPTURE_DRAG = 1 << 0; // 拖拽继续送给 pressOwner,即使光标已移出
public static final int CAPTURE_RELEASE = 1 << 1; // 鼠标松开仍路由给 pressOwner(默认开)
public static final int CAPTURE_SCROLL = 1 << 2; // 预留:按下期间滚轮跟随 pressOwner
public static final int BLOCK_HOVER = 1 << 3; // 预留:按下期间屏蔽悬停
public static final int BLOCK_RIGHT = 1 << 4; // 按下期间拦截右键(btn == 1 直接消费)
默认值由 pressFlags() 返回 CAPTURE_DRAG | CAPTURE_RELEASE,即普通元素按下后的拖拽与松开仍路由给 pressOwner,但不处理右键。位本身只是声明,决定「何时检查哪一位」的是 InteractionManager,对应关系为:按下瞬间,它在记录 pressOwner 的同时调用 pressOwner.pressFlags(),将这些位快照保存;右键分发前,路由层检查 BLOCK_RIGHT,若当前有 pressOwner、位为真且按钮为右键,则直接消费,不再向下传递;拖拽分发时检查 CAPTURE_DRAG,把事件交给 pressOwner;松开分发时检查 CAPTURE_RELEASE,并在处理后清零。CAPTURE_SCROLL(1 << 2)与 BLOCK_HOVER(1 << 3)目前只有定义:mouseScrolled 的分发路径并不读取 CAPTURE_SCROLL,悬停判定也不读取 BLOCK_HOVER,因此这两个位目前不产生任何效果;修改滚动捕获或悬停屏蔽语义时,需先补上消费端。
以 Slider 为例说明一次完整覆写。滑条需要在拖动期间持续接收事件,同时抑制悬停态与右键菜单,因此覆写为四个位的并集:
@Override public int pressFlags() {
return CAPTURE_DRAG | CAPTURE_RELEASE | BLOCK_HOVER | BLOCK_RIGHT;
}
按住滑条拖动时,拖拽与松开事件均路由给滑条(CAPTURE 两位),右键在分发前被拦截,不会打开上下文菜单。确认某个位的实际效果时,可在对应分发行下断点,比读声明更直接:位的声明在 Element,消费全在 InteractionManager。
LayoutElement 容器
LayoutElement<T extends LayoutElement<T>> extends Element<T>(62 行)在元素之上增加子元素管理与滚动,是所有布局容器的共同父类。
protected final List<Element<?>> children; // 子元素列表
protected float scrollX, scrollY; // 当前滚动位置
protected float targetScrollX, targetScrollY; // 平滑滚动目标
protected OverflowMode overflowMode = OverflowMode.VISIBLE; // VISIBLE | HIDDEN | SCROLL
addChild 将子元素加入 children、调用 setParent 并执行 markDirty,三步缺一不可:缺少 markDirty 时父容器不会重排,缺少 setParent 时子元素无法取得父链。滚动采用「目标值 + 每帧追赶」的模型:scrollTo/scrollBy 只改 targetScrollX/Y,真正的位移在 tick() 中按 scroll += (target - scroll) * 0.3 逼近,残差小于 0.01 时直接吸附到目标,因此平滑滚动由该模型自动提供,组件无需额外实现。overflow(v) 改裁剪模式时同样会 markDirty。
渲染与事件也由容器统一处理。extractRenderState 先确保自身完成 layout(),再按加入顺序遍历子元素、跳过不可见者。事件经 Hit.topmost 分发:先用 Hit.hitOrder() 得到自顶向下的候选序列,分发给第一个消费者,第一个返回 true 的元素获胜,后续候选不再收到事件;按下目标 pressOwner 也在 mousePressed 中先清空,再记录为最深层命中者。
Margin 外边距写法
外边距参与父元素的布局计算(测量时把 margin 计入占用空间)。三种构造方式覆盖常见场景:
element.margin(Margin.all(8)); // 所有边 8px
element.margin(Margin.symmetric(4, 8)); // 上下 4px,左右 8px
element.margin(new Margin(top, right, bottom, left)); // 各边独立设置
不变量
- setter 是几何变更的唯一入口:
width/height/size/margin/visible等 setter 都会调用markDirty();直接给字段赋值会跳过脏标记,父容器不会重排,界面停留在旧几何上。这条同样写在 布局系统 的不变量里,两处必须一致。 markDirty()沿父链冒泡、layout()单点执行:脏标记传播到根为止,实际执行重排的只有Element.layout()一个入口(源码中不存在layoutElement()这样的方法,也不得新增)。CAPTURE_SCROLL没有消费端:位已定义(1 << 2)但 InteractionManager 滚轮路径不读取它,该位目前不产生任何效果;补上消费端时需同步补全该位的说明。- 命中链 H0 不可覆写:
rectHit是静态方法,子类只能覆写shapeHit,最终isHit还要叠加裁剪判定;削弱这一约束会破坏整条命中链的规则(命中链语义见 交互引擎)。 onOutsideInteraction()只响应明确输入:点击、松开、拖拽、滚轮落在浮层外时由 InteractionManager 调用,悬停有意不触发;把悬停纳入会破坏「先关闭后分发」的时序。
改动波及
- 改
pressFlags()的默认位,全部组件的按下行为一起变化;BLOCK 位一旦置位,InteractionManager 的右键与悬停判定立即改变路径。 - 改
markDirty()或layout()的语义,会波及三种容器的测量排列与所有组件的尺寸,详见 布局系统。 - 改
removeFromParent()的摘除条件,会波及覆盖层推入路径:迁移元素时若不再摘除,旧容器会继续绘制已迁出的子元素(渲染与输入的两棵树如何分工见 根树与层级系统)。 - 改
Hit.topmost的「首个消费者获胜」规则,会波及嵌套容器内的点击目标选择与pressOwner记录,下游行为由 交互引擎 描述。 - 改覆盖层关闭时的按下清理(
releasePressOwnerIfOwnerIs),会波及 SelectDropdown 等覆盖层组件的关闭时序,详见 覆盖层系统。
相关文档
- 布局系统:脏标记流水线、坐标系统与容器的完整机制。
- 交互引擎:命中链、事件分发、焦点与捕获的权威说明。
- 覆盖层系统:覆盖层如何扩展元素模型。
- 项目概述:继承层次总图与组件清单。
- Developer: 核心概念:在界面中摆放控件的使用侧入口