Files

12 KiB
Raw Permalink Blame History

Splitter 桌面版实现原理

本文档描述 Splitter 桌面版的内部实现范围、源码职责、状态流、生命周期、资源边界和维护规则。公共设计与 API 契约见 Splitter 桌面版架构设计,变化记录见 Splitter Changelog。涉及控件 Token 的实现应同时阅读 Splitter Token 设计。

1. 实现定位

Splitter 的实现由一个 public 根控件和三个 internal 协作层组成:

Splitter
  -> SplitterTheme / PART_SplitterPanel
  -> SplitterPanel
  -> SplitterHandle
  -> SplitterDragBar

Splitter 暴露用户 API、Children、附加属性和 resize 事件;SplitterPanel 负责布局算法、面板集合、handle 生成和折叠状态;SplitterHandle 负责单条分割边界的状态、折叠按钮和 drag bar 协调;SplitterDragBar 负责 pointer drag 入口和 grip 视觉。

实现文档不复述完整属性清单。完整 public surface 以源码和 API 契约摘要为准。

2. 源码文件结构

主要源码文件:

  • src/AtomUI.Desktop.Controls/Splitter/Splitter.cs
  • src/AtomUI.Desktop.Controls/Splitter/SplitterPanel.cs
  • src/AtomUI.Desktop.Controls/Splitter/SplitterHandle.cs
  • src/AtomUI.Desktop.Controls/Splitter/SplitterDragBar.cs
  • src/AtomUI.Desktop.Controls/Splitter/SplitterPanelCollapsible.cs
  • src/AtomUI.Desktop.Controls/Splitter/SplitterResizeEventArgs.cs
  • src/AtomUI.Desktop.Controls/Splitter/SplitterToken.cs
  • src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterTheme.axaml
  • src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterHandleTheme.axaml
  • src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterDragBarTheme.axaml

Gallery 与文档结构:

  • controlgallery/AtomUIGallery/ShowCases/Layout/Splitter
  • tests/AtomUIGallery.Tests/ShowCases/SplitterShowCasePageTests.cs
  • tests/AtomUIGallery.Tests/ShowCases/SplitterShowCaseExamples.snapshot
  • docs/controls/desktop/layout/splitter/

职责边界:

  • Splitter.cs 保留 public API、附加属性、Children 同步、template part 获取和 resize 事件抛出。
  • SplitterPanel.cs 保留布局、拖拽、折叠、尺寸约束和 handle 状态刷新。
  • SplitterHandle.cs 保留单个 handle 的按钮、hover、dragging、collapse request 和 drag event 转发。
  • SplitterDragBar.cs 保留 Thumb 输入事件和禁用拖拽拦截。
  • Theme 文件负责静态视觉结构、template binding、selector 和 TokenResource 映射。
  • Token 文件只提供组件视觉变量,不保存实例状态。

3. 核心类职责

3.1 Splitter

Splitter 是唯一 public 控件入口:

  • 持有 Children 集合。
  • 注册根控件属性和面板附加属性。
  • 在 OnApplyTemplate 中获取 PART_SplitterPanel。
  • 把 Children 同步到 SplitterPanel.Children。
  • 通过 RaiseResizeStarted、RaiseResizeDelta、RaiseResizeCompleted 把 internal drag 状态转换为 public resize 事件。
  • 注册 SplitterToken scope。

维护规则:

  • 不把 internal handle 或 panel 对象暴露给用户代码。
  • 模板重套用时必须保证 Children 同步不会重复持有旧视觉对象。
  • 新增 Splitter 实例级样式 API 时,入口必须先定义在 Splitter 上,再由 SplitterPanel 传递给 internal handle。

3.2 SplitterPanel

SplitterPanel 是布局和状态 owner:

  • 从 Children 中识别用户面板。
  • 在相邻面板之间创建 SplitterHandle。
  • 监听面板附加属性变化并触发布局或 handle 状态刷新。
  • 根据 Orientation、HandleSize、尺寸约束和折叠状态执行 measure/arrange。
  • 处理 drag started/delta/completed,按 IsLazy 决定实时布局或延迟提交。
  • 处理折叠和恢复,维护可见面板边界。

维护规则:

  • _panels 是用户面板列表,_handles 是 internal handle 列表,两者不能混淆。
  • handle 的创建、事件订阅和移除必须成对维护。
  • 面板属性变化必须从 SplitterPanel 统一归一,不允许 handle 直接修改用户面板业务状态。
  • 尺寸计算必须同时考虑固定值、百分比、默认尺寸、最小值、最大值和折叠尺寸。

