# Avatar 桌面版实现原理 本文档描述 Avatar 控件家族的源码 ownership、图片加载状态流、模板生命周期和资源释放规则。公共契约见 [Avatar 控件家族架构设计](overview.md),Token 语义见 [Avatar Token 设计](token.md)。 Popup 接入边界:`AvatarGroup` 负责业务状态和内容准备,内部 `FlyoutHost` 仅作为 relay 适配层,fold-count Flyout Popup 负责实际显示。模板重建或宿主切换时必须先释放旧 relay,再绑定新的 Popup;普通外点、Escape、失焦和业务关闭在 pinned 状态下被拦截,detach、窗口销毁、跨 TopLevel 和无效锚点必须走生命周期关闭并释放 Popup host。完整状态机见 [Popup 钉住打开设计](../../other/popup/popup-pinned-open-design.md)。 ## 1. 实现定位 本文覆盖单头像、桌面 AvatarGroup、共享单图 controller、主题接入和图片租约生命周期。具体 StyledProperty 注册、Token 派生值和 AXAML selector 仍以源码为准;本文只记录跨文件协作和维护时不可破坏的实现边界。 ## 2. 源码文件结构 `AtomUI.Controls` 拥有单头像实现: ```text src/AtomUI.Controls/Avatar/ ├── AbstractAvatar.cs ├── Avatar.cs ├── AvatarPseudoClass.cs ├── AvatarShape.cs ├── AvatarToken.cs └── Themes/AvatarTheme.axaml ``` `AtomUI.Desktop.Controls` 只拥有桌面组合控件: ```text src/AtomUI.Desktop.Controls/Avatar/ ├── AvatarGroup.cs ├── AvatarGroupFoldInfo.cs └── Themes/AvatarGroupTheme.axaml ``` `Avatar` 和 `AvatarToken` 不在 Desktop 包中保留副本。Desktop 通过对 Controls 的正常项目引用复用类型和 Token resource。 ## 3. 核心类职责 - `AbstractAvatar`:Avalonia 属性、图片 controller host、内容优先级、尺寸/形状归一、加载状态和模板生命周期。 - `Avatar`:可实例化的公共控件类型,不建立第二套状态。 - `ImageLoadController`:与 `AsyncImage` 共用的单图片请求 owner,负责 generation、取消、fallback、Reload、progress 和租约提交。 - `AvatarToken`:从全局 Token 派生 Avatar 与 AvatarGroup 共用的视觉值。 - `AvatarGroup`:子 Avatar 排列、重叠宽度、折叠计数和 Flyout 生命周期。 ## 4. 状态与数据流 ```text Source / FallbackSource / RequestOptions / Bounds / render scaling -> ImageLoadController generation -> Application.GetImageLoader().LoadAsync(...) -> ImageContentValidator / SvgContentValidator (Application SVG conformance mode) -> ImageLoadResult lease -> UI dispatcher generation check -> LoadedImage + LoadState/Error/Progress -> ContentType(Image/Text/Icon) + pseudo-classes -> AvatarTheme ``` `AbstractAvatar` 实现 `IImageLoadControllerHost`。它只向 controller 暴露当前 Visual、Normal priority、attach 状态、16 px 量化后的物理解码尺寸和状态提交回调,不直接访问 transport 或 cache。 `SvgContentValidator` 位于 Shared loader 管线中,在 codec 和 Avatar 状态提交之前执行。Application 构建时冻结默认 `Compatible`/可选 `Strict` 模式;Avatar 不把模式写入 `ImageRequestOptions`,也不根据来源类型选择另一条 SVG 校验路径。 Source、FallbackSource 或 RequestOptions 变化调用 `SourceConfigurationChanged()`。Arrange 完成后调用 `RefreshSize()`;只有有效 Bounds 产生新的尺寸 bucket 时才需要新请求。属性快照和状态提交均在 UI 线程边界完成。 ## 5. 组合结构模型 ### 控件角色图 ```text Avatar -> Panel#RootLayout -> PixelAlignedBorder#Frame (internal-observable) -> IconPresenter#IconPresenter (internal-observable) -> Border -> Image#ImagePresenter (internal-observable) -> Border -> Viewbox (internal-observable) -> TextBlock#PART_TextPresenter (template-stable) AvatarGroup -> child Avatar collection (public) -> fold Avatar + Flyout (internal-observable) ``` ### 协作节点 | 节点 | 类型 | 来源 | 生命周期 owner | 影响的 public API | 稳定性 | Agent 使用边界 | | --- | --- | --- | --- | --- | --- | --- | | `Avatar` | public control | `AvatarTheme.axaml` | visual tree | 单头像全部 API | public | 用户可直接使用 | | `ImageLoadController` | internal coordinator | C# | `AbstractAvatar` | Source、fallback、状态、Reload | internal-observable | 只用于理解统一状态机 | | Text `Viewbox` | layout presenter | `AvatarTheme.axaml` | visual tree | Text、Gap、Size、Font | internal-observable | 负责自然尺寸测量、向下缩放和居中,不作为独立 API 暴露 | | `PART_TextPresenter` | `TextBlock` | `AvatarTheme.axaml` | template | Text、Font | template-stable | 提供自然排版尺寸;改名需同步实现、主题和文档 | | `AvatarGroup` | public control | `AvatarGroupTheme.axaml` | visual tree | Children、折叠和尺寸投影 | public | 用户可直接使用 | | fold Avatar/Flyout | internal visual | C# + Theme | `AvatarGroup` | MaxDisplayCount、trigger、fold colors | internal-observable | 不作为独立 API 暴露 | 内容选择只有三个结果: 1. `LoadedImage != null` 时选择 Image。 2. 否则 `Text != null` 时选择 Text。 3. 否则选择 Icon。 因此 Loading、取消或主来源失败不会主动清空 Text/Icon。Controller 在主来源终态失败后至多请求一次 FallbackSource;主来源 和 fallback 身份相同则不重复请求。fallback 成功时设置 `:loaded` 与 `:fallback`,最终失败时设置 `:failed`。 事件与状态提交顺序必须一致:先保存当前结果和状态,再触发 `ImageOpened`;最终失败先提交错误状态,再触发 `ImageFailed`。 取消、detach 和 generation 过期不属于图片失败事件。 ## 6. 生命周期与模板接入 成功的 `ImageLoadResult` 是图片使用租约。Controller 持有当前租约,来源变化、detach、最终失败或 controller dispose 时释放。 替换图片时先接管新租约,再释放旧租约,避免 decoded cache entry 在视觉切换前被销毁。 `Reload()` 不创建新的公开来源状态;它复制当前 options,并只把本次请求的 `CacheRead` 覆盖为 `ImageCacheReadPolicy.RefreshSource`: - HTTP 允许使用已有 metadata 做条件重验证。 - File、Asset、StorageFile、Bytes 和 Stream 强制重新读取来源。 - Reload 期间可保留旧图片;新结果只有通过 generation 校验后才能替换。 `new BorrowedImageSource(image)` 是 borrowed source。其结果租约只约束消费期,不能 dispose 调用方的 `IImage`。 | 获取/建立 | 对应释放 | | --- | --- | | controller attach 与 loader waiter | visual detach 时 controller detach/cancel | | 当前 `ImageLoadResult` lease | Source 清空、替换终态、detach 或 dispose | | AvatarGroup motion binding | group detach 时 dispose | | AvatarGroup fold Flyout/临时视觉 | rebuild、detach 或 owner 释放时清理 | Reattach 时 controller 使用当前 Source 和当前尺寸重新请求。文字布局不订阅 template part 事件;模板重套用后由 Border、Viewbox 和 TextBlock 的标准 measure/arrange 流程重新建立可用宽度、自然尺寸与缩放关系。AvatarGroup 只管理组合视觉,不取消或复用子 Avatar 的图片请求。 ## 7. 交互与事件处理 Avatar 没有独立的 pointer 或 keyboard 状态机;基础交互由 Avalonia 控件和 Theme 处理。图片回调先在 UI dispatcher 校验 generation 和 attach 状态,再提交结果。成功时先保存 lease、状态和内容,再触发 `ImageOpened`;最终失败时先提交 error 状态, 再触发 `ImageFailed`。取消、detach 和过期 generation 不触发失败事件。`Reload()` 是唯一命令式图片入口。 ## 8. 内部算法与关键流程 `AvatarTheme.axaml` 通过 internal `ContentType` 选择 Image、Text 或 Icon presenter。唯一稳定 template part 是 `PART_TextPresenter: Avalonia.Controls.TextBlock`。 `AbstractAvatar` 只把 `Gap` 投影为模板内部的水平 Text padding。Border 用该 padding 定义文字可用区域,Viewbox 以 `PART_TextPresenter` 的自然排版尺寸为输入并使用 `StretchDirection=DownOnly`:自然宽度不超过可用宽度时保持原尺寸,超过时 等比缩小到可用区域。缩放和居中由同一个模板布局坐标系负责,不单独调用文字测量工具,也不叠加横向补偿 transform;Text、 Font、Gap 或控件尺寸变化通过 Avalonia measure/arrange 自动重新计算。 `Size` 非 `NaN` 时保存原 SizeType 并切换到 Custom;恢复 `NaN` 后回到原 SizeType。Circle 形状把最终宽度的一半写入模板 CornerRadius。图片请求尺寸来自 Bounds 与 TopLevel render scaling,而不是 Token 名称或逻辑像素直接值。 ## 9. 资源、性能与 AOT 边界 - 不进行同步网络或文件 I/O。 - 不使用固定延迟等待布局;Arrange 和尺寸 bucket 是唯一尺寸就绪信号。 - 文字适配使用无状态的 Border/Viewbox 布局,不重复测量字形,不持有 template part 事件订阅。 - 不创建控件私有 `HttpClient`、cache 或 scheduler。 - Theme、codec 和 loader 通过静态注册保留,不使用反射扫描。 - Browser 使用统一 loader 的 Browser 能力矩阵;Avatar 不增加平台分支 API。 - SVG 一致性模式是封闭的应用级 enum;Avatar 不持有 validator delegate,不增加反射或 NativeAOT 动态发现边界。 - 快速 Source 切换只允许当前 generation 回写,旧结果必须及时释放。 ## 10. 维护不变量 - `ImageLoadController` 是 Avatar 与 `AsyncImage` 的单图状态机 owner,不在 Avatar 内复制请求协调逻辑。 - 内容优先级始终为 Image、Text、Icon;Loading 和取消不清空可用降级内容。 - 每个已提交 cache image 必须由一个有效 lease 支撑;borrowed image 永不由控件销毁。 - Source、options、尺寸或 attach generation 变化后,旧结果只能释放,不能回写状态。 - `Compatible`/`Strict` 差异只能来自 Shared `SvgContentValidator`;Avatar 不按 Asset/File/HTTP 或单次请求改写该结论。 - Template reapply、detach、group rebuild 和 Application dispose 都有明确的取消、解绑和释放路径。 - 文字的自然尺寸、可用宽度、缩放和居中必须由同一个模板布局路径完成;不得恢复独立文字测量或补偿平移。 - 单头像和 Token 只存在于 `AtomUI.Controls`,Desktop 包只拥有 AvatarGroup 组合能力。 ## 11. 测试与验证 维护 Avatar 时至少覆盖: - Source、FallbackSource、Text、Icon 优先级与状态伪类。 - A -> B 快速切换、Reload、detach/reattach、尺寸 bucket 和 render scaling。 - 主失败/fallback 成功、最终失败、事件顺序和 progress generation。 - owned cache image 与 borrowed image 的释放责任。 - AvatarTheme 独立于 Desktop 类型,AvatarGroup 仍能消费同一 Token。 - 短文本不放大;临界宽度与长文本缩小后保持水平居中,宽度不超过 `Width - 2 * Gap`;运行时 Gap 变化重新布局。 - Gallery 示例只使用 `Source`/`ImageSource`。 - 默认 Application `Compatible` 模式加载含重复 id 的本地 SVG;`Strict` 下同一来源报告 `InvalidImageData`,并继续验证 FallbackSource、`ImageFailed`、伪类和 generation 语义。两种模式对不安全或超预算 SVG 的结果完全相同。