12 KiB
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.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/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/AtomUIGallery.Tests/ShowCases/SplitterShowCasePageTests.cstests/AtomUIGallery.Tests/ShowCases/SplitterShowCaseExamples.snapshotdocs/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 事件。 - 注册
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决定实时布局或延迟提交。 - 处理折叠和恢复,维护可见面板边界。
维护规则:
_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 发布流程验证。