3.3 SplitterHandle

SplitterHandle 是 internal 分割边界:

  • 接收 Orientation、IsDragEnabled、line brush、line thickness 等视觉状态。
  • 获取 PART_DragBar 和折叠按钮。
  • 转发 SplitterDragBar 的 drag event。
  • 根据可折叠状态、hover side 和 icon display mode 更新折叠按钮。
  • 使用 :pointerover 和 :dragging 驱动主题 selector。

维护规则:

  • SplitterHandle 不应成为用户样式 API。
  • 折叠按钮点击只抛出 request,由 SplitterPanel 决定是否真正折叠或恢复。
  • LineBrush、LineThickness、LineCornerRadius 这类分割线视觉状态必须可由 Splitter 根控件统一传入,不能只停留在 internal handle 默认值。

3.4 SplitterDragBar

SplitterDragBar 继承 AtomUI Thumb:

  • 提供 pointer drag 入口。
  • 通过 IsDragEnabled 禁用拖拽输入。
  • 使用 Orientation 设置 resize cursor。
  • 使用 line brush、line thickness 和 line corner radius 展示 grip。
  • 使用 :dragging 表达拖拽状态。

维护规则:

  • drag bar 负责输入和 grip 视觉,不负责面板尺寸计算。
  • grip 的厚度应来自 LineThickness,不能绕过 handle 状态直接写死 token。
  • drag bar 命中区域尺寸和可见 grip 尺寸必须分离。

4. 状态与数据流

Splitter 的主状态流:

User children / attached panel properties
  -> Splitter.Children
  -> SplitterPanel.Children
  -> _panels + generated _handles
  -> Measure / Arrange / collapse state

拖拽状态流:

Pointer drag on SplitterDragBar
  -> SplitterHandle.DragStarted / DragDelta / DragCompleted
  -> SplitterPanel.HandleDragStarted / HandleDragDelta / HandleDragCompleted
  -> Apply panel sizes
  -> Splitter.ResizeStarted / ResizeDelta / ResizeCompleted

折叠状态流:

Collapse IconButton click
  -> SplitterHandle.CollapsePreviousRequested / CollapseNextRequested
  -> SplitterPanel ExpandLeftAtHandle / ExpandRightAtHandle
  -> attached IsCollapsed + panel size update
  -> handle visibility and button state refresh

样式状态流:

Splitter public style API / SplitterToken
  -> SplitterPanel owner state
  -> SplitterHandle line state
  -> PART_HandleLine + SplitterDragBar
  -> PART_Grip

维护要求:

  • 外部设置的 Avalonia 属性必须在模板应用前后保持一致。
  • HandleSize 影响布局和 hit area;line thickness 只影响可见线条和 grip。
  • IsResizable=False 只禁用对应边界拖拽,不应移除分割线视觉。
  • IsLazy=True 时中间 drag feedback 不应提前写入面板 Size。
  • overview.md 的 API 契约说明应与源码实际状态流一致。

5. 生命周期与模板接入

Splitter 生命周期:

  • 构造阶段订阅 Children.CollectionChanged 并注册 Token scope。
  • ChildrenChanged 把用户集合变更同步到当前 SplitterPanel。
  • OnApplyTemplate 获取 PART_SplitterPanel,设置 owner,并同步现有 Children。

SplitterPanel 生命周期:

  • 构造阶段订阅自身 Children.CollectionChanged。
  • RefreshPanelsAndHandles 移除旧 handle、解除事件订阅、重建面板列表、创建新 handle、建立 handle 事件。
  • SyncTrackedPanels 跟踪用户面板属性变化,并在面板离开时解绑。
  • 布局变化和属性变化触发 measure/arrange 或 handle state update。

SplitterHandle 生命周期:

  • OnApplyTemplate 先解除旧 SplitterDragBar 和按钮事件,再获取新 part 并重新订阅。
  • SetDragging 同步 handle 和 drag bar 的 :dragging 状态。
  • UpdateCollapseButtons 在 hover、orientation、collapse state 变化后刷新按钮。

稳定接入点:

接入点 Owner 维护规则
PART_SplitterPanel Splitter public template part,重命名需要同步主题、实现、测试和文档。
PART_HandleLine SplitterHandle internal theme part,用于线条视觉,不作为用户 API。
PART_DragBar SplitterHandle internal theme part,用于 pointer drag,不作为用户 API。
PART_Grip SplitterDragBar internal theme part,用于 grip 视觉,不作为用户 API。
collapse buttons SplitterHandle internal theme part,用于折叠操作。

