8.6 KiB
ToggleSwitch 桌面版实现原理
本文档描述 ToggleSwitch 桌面版的内部布局、状态流转、模板接入、加载动画、内容绑定、Form 和 Token 使用。公共设计与 API 契约见 ToggleSwitch 桌面版架构设计,Token 语义见 ToggleSwitch Token 设计,变化记录见 ToggleSwitch Changelog。
1. 实现定位
ToggleSwitch 的 public 桌面类型位于 AtomUI.Desktop.Controls,实际交互和布局实现位于 AtomUI.Controls.AbstractToggleSwitch。桌面层通过 ToggleSwitchToken 和主题文件提供 参考设计体系 风格视觉。本文档聚焦 AtomUI 增强层,不重新说明 Avalonia ToggleButton 的基础点击和键盘切换算法。
2. 源码文件结构
主要源码:
src/AtomUI.Desktop.Controls/Switch/ToggleSwitch.cs:public 桌面控件,注册ToggleSwitchTokenresource scope。src/AtomUI.Controls/Switch/AbstractToggleSwitch.cs:公共 API、测量、布局、内容绑定、loading、hit test、Form 和渲染主逻辑。src/AtomUI.Controls/Switch/SwitchKnob.cs:把手绘制、加载指示绘制、把手宽度动画和加载动画生命周期。src/AtomUI.Desktop.Controls/Switch/ToggleSwitchToken.cs:ToggleSwitch 控件级 Token。src/AtomUI.Desktop.Controls/Switch/Themes/ToggleSwitchTheme.axaml:主模板、状态 selector、SizeType 分支和 Token 引用。src/AtomUI.Desktop.Controls/Switch/Themes/SwitchKnobTheme.axaml:把手主题、加载动画周期和动效。
3. 核心类职责
ToggleSwitch 是桌面包对外入口。它不新增 API,也不重写布局和交互,只把实例接入 ToggleSwitchToken scope。
AbstractToggleSwitch 是状态和布局核心。它负责读取 IsChecked、IsLoading、SizeType、OnContent、OffContent 等状态,计算轨道尺寸、把手位置、内容偏移和 Form value。
SwitchKnob 是内部把手。它根据 KnobSize 和 KnobRenderWidth 绘制圆形或胶囊把手,在 loading 状态下绘制旋转 loading arc。
WaveSpiritDecorator 是视觉效果边界,只在 checked 状态变化且 IsWaveSpiritEnabled=true 时播放,不参与值状态计算。
4. 状态与数据流
基础状态流:
IsChecked / IsPressed / SizeType / content / token
↓
MeasureOverride
↓
Track size + content desired size
↓
CalculateElementsOffset
↓
KnobRect + KnobMovingRect + OnContentOffset + OffContentOffset
↓
ArrangeOverride + Render
checked 变化时,如果启用 motion,控件先基于当前 groove size 重新计算元素位置,再由 RectTransition 和 PointTransition 执行动画。_isCheckedChanged 用于让 arrange 阶段在 checked 切换后使用 KnobMovingRect,保持过渡位置一致。
loading 状态流:
IsLoading changed
→ HandleLoadingState
→ set cursor local value / dispose cursor local value
→ SwitchKnob.NotifyStartLoading / NotifyStopLoading
Form 状态流:
Form.SetValue(bool?) → IsChecked
IsChecked changed → Form ValueChanged
Form.GetValue() → IsChecked
Form.ClearValue() → IsChecked = null
5. 生命周期与模板接入
OnApplyTemplate 获取以下稳定 part:
PART_SwitchKnobPART_OnContentPresenterPART_OffContentPresenterPART_WaveSpiritPART_MainContainer
模板接入后,控件把当前 KnobSize 写入 SwitchKnob,再根据当前 IsLoading 同步把手 loading 状态。模板 part 没有全局事件订阅,主要生命周期风险来自内容图标 relay binding 和 loading animation。
内容变化时,SetupContent 会释放旧 on/off 内容对应的 CompositeDisposable,再为新的 PathIcon / Icon 建立尺寸和前景色绑定。维护时必须保持内容替换和 disposable 释放成对出现。
SwitchKnob 在 OnAttachedToVisualTree 中根据 _isLoading 启动加载动画,在 OnDetachedFromVisualTree 中取消并释放 CancellationTokenSource。加载旋转使用共享无限动画 helper,并显式采用 PlaybackBehavior.OnlyIfVisible;ToggleSwitch 自身或 Visual 祖先不可见时 animation clock 暂停,重新可见后继续。
6. 交互与事件处理
Pointer press 流程:
OnPointerPressed
→ if !IsLoading then base.OnPointerPressed
→ if knob not mid-animation
→ _isCheckedChanged=false
→ AdjustOffsetOnPressed
Pointer release 流程:
OnPointerReleased
→ if !IsLoading then base.OnPointerReleased
→ AdjustOffsetOnReleased
→ InvalidateArrange
loading 状态下 pointer press/release 不进入基类逻辑,因此不会切换 IsChecked。键盘切换仍来自 ToggleButton 基类,维护 loading 行为时必须确认键盘路径也不产生业务不一致。
HitTest(Point) 只在控件 enabled 且非 loading 时返回 groove rect 命中结果。该接口用于自定义命中测试,不应把内容 presenter 或 wave 外扩区域作为可点击范围。
7. 内部算法与关键流程
7.1 测量与宽度计算
测量阶段分别测量 on/off content presenter,取二者最大宽度作为内容宽度,再叠加 InnerMinMargin 和 InnerMaxMargin。最终宽度取该值与 TrackMinWidth 的较大值,高度使用 TrackHeight。
7.2 把手位置
HandleRect(bool isChecked, Size controlSize) 根据 checked 状态计算把手左上角。unchecked 时把手位于左侧 TrackPadding,checked 时位于 controlSize.Width - TrackPadding - handleSize。按下时把手宽度乘以 STRETCH_FACTOR。
7.3 内容偏移
ExtraInfoRect 根据当前状态和内部边距计算内容区域。checked 时 on content 进入可见区域,off content 移到右侧不可见区域;unchecked 时反向处理。内容偏移使用 PointTransition 过渡。
7.4 轨道绘制
AbstractToggleSwitch.Render 使用 DrawPilledRect 绘制轨道,并通过 SwitchOpacity 控制 disabled/loading 透明度。轨道背景来自 GrooveBackground,通常由主题 selector 设置。
7.5 把手与 loading 绘制
SwitchKnob.Render 根据 KnobRenderWidth 和高度判断绘制圆形或胶囊把手。loading 状态下,按当前 Rotation 绘制 90 度 arc。GetLoadIndicatorPen 缓存 pen,并在 brush 引用变化时重建。
8. 资源、性能与 AOT 边界
ToggleSwitch 不通过反射访问模板结构。模板结构由稳定 part 和 AXAML TemplateBinding 表达。
资源与生命周期边界:
- on/off 图标内容的 relay binding 必须在内容替换时释放。
IsLoading设置的 cursor local value 必须在退出 loading 时 dispose。SwitchKnob的 loadingCancellationTokenSource必须在停止 loading 和 detach 时释放。- 隐藏祖先下的 loading animation 不得继续推进 animation clock。
- Token 只表达尺寸、颜色、阴影、字体和加载动画周期,不承载
IsChecked、IsLoading或内容实例状态。
当前图标内容绑定使用 C# relay binding,因为目标对象来自用户提供的 runtime content,不是稳定模板 part。模板内部固定关系应继续优先使用 AXAML binding 和 selector。
AOT 边界:
- Token 类型通过 generator 显式注册。
- API 与 Token 契约由控件文档、源码 public surface、Token 类型或生成数据维护。
- 文档中列出的 part 名称必须与 AXAML 和 C# 查找代码一致。
9. 维护不变量
内部重构必须保持以下不变量:
ToggleSwitchpublic 类型保持轻量桌面入口,不把共享实现复制到 Desktop 包。AbstractToggleSwitch继续作为 API 和状态核心。- loading 不触发新切换,并释放 cursor local value 和 animation token。
- 内容图标 relay binding 在内容替换时释放。
MeasureOverride必须同时考虑 on/off content 的最大宽度。SwitchKnobloading animation detach 时必须取消。SizeType=Custom默认与 Middle 视觉一致。SwitchOpacity只表达 disabled/loading 视觉透明度,不改变 enabled 状态。- WaveSpirit 只作为视觉反馈,不影响
IsChecked。
10. 测试与验证
验证范围:
- checked、unchecked、null 绑定和 Form 读写清空。
- loading 状态下 pointer 和命中测试不触发切换。
- loading attach/detach 不泄漏动画 token。
OnContent/OffContent为文本和图标时的测量、偏移、前景色和尺寸同步。- Large / Middle / Small / Custom 下的高度、最小宽度、内容边距和图标尺寸。
- disabled、pointerover、pressed、checked 和 unchecked 主题 selector。
- WaveSpirit 开关不改变值状态。
- Gallery ToggleSwitch 示例、源码片段。
- 文档改动运行
git diff --check,并检查相对链接存在。