Files

12 KiB
Raw Permalink Blame History

Card 桌面版实现原理

本文档描述 Card 桌面版的内容类型识别、Actions 同步、Grid 内容容器生命周期、Tabs 内容桥接、主题状态同步和维护边界。公共设计与 API 契约见 Card 桌面版架构设计,Token 语义见 Card Token 设计,变化记录见 Card Changelog。

1. 实现定位

Card 的实现重点是把 Header/Extra/Cover/Content/Actions 组合成一个稳定面板,并根据直接内容类型选择不同 body 视觉。Card 自身不维护业务数据模型,也不管理 tab 选择、grid item 业务状态或 action 命令执行。

本文档只描述 Card 相关实现结构,不重新说明 HeaderedContentControl、ItemsControl、TabControl、Skeleton、Avalonia Grid 或 Token 系统的通用机制。

2. 源码文件结构

主要源码:

  • src/AtomUI.Desktop.Controls/Card/Card.cs:根 Card 公共 API、Actions 集合、内容类型识别、Header/Content 边框和圆角派生状态、Size/Motion 内容同步。
  • src/AtomUI.Desktop.Controls/Card/CardActionPanel.cs:内部操作区,承载 Actions,维护 UniformGrid children,并渲染分隔线。
  • src/AtomUI.Desktop.Controls/Card/CardActionButton.cs:Card 操作区推荐按钮类型,提供主题 key 边界。
  • src/AtomUI.Desktop.Controls/Card/CardMetaContent.cs:头像、标题和描述内容的元信息承载控件。
  • src/AtomUI.Desktop.Controls/Card/CardGridContent.cs:Card grid 内容的 ItemsControl,负责 Grid panel 行列定义同步和容器准备/清理。
  • src/AtomUI.Desktop.Controls/Card/CardGridItem.cs:Grid item 容器,负责 content、row/column/span 和 hover/size/motion 状态。
  • src/AtomUI.Desktop.Controls/Card/CardTabsContent.cs:Card tabs 内容桥接控件,负责把 Items 集合复制到内部 TabControl。
  • src/AtomUI.Desktop.Controls/Card/CardPseudoClass.cs:Card 稳定伪类常量。
  • src/AtomUI.Desktop.Controls/Card/CardToken.cs:Card 控件 Token。
  • src/AtomUI.Desktop.Controls/Card/Themes/*.axaml:根主题、操作区、操作按钮、Meta、Grid、Tabs 等主题资源。

3. 核心类职责

Card 是状态协调器。它继承 HeaderedContentControl,负责公共属性、Headerless 伪类、Actions 同步、ContentType 派生、CardToken scope 注册和初始动效屏蔽。

CardActionPanel 是内部 action layout 和 render owner。它持有内部 Actions 集合,模板接入后将集合写入 UniformGrid,并在 Render 中绘制操作区顶部边线和纵向分隔线。

CardGridContent 是 grid 内容的 ItemsControl。它生成或复用 CardGridItem,把 ColumnDefinitions / RowDefinitions 同步到 ItemsPresenter 的 Grid panel,并为 PrepareCardGridItem 提供 container 生命周期 disposable。

CardGridItem 是 grid item 容器。它把 Row、Column、RowSpan、ColumnSpan 的变化写入 Avalonia Grid attached properties。

CardTabsContent 是 Card 与 TabControl 的桥接层。它承载 XAML Items,也暴露 TabItemsSource 和 TabItemTemplate,模板中真正的选择和内容显示由 TabControl 负责。

CardMetaContent 和 CardActionButton 是主题语义类型。它们逻辑很薄,但类型本身是用户和主题可依赖的契约。

4. 状态与数据流

内容类型流:

Card.Content changed
  -> Dispose old content state bindings
  -> inspect direct content
  -> ContentType = Meta / Grid / Tabs / Default
  -> bind SizeType/IsMotionEnabled to Grid or Tabs content when needed
  -> ConfigureContentCornerRadius()
  -> ConfigureHeaderBorderThickness()

ContentType 是内部 DirectProperty。它只由根 Card 根据直接 Content 类型维护,主题根据该状态选择 body padding、header border 和 corner radius。

Actions 流:

Card.Actions.CollectionChanged
  Add/Remove/Replace/Move
  -> PART_ActionPanel.Actions
  -> IsActionsPanelVisible = Actions.Count > 0

CardActionPanel.Actions.CollectionChanged 再同步到 UniformGrid.Children,并设置 UniformGrid.Columns = Actions.Count。

Grid 内容流:

CardGridContent.ColumnDefinitions / RowDefinitions
  -> SyncGridPanelProperties()
  -> ItemsPresenter.Panel as Grid
  -> grid.ColumnDefinitions / grid.RowDefinitions

Grid item 容器流:

PrepareContainerForItemOverride
  -> CardGridItem.Content / ContentTemplate
  -> bind SizeType / IsEnabled / IsMotionEnabled
  -> PrepareCardGridItem(..., CompositeDisposable)

ClearContainerForItemOverride
  -> dispose prepared container state

Tabs 内容流:

CardTabsContent.Items.CollectionChanged
  -> PART_TabControl.Items Add/Remove/Replace/Move

TabItemsSource 和 TabItemTemplate 通过 TemplateBinding 直接交给内部 TabControl。

5. 生命周期与模板接入

Card 构造阶段:

  • 注册 CardToken.ScopeProvider。
  • 订阅 Actions.CollectionChanged。Actions 是 Card 自身持有的生命周期集合,不需要在普通 detach 中解除。

Card 初始化和加载:

  • OnInitialized() 调用 DisableTransitions(),避免初始样式应用触发过渡。
  • OnLoaded() 通过 dispatcher 在加载完成后启用过渡。

Card 模板接入:

  • OnApplyTemplate() 清空旧 CardActionPanel.Actions。
  • 获取 PART_ActionPanel。
  • 将当前 Actions 复制到新的 action panel。
  • 重新配置 ContentType、Content corner radius 和 Header border。

内容状态绑定:

  • CardTabsContent 和 CardGridContent 作为直接内容时,Card 使用 BindUtils.RelayBind 同步 SizeType 和 IsMotionEnabled。
  • 该绑定无法放在 Card 根模板中表达,因为绑定目标是运行时 Content 对象而非稳定 template part。
  • _contentStateBindings 是这组绑定的 owner,内容重新配置时必须先释放旧绑定。

GridContent 生命周期:

  • OnApplyTemplate() 获取 PART_ItemsPresenter,调用 ApplyTemplate() 后同步 Grid 行列定义。
  • PrepareContainerForItemOverride() 每次准备容器前先释放该容器旧状态,防止 recycle 时保留旧 binding 或订阅。
  • ClearContainerForItemOverride() 释放对应 CompositeDisposable,再调用 base。

TabsContent 生命周期:

  • 构造阶段订阅自有 Items.CollectionChanged。
  • OnApplyTemplate() 清空旧内部 TabControl items,获取 PART_TabControl,再复制当前 Items。
  • Items 是控件自有集合,订阅随控件实例生命周期存在。

6. 交互与事件处理

Card 自身不处理 keyboard、focus、pointer capture、drag/drop 或 popup 事件。

交互路径主要由主题和内部控件负责:

  • IsHoverable 通过 selector 设置 cursor、pointer over 阴影和边框厚度。
  • CardActionPanel pointer over 改变 Foreground。
  • CardActionButton pointer over 改变 IconBrush。
  • Tab 选择由内部 TabControl 处理。
  • Loading 状态由 Skeleton 处理。

集合事件路径:

  • Card.Actions.CollectionChanged
  • CardActionPanel.Actions.CollectionChanged
  • CardTabsContent.Items.CollectionChanged

这些路径只同步视觉集合,不执行业务命令。

7. 内部算法与关键流程

7.1 ContentType 配置

内容类型配置必须先释放旧内容状态绑定,再按当前直接内容设置内部状态:

CardMetaContent -> Meta
CardTabsContent -> Tabs + bind state
CardGridContent -> Grid + bind state
others/null -> Default

Default 回退是必要状态,防止从 Grid/Tabs/Meta 切回普通内容后主题仍保留旧 padding、border 或 corner radius 分支。

7.2 Header 边框和内容圆角

Card 根据 ContentType 计算两个内部状态:

ContentType=Grid
  HeaderBorderThickness = 0
  EffectiveCornerRadius = top corners only

otherwise
  HeaderBorderThickness = bottom border
  EffectiveCornerRadius = CornerRadius

Borderless 和 Outlined 的实际边框厚度由 ConfigureContentBorderThickness() 根据 StyleVariant 计算。

7.3 Headerless 伪类

Headerless 条件:

Header == null
&& HeaderTemplate == null
&& Extra == null
&& ExtraTemplate == null

该伪类只表示 Header 区域是否有可展示入口,不受 Cover、Content、Actions 或 Loading 影响。

7.4 Grid item 准备和清理

CardGridContent 复用 Avalonia ItemsControl 容器机制:

  • 非 Visual item 且 CardGridItem.Content 未显式设置时,将 item 作为 content。
  • ItemTemplate 非空时同步到 CardGridItem.ContentTemplate。
  • Size、Enabled 和 Motion 状态通过 Avalonia property binding 同步到容器。
  • PrepareCardGridItem 允许派生类追加容器级绑定或订阅,追加内容必须放入传入的 CompositeDisposable。

容器 recycle 或清理时,CompositeDisposable 必须被释放,防止旧 item 状态泄漏到新 item。

7.5 Actions 渲染

CardActionPanel 使用 UniformGrid 均分 action。Render 阶段用当前 Actions 子项的 bounds 绘制:

  • 顶部横线。
  • 每个非最后 action 右侧纵向分隔线。

分隔线颜色和粗细来自 BorderBrush 和 BorderThickness.Left。

8. 资源、性能与 AOT 边界

Card 不依赖运行时反射发现模板结构。模板协作通过固定 part 名称、强类型控件和 Avalonia property binding 完成。

资源与生命周期边界:

  • CardToken 通过 token generator 注册,Theme 通过 CardTokenResource 使用。
  • CardActionButton、CardGridItem、CardMetaContent 等是 Visual 控件,Token resource 由主题和视觉树生命周期管理。
  • Card 到动态 Content 的 Size/Motion 同步使用 BindUtils.RelayBind,因为目标不是稳定 template part;绑定 owner 是 _contentStateBindings。
  • CardGridContent.PrepareCardGridItem 的扩展 disposable 由 _cardGridItemDisposables 管理,并在 container clear/recycle 时释放。
  • 不在 layout hot path 中读取全局资源或创建反射路径。

AOT 边界:

  • 不新增字符串 path binding、ReflectionBinding、assembly scan、Activator.CreateInstance(Type) 或动态成员访问。
  • API 与 Token 契约由控件文档、源码 public surface、Token 类型或生成数据维护。
  • Card 的动态内容同步只使用 AvaloniaProperty 强类型绑定。

9. 维护不变量

内部重构必须保持以下不变量:

  • ContentType 必须覆盖 Meta、Grid、Tabs 和 Default 四个分支。
  • 重新配置内容类型前必须释放旧内容状态绑定。
  • CardGridContent 的行列定义 wrapper 必须读写 ColumnDefinitionsProperty 和 RowDefinitionsProperty。
  • PrepareCardGridItem 传入的 CompositeDisposable 必须有清理路径。
  • CardActionPanel 换模板时必须清空旧 panel children,避免 action visual 同时挂到多个父级。
  • CardTabsContent 换模板时必须清空旧 TabControl items,再复制当前 Items。
  • Actions、CardTabsContent.Items 的 Reset 行为当前为 NotSupportedException,不能在结构整理中静默改变。
  • 初始 transition 禁用/加载后启用的顺序不能在未验证视觉影响时移除。
  • 空实现或薄实现的主题语义类型不能随意删除。

10. 测试与验证

验证范围:

  • CardBehaviorTests:CardGridContent 行列定义 wrapper、ContentType 回退、旧内容 SizeType 绑定释放、GridItem container disposable 释放。
  • Gallery Card 示例:Basic、NoBorder、Simple、CustomizedContent、CardInColumn、GridCard、InnerCard、LoadingCard、WithTabs、MoreContentConfiguration。
  • CardShowCasePageTests:Card ShowCase 页面结构、本地化、源码片段和示例 snapshot。
  • 修改运行时代码时运行 tests/AtomUI.Desktop.Controls.Tests 中 Card 相关测试,并按影响范围扩大到完整 Desktop 控件测试。
  • 修改 Gallery 或 Gallery 示例或源码片段时运行 tests/AtomUIGallery.Tests 中 Card ShowCase 相关测试。
  • 文档改动运行 git diff --check,并检查相对链接存在。