14 KiB
Space 控件实现原理
1. 概述
AtomUI 的 Space 控件系列是对 Ant Design 5.0 Space 组件 的 C# / Avalonia 实现。Ant Design 中 Space 组件的设计意图是:为相邻组件提供统一的间距管理,避免开发者手动为每个组件设置 margin 或 padding。
AtomUI 将 Space 组件拆分为两个独立控件:
| 控件 | 对应 Ant Design | 功能定位 |
|---|---|---|
Space |
Space |
通用间距布局面板,支持水平/垂直排列、自动换行、间距预设(Small/Middle/Large/Custom)、Split 分隔符 |
CompactSpace |
Space.Compact |
紧凑组合模式,将多个控件无间距地组合在一起,自动处理圆角裁剪和边框重叠 |
二者虽然都位于 AtomUI.Desktop.Controls/Space/ 目录下,但功能和接口设计完全不同。
2. Space 控件(通用间距面板)
2.1 核心属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Orientation |
Orientation |
Horizontal |
排列方向 |
SizeType |
CustomizableSizeType |
Small |
间距预设档位(Small/Middle/Large/Custom) |
ItemSpacing |
double |
由 SizeType 决定 | 主轴方向的子项间距 |
LineSpacing |
double |
由 SizeType 决定 | 换行时的行间距 |
ItemsAlignment |
SpaceItemsAlignment |
Start |
交叉轴方向对齐方式(Start/Center/End) |
ItemWidth / ItemHeight |
double |
NaN |
统一子项宽/高(不设置则使用子项自身尺寸) |
SplitTemplate |
ITemplate<Control>? |
null |
子项之间插入的分隔符模板 |
Children |
AvaloniaList<Control> |
— | 子控件列表(标记为 [Content]) |
2.2 间距计算与 Token 系统
Space 的间距通过 Design Token 系统驱动。在构造函数中,Space 通过 ConfigureInstanceStyle() 方法注册了三组内联 Style,分别绑定到 SpaceToken 中的 Token 值:
| SizeType | ItemSpacing / LineSpacing 来源 | SharedToken 对应值 |
|---|---|---|
Small |
SpaceTokenKind.GapSmallSize |
SharedToken.SpacingXS(8px) |
Middle |
SpaceTokenKind.GapMiddleSize |
SharedToken.Spacing(16px) |
Large |
SpaceTokenKind.GapLargeSize |
SharedToken.SpacingLG(24px) |
Custom |
不绑定 Token,由用户直接设置 ItemSpacing / LineSpacing |
— |
// SpaceToken.CalculateTokenValues 中的推导逻辑
GapSmallSize = SharedToken.SpacingXS;
GapMiddleSize = SharedToken.Spacing;
GapLargeSize = SharedToken.SpacingLG;
2.3 布局算法
Space 的布局采用 UV 坐标系模型(类似 WPF WrapPanel),通过内部 UVSize 结构体在水平和垂直方向之间统一抽象:
- U 轴:主轴方向(Horizontal 时为水平,Vertical 时为垂直)
- V 轴:交叉轴方向
MeasureOverride
遍历 VisualChildren:
计算每个子项的 childSize(尊重 ItemWidth/ItemHeight 设置)
如果当前行累计 U + childSize.U + spacing > 可用空间 U:
换行:记录当前行高度到 panelSize.V,重置 curLineSize
否则:
累加到当前行 curLineSize.U += childSize.U + spacing
最终合并所有行的尺寸
关键细节:
- 不可见子项(
!child.IsVisible)不产生间距(nextSpacing = 0) - 换行时
lineSpacing仅在已有前一行时才累加
ArrangeOverride
按行逐个排列子项,支持 SpaceItemsAlignment 交叉轴对齐:
Start:子项贴行的起始边Center:子项在行高中居中End:子项贴行的末端
2.4 SplitTemplate(分隔符)
当设置了 SplitTemplate 时,Space 会在每两个子项之间插入一个由模板实例化的控件。此时集合变更的处理逻辑变为完全重建 Visual/Logical 树(HandleSplitTemplateChanged),因为分隔符的数量和位置与子项数量耦合。
2.5 对子控件的要求
Space 对子控件没有任何接口要求。任何 Control 都可以作为 Space 的子项,Space 只负责间距排列,不会修改子控件的任何属性。
3. CompactSpace 控件(紧凑组合面板)
3.1 设计意图
CompactSpace 对应 Ant Design 的 Space.Compact,用于将多个表单控件(按钮、输入框、选择器等)无间距地组合在一起,形成一个视觉整体。它的核心行为包括:
- 零间距排列:子项之间没有间距,紧密贴合
- 圆角裁剪:首个子项保留起始侧圆角,末个子项保留结束侧圆角,中间子项圆角全部清零
- 边框重叠消除:通过
RenderTransform负偏移消除相邻子项的重复边框 - ZIndex 动态管理:聚焦/悬停的子项自动提升 ZIndex,确保边框不被遮盖
3.2 核心属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Orientation |
Orientation |
Horizontal |
排列方向 |
SizeType |
SizeType |
Middle |
尺寸档位,自动传递给子控件 |
Children |
AvaloniaList<Control> |
— | 子控件列表(必须实现 ICompactSpaceAware) |
ItemSize(附加属性) |
CompactSpaceSize |
Auto |
子项宽度分配模式(Auto / 像素值 / Star 比例值) |
3.3 ICompactSpaceAware 接口
这是 CompactSpace 与子控件交互的核心契约。定义在 ICompactSpaceAware.cs:
internal interface ICompactSpaceAware
{
void NotifyPositionChange(SpaceItemPosition? position);
void NotifyOrientationChange(Orientation orientation);
bool IsAlwaysActiveZIndex() => false;
bool IgnoreZIndexChange() => false;
double GetBorderThickness();
}
| 方法 | 调用时机 | 子控件应做的响应 |
|---|---|---|
NotifyPositionChange(position) |
子项集合变化或排列更新时 | 更新自身圆角裁剪策略 |
NotifyOrientationChange(orientation) |
方向变化时 | 更新圆角和偏移方向 |
IsAlwaysActiveZIndex() |
添加子项时查询 | 返回 true 则始终置于高 ZIndex(如 Primary Button) |
IgnoreZIndexChange() |
添加子项时查询 | 返回 true 则不参与 Focus/Hover ZIndex 管理 |
GetBorderThickness() |
布局阶段计算偏移量 | 返回主轴方向的边框厚度 |
3.4 位置通知与圆角裁剪
CompactSpace 在 ConfigureSizeDefinitions() 中计算每个子项的位置:
SpaceItemPosition.First — 第一个子项
SpaceItemPosition.Middle — 中间子项
SpaceItemPosition.Last — 最后一个子项
First | Last — 唯一的子项(保留全部圆角)
子控件收到 NotifyPositionChange 后,通过静态辅助方法 CompactSpace.CalculateEffectiveCornerRadius() 计算有效圆角:
水平方向:
First → 清零右侧圆角(TopRight = 0, BottomRight = 0)
Middle → 全部圆角清零
Last → 清零左侧圆角(TopLeft = 0, BottomLeft = 0)
垂直方向:
First → 清零底部圆角
Middle → 全部圆角清零
Last → 清零顶部圆角
3.5 边框重叠消除
CompactSpaceItem(每个子项的包装容器)在 MeasureOverride 中,根据子项位置和边框厚度计算负偏移:
var delta = borderThickness * PositionIndex;
if (CompactSpaceOrientation == Orientation.Horizontal)
RenderTransform = new TranslateTransform(-delta, 0);
else
RenderTransform = new TranslateTransform(0, -delta);
这使得第 N 个子项向前偏移 N × borderThickness 像素,实现视觉上的边框合并。
3.6 ZIndex 动态管理
CompactSpace 为子项注册 GotFocus、PointerEntered、PointerExited 事件和 FocusWithIn 伪类监听:
- 聚焦/悬停时:当前子项
ZIndex = 1000(ACTIVE_ZINDEX),其他非 Active 子项重置为0 IsAlwaysActiveZIndex():Primary Button 等始终保持高 ZIndex,避免被相邻控件遮盖IgnoreZIndexChange():如CompactSpaceAddOn不参与 ZIndex 管理
3.7 CompactSpaceSize 尺寸分配
通过附加属性 CompactSpace.ItemSize 控制子项在 Grid 中的空间分配:
| 设置方式 | 说明 | 示例 |
|---|---|---|
Auto(默认) |
子项按自身尺寸显示 | <atom:Button /> |
| 像素值 | 固定像素宽/高 | atom:CompactSpace.ItemSize="100" |
| Star 比例值 | 按比例分配剩余空间 | atom:CompactSpace.ItemSize="3*" |
3.8 辅助组件
| 组件 | 说明 |
|---|---|
CompactSpaceItem |
内部包装容器(Decorator),负责中继 ICompactSpaceAware 属性、计算偏移量 |
CompactSpaceAddOn |
用于在 CompactSpace 中添加前/后缀标签(如 $ 符号),实现 ICompactSpaceAware |
CompactSpaceFiller |
占位空白区域,不参与圆角/ZIndex/边框计算 |
4. 已实现 ICompactSpaceAware 的控件
以下控件已实现 ICompactSpaceAware 接口,可直接放入 CompactSpace:
| 控件 | 所在项目 | 特殊行为 |
|---|---|---|
Button |
AtomUI.Desktop.Controls | IsAlwaysActiveZIndex() → Primary 类型返回 true |
TextBox |
AtomUI.Desktop.Controls | 标准圆角裁剪和边框偏移 |
ButtonSpinner |
AtomUI.Desktop.Controls | 通过 DecoratedBox 判断边框样式 |
NumericUpDown |
AtomUI.Desktop.Controls | 继承自 ButtonSpinner 的行为 |
AbstractSelect |
AtomUI.Desktop.Controls | 所有选择器的基类 |
InfoPickerInput |
AtomUI.Desktop.Controls | 日期/时间选择器的基类 |
CompactSpaceAddOn |
AtomUI.Desktop.Controls | IgnoreZIndexChange() → 返回 true |
CompactSpaceFiller |
AtomUI.Desktop.Controls | 空操作实现,不参与任何计算 |
AbstractColorPicker |
AtomUI.Desktop.Controls.ColorPicker | 颜色选择器 |
实现模式示例(以 Button 为例)
public class Button : AvaloniaButton,
ISizeTypeAware,
ICompactSpaceAware, // ← 实现接口
...
{
// 1. 注册 CompactSpace 相关的 StyledProperty(使用 AddOwner 模式)
internal static readonly StyledProperty<SpaceItemPosition?> CompactSpaceItemPositionProperty =
CompactSpaceAwareControlProperty.CompactSpaceItemPositionProperty.AddOwner<Button>();
internal static readonly StyledProperty<Orientation> CompactSpaceOrientationProperty =
CompactSpaceAwareControlProperty.CompactSpaceOrientationProperty.AddOwner<Button>();
internal static readonly StyledProperty<bool> IsUsedInCompactSpaceProperty =
CompactSpaceAwareControlProperty.IsUsedInCompactSpaceProperty.AddOwner<Button>();
// 2. 声明 EffectiveCornerRadius(主题中使用此属性代替 CornerRadius)
internal static readonly DirectProperty<Button, CornerRadius> EffectiveCornerRadiusProperty = ...;
// 3. 监听属性变化,更新有效圆角
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
...
if (change.Property == CornerRadiusProperty ||
change.Property == CompactSpaceItemPositionProperty ||
change.Property == CompactSpaceOrientationProperty)
{
ConfigureEffectiveCornerRadius();
}
}
// 4. 使用静态辅助方法计算有效圆角
private void ConfigureEffectiveCornerRadius()
{
EffectiveCornerRadius = CompactSpace.CalculateEffectiveCornerRadius(
CornerRadius, IsUsedInCompactSpace,
CompactSpaceItemPosition, CompactSpaceOrientation);
}
// 5. 实现 ICompactSpaceAware 接口方法
void ICompactSpaceAware.NotifyPositionChange(SpaceItemPosition? position)
{
IsUsedInCompactSpace = position != null;
CompactSpaceItemPosition = position;
}
void ICompactSpaceAware.NotifyOrientationChange(Orientation orientation)
{
CompactSpaceOrientation = orientation;
}
bool ICompactSpaceAware.IsAlwaysActiveZIndex()
{
return ButtonType == ButtonType.Primary; // Primary 按钮始终高 ZIndex
}
double ICompactSpaceAware.GetBorderThickness()
{
return CompactSpaceOrientation == Orientation.Horizontal
? BorderThickness.Left : BorderThickness.Top;
}
}
共享属性定义
所有 ICompactSpaceAware 控件通过 CompactSpaceAwareControlProperty 共享属性(使用 AddOwner 模式):
internal abstract class CompactSpaceAwareControlProperty
{
public static readonly StyledProperty<SpaceItemPosition?> CompactSpaceItemPositionProperty = ...;
public static readonly StyledProperty<Orientation> CompactSpaceOrientationProperty = ...;
public static readonly StyledProperty<bool> IsUsedInCompactSpaceProperty = ...;
}
5. 架构关系图
CompactSpace
│
├── ConfigureSizeDefinitions() ── 计算子项位置并通知
│ │
│ ├── ICompactSpaceAware.NotifyPositionChange(First/Middle/Last)
│ └── ICompactSpaceAware.NotifyOrientationChange(Horizontal/Vertical)
│
├── NotifyAddCompactSpaceItem() ── 包装为 CompactSpaceItem + 事件绑定
│ │
│ ├── GotFocus / PointerEntered / PointerExited → ZIndex 管理
│ ├── ISizeTypeAware? → 绑定 SizeType
│ └── CompactSpaceItem → RelayBind 属性到子控件
│
└── CompactSpaceItem (Decorator) ── 计算 RenderTransform 偏移消除边框重叠
│
└── 子控件 (ICompactSpaceAware)
│
├── EffectiveCornerRadius ← CalculateEffectiveCornerRadius()
├── GetBorderThickness() → 用于偏移量计算
└── IsAlwaysActiveZIndex() / IgnoreZIndexChange() → ZIndex 策略