← 返回索引

范围:元素基类行为、按下位标志(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 等覆盖层组件的关闭时序,详见 覆盖层系统。

相关文档