12 KiB
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 阴影和边框厚度。CardActionPanelpointer over 改变 Foreground。CardActionButtonpointer over 改变 IconBrush。- Tab 选择由内部
TabControl处理。 - Loading 状态由
Skeleton处理。
集合事件路径:
Card.Actions.CollectionChangedCardActionPanel.Actions.CollectionChangedCardTabsContent.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,并检查相对链接存在。