6. 交互与事件处理

交互规则:

  • Pointer drag 只从 SplitterDragBar 进入。
  • SplitterHandle 只转发 drag event 和 collapse request。
  • SplitterPanel 是唯一可修改面板尺寸和折叠状态的内部 owner。
  • Splitter 只负责对外抛 resize 事件。

resize 事件语义:

  • ResizeStarted 在某个 handle 开始拖拽时触发。
  • ResizeDelta 在尺寸变化过程中触发,lazy 模式下应符合延迟提交语义。
  • ResizeCompleted 在拖拽结束后触发。
  • 事件参数暴露 handle index 和尺寸快照,不暴露 internal handle 对象。

禁用拖拽语义:

  • Splitter.IsResizable=False 的面板会影响相邻 handle 的 IsDragEnabled。
  • IsDragEnabled=False 时 drag bar 不处理 pointer pressed/moved/released。
  • 禁用拖拽不等于隐藏分割线;视觉仍应表达面板边界。

7. 内部算法与关键流程

维护者需要重点关注:

  • RefreshPanelsAndHandles:从用户 children 派生 internal handle。
  • UpdateHandleState:根据相邻面板、折叠状态和尺寸约束决定 drag/collapse 能力。
  • measure/arrange 流程:把 Dimension、HandleSize 和剩余空间归一为实际尺寸。
  • drag 流程:实时或 lazy 地更新尺寸。
  • collapse 流程:寻找可见边界、可恢复面板和尺寸 owner。

布局算法不应把视觉线条厚度混入 handle 占位尺寸。可见线条变粗时,只应该影响 handle 内部视觉,不应该让面板布局发生额外跳动,除非用户显式改变 HandleSize。

8. 资源、性能与 AOT 边界

资源边界:

  • 不通过运行时反射扫描 public API、Token 或 Gallery 示例数据。
  • 不把可静态声明的模板结构迁移到 C# 动态创建。
  • handle、drag bar、button 的事件订阅必须在模板重套用或 handle 移除时解绑。
  • _trackedPanels 中的面板属性订阅必须在面板离开时解绑。

性能边界:

  • 面板集合变化时只重建必要的 handle 结构,避免把业务子控件重新包装成不可追踪结构。
  • 拖拽过程中避免创建临时视觉对象。
  • lazy 模式下拖拽反馈应复用现有 drag bar 状态,不引入额外弹层或全局输入监听。
  • 分割线样式变化应通过 Avalonia 属性和模板绑定流动,不使用手动遍历视觉树刷新。

AOT 边界:

  • API 与 Token 契约不通过运行时反射生成。
  • Source generator 生成文件不手工编辑。
  • 文档、Gallery 和源码发生冲突时,应修复源文档或结构化数据,不直接改 docs/AI/generated/llms 生成产物。

9. 维护不变量

维护 Splitter 时不得破坏:

  • Public API、默认值、事件顺序和 Gallery 可观察行为。
  • HandleSize 作为 hit area 的语义。
  • SplitterPanel 作为尺寸与折叠状态 owner 的语义。
  • internal handle template part 的绑定关系和事件释放路径。
  • Light/Dark、Browser/Desktop 和不同方向下的主题一致性。
  • API 契约摘要、Token 语义、ShowCase 示例和控件文档的一致性。

新增分割线样式能力时必须遵守:

  • API 定义在 Splitter,不定义在 internal handle 上作为用户入口。
  • LineThickness 不替代 HandleSize。
  • LineCornerRadius 同时作用于 PART_HandleLine 和 PART_Grip。
  • 默认值来自 Splitter Token 或 SharedToken,保证现有视觉不变。
  • API 契约摘要、Token 语义、ShowCase 示例和回归测试同步更新。

10. 测试与验证

推荐验证:

  • 纯文档改动运行 git diff --check 并检查相对链接。
  • 控件 API 或行为变更运行 tests/AtomUI.Desktop.Controls.Tests。
  • Gallery 示例或源码片段变更运行 tests/AtomUIGallery.Tests 中 Splitter 相关测试。
  • 主题变更检查 SplitterTheme.axaml、SplitterHandleTheme.axaml、SplitterDragBarTheme.axaml 中 TemplateBinding、TokenResource 和 selector 是否一致。
  • AOT、生成器或动态数据路径变更按 Gallery NativeAOT 发布流程验证。