Files

178 lines
8.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ToggleSwitch 桌面版实现原理
本文档描述 ToggleSwitch 桌面版的内部布局、状态流转、模板接入、加载动画、内容绑定、Form 和 Token 使用。公共设计与 API 契约见 [ToggleSwitch 桌面版架构设计](overview.md),Token 语义见 [ToggleSwitch Token 设计](token.md),变化记录见 [ToggleSwitch Changelog](changelog.md)。
## 1. 实现定位
ToggleSwitch 的 public 桌面类型位于 `AtomUI.Desktop.Controls`,实际交互和布局实现位于 `AtomUI.Controls.AbstractToggleSwitch`。桌面层通过 `ToggleSwitchToken` 和主题文件提供 参考设计体系 风格视觉。本文档聚焦 AtomUI 增强层,不重新说明 Avalonia `ToggleButton` 的基础点击和键盘切换算法。
## 2. 源码文件结构
主要源码:
- `src/AtomUI.Desktop.Controls/Switch/ToggleSwitch.cs`:public 桌面控件,注册 `ToggleSwitchToken` resource 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. 状态与数据流
基础状态流:
```text
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 状态流:
```text
IsLoading changed
→ HandleLoadingState
→ set cursor local value / dispose cursor local value
→ SwitchKnob.NotifyStartLoading / NotifyStopLoading
```
Form 状态流:
```text
Form.SetValue(bool?) → IsChecked
IsChecked changed → Form ValueChanged
Form.GetValue() → IsChecked
Form.ClearValue() → IsChecked = null
```
## 5. 生命周期与模板接入
`OnApplyTemplate` 获取以下稳定 part:
- `PART_SwitchKnob`
- `PART_OnContentPresenter`
- `PART_OffContentPresenter`
- `PART_WaveSpirit`
- `PART_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 流程:
```text
OnPointerPressed
→ if !IsLoading then base.OnPointerPressed
→ if knob not mid-animation
→ _isCheckedChanged=false
→ AdjustOffsetOnPressed
```
Pointer release 流程:
```text
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` 的 loading `CancellationTokenSource` 必须在停止 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. 维护不变量
内部重构必须保持以下不变量:
- `ToggleSwitch` public 类型保持轻量桌面入口,不把共享实现复制到 Desktop 包。
- `AbstractToggleSwitch` 继续作为 API 和状态核心。
- loading 不触发新切换,并释放 cursor local value 和 animation token。
- 内容图标 relay binding 在内容替换时释放。
- `MeasureOverride` 必须同时考虑 on/off content 的最大宽度。
- `SwitchKnob` loading 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`,并检查相对链接存在。