16 KiB
Splitter 桌面版实现原理
本文档描述 Splitter 桌面版的内部实现范围、源码职责、状态流、生命周期、资源边界和维护规则。公共设计与 API 契约见 Splitter 桌面版架构设计,Semantic Part 契约见 Splitter Semantic Part 契约,变化记录见 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.cssrc/AtomUI.Desktop.Controls/Splitter/Splitter.SemanticParts.cssrc/AtomUI.Desktop.Controls/Splitter/SplitterPanel.cssrc/AtomUI.Desktop.Controls/Splitter/SplitterHandle.cssrc/AtomUI.Desktop.Controls/Splitter/SplitterDragBar.cssrc/AtomUI.Desktop.Controls/Splitter/SplitterPanelCollapsible.cssrc/AtomUI.Desktop.Controls/Splitter/SplitterResizeEventArgs.cssrc/AtomUI.Desktop.Controls/Splitter/SplitterDraggerDoubleClickedEventArgs.cssrc/AtomUI.Desktop.Controls/Splitter/SplitterToken.cssrc/AtomUI.Desktop.Controls/Splitter/Themes/SplitterTheme.axamlsrc/AtomUI.Desktop.Controls/Splitter/Themes/SplitterHandleTheme.axamlsrc/AtomUI.Desktop.Controls/Splitter/Themes/SplitterDragBarTheme.axaml
Gallery 与文档结构:
controlgallery/AtomUIGallery/ShowCases/Layout/Splittertests/AtomUI.Desktop.Controls.Tests/Splitter/SplitterSemanticPartTests.cstests/AtomUIGallery.Tests/ShowCases/SplitterShowCasePageTests.cstests/AtomUIGallery.Tests/ShowCases/SplitterShowCaseExamples.snapshotdocs/controls/desktop/layout/splitter/
职责边界:
Splitter.cs保留 public API、附加属性、Children 同步、template part 获取、resize 和 dragger 双击事件抛出。Splitter.SemanticParts.cs只承载[SemanticPart]descriptor 声明,生成SplitterSemanticParts常量与SplitterPanelStyle/SplitterDraggerStyleSemantic Style。SplitterPanel.cs保留布局、拖拽、折叠、尺寸约束、handle 状态刷新和 Semantic Part marker 维护。SplitterHandle.cs保留单个 handle 的按钮、hover、dragging、collapse request 和 drag event 转发。SplitterDragBar.cs保留 Thumb 输入事件、双击识别、第二次 drag start 抑制和禁用拖拽拦截。- Theme 文件负责静态视觉结构、template binding、selector、TokenResource 映射和静态 semantic marker 声明。
- Token 文件只提供组件视觉变量,不保存实例状态。
3. 核心类职责
3.1 Splitter
Splitter 是唯一 public 控件入口:
- 持有
Children集合。 - 注册根控件属性和面板附加属性。
- 在
OnApplyTemplate中获取PART_SplitterPanel。 - 把
Children同步到SplitterPanel.Children。 - 通过
RaiseResizeStarted、RaiseResizeDelta、RaiseResizeCompleted把 internal drag 状态转换为 public resize 事件。 - 通过
RaiseDraggerDoubleClicked把 internal dragger 双击转换为 publicDraggerDoubleClicked,只暴露 handle index。 - 注册
SplitterTokenscope。
维护规则:
- 不把 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决定实时布局或延迟提交。 - 处理折叠和恢复,维护可见面板边界。
- 维护 Semantic Part marker:为新跟踪面板添加
semantic-panel,为新建 handle 添加semantic-scope-handle, 面板离开时回收 Splitter 自己添加的 marker。
维护规则:
_panels是用户面板列表,_handles是 internal handle 列表,两者不能混淆。- handle 的创建、事件订阅和移除必须成对维护。
- 面板属性变化必须从
SplitterPanel统一归一,不允许 handle 直接修改用户面板业务状态。 - 尺寸计算必须同时考虑固定值、百分比、默认尺寸、最小值、最大值和折叠尺寸。
_semanticPanelMarkersAdded只记录「由 Splitter 添加」的semantic-panelmarker;面板离开时只回收该集合 中的条目,用户预先声明的semantic-panel类必须保留。
3.3 SplitterHandle
SplitterHandle 是 internal 分割边界:
- 接收
Orientation、IsDragEnabled、line brush、line thickness 等视觉状态。 - 获取
PART_DragBar和折叠按钮。 - 转发
SplitterDragBar的 drag event。 - 转发
SplitterDragBar的双击通知,并沿模板重套用生命周期成对订阅和解除。 - 根据可折叠状态、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禁用拖拽输入。 - 使用平台提供的 pointer
ClickCount识别第二次左键按下,在释放阶段发出双击通知。 - 第二次按下不进入 Thumb drag 流;捕获丢失会取消待释放的双击通知。
- 使用
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
双击状态流:
Second left PointerPressed on SplitterDragBar
-> suppress second Thumb DragStarted
-> matching left PointerReleased
-> SplitterHandle.DraggerDoubleClicked
-> SplitterPanel forwards HandleIndex
-> Splitter.DraggerDoubleClicked
-> application-owned reset policy
折叠状态流:
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、解除 drag、double-click 和 collapse 事件订阅、重建面板列表、创建新 handle、建立 handle 事件。SyncTrackedPanels跟踪用户面板属性变化,为新面板添加semantic-panelmarker,并在面板离开时解绑、 回收 Splitter 自己添加的 marker。CreateHandle为每个新 handle 添加semantic-scope-handlemarker;handle 重建时 marker 随新实例。- 布局变化和属性变化触发 measure/arrange 或 handle state update。
Semantic Part marker 的获取与释放成对存在:
- 面板 marker:
SyncTrackedPanels添加,面板离开_panels时在SyncTrackedPanels中移除;只有_semanticPanelMarkersAdded记录的条目会被回收。 - handle marker:
CreateHandle添加,handle 随RefreshPanelsAndHandles销毁时随实例释放,无独立回收路径。 - 静态 marker(
semantic-scope-panel/semantic-dragger)由主题声明,模板重套用时随模板节点重建。
SplitterHandle 生命周期:
OnApplyTemplate先解除旧SplitterDragBar的 drag/double-click 事件和按钮事件,再获取新 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、dragger double-click 和 collapse request。SplitterPanel是唯一可修改面板尺寸和折叠状态的内部 owner。Splitter只负责对外抛 resize 事件。
dragger 双击事件语义:
- 双击时间和距离由 Avalonia 平台输入系统计算为
ClickCount,控件不维护固定毫秒计时器。 - 第一次按下和释放保留原有 drag started/completed 语义;第二次按下不再启动 drag。
DraggerDoubleClicked在第二次左键释放后触发,捕获丢失时取消。- 双击通知直接沿所属 drag bar、handle、panel 和 Splitter 传递,不使用冒泡 routed event,避免嵌套 Splitter 通知外层。
- 双击通知独立于
IsDragEnabled;控件不自动恢复Size、DefaultSize或IsCollapsed。
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 移除时解绑。
- drag bar 仅持有一个待释放双击状态;该状态在 release 和 pointer capture lost 时清除,不使用 timer 或全局输入监听。
_trackedPanels中的面板属性订阅必须在面板离开时解绑。
性能边界:
- 面板集合变化时只重建必要的 handle 结构,避免把业务子控件重新包装成不可追踪结构。
- 拖拽过程中避免创建临时视觉对象。
- lazy 模式下拖拽反馈应复用现有 drag bar 状态,不引入额外弹层或全局输入监听。
- 分割线样式变化应通过 Avalonia 属性和模板绑定流动,不使用手动遍历视觉树刷新。
AOT 边界:
- API 与 Token 契约不通过运行时反射生成。
- Source generator 生成文件不手工编辑。
- 文档、Gallery 和源码发生冲突时,应修复源文档或结构化数据。
9. 维护不变量
维护 Splitter 时不得破坏:
- Public API、默认值、事件顺序和 Gallery 可观察行为。
HandleSize作为 hit area 的语义。SplitterPanel作为尺寸与折叠状态 owner 的语义。- internal handle template part 的绑定关系和事件释放路径。
DraggerDoubleClicked的释放时机、零基 handle index、第二次 drag start 抑制、禁用拖拽可用性和嵌套隔离。- Semantic Part 契约:
root/panel/dragger的名称、selector class、SelectorRoute、ContractType与 cardinality(完整定义见 Splitter Semantic Part 契约)。 - 主题静态 marker(
semantic-scope-panel/semantic-dragger)与运行时 marker(semantic-panel/semantic-scope-handle)的放置位置与回收路径。 - Light/Dark、Browser/Desktop 和不同方向下的主题一致性。
- API 契约摘要、Token 语义、ShowCase 示例和控件文档的一致性。
- 根框架外观 API(
Background、BorderBrush、BorderThickness、CornerRadius、BorderDashArray、BorderDashOffset)直接 TemplateBinding 到SplitterTheme的Frame,不经过SplitterPanel或 internal handle 转发。
新增分割线样式能力时必须遵守:
- 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。 - 双击行为测试覆盖释放时机、handle index、重复 drag start、禁用拖拽、捕获丢失、handle 重建解绑和嵌套隔离。
- Semantic Part 契约变更运行
tests/AtomUI.Desktop.Controls.Tests中的SplitterSemanticPartTests, 并走查 Gallery Semantic Parts 标签页的预览与描述。 - Gallery 示例或源码片段变更运行
tests/AtomUIGallery.Tests中 Splitter 相关测试。 - 主题变更检查
SplitterTheme.axaml、SplitterHandleTheme.axaml、SplitterDragBarTheme.axaml中 TemplateBinding、TokenResource、selector 和 semantic marker 是否一致。 - AOT、生成器或动态数据路径变更按 Gallery NativeAOT 发布流程验证。