mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-19 23:30:44 +08:00
271 lines
15 KiB
Markdown
271 lines
15 KiB
Markdown
# SplitButton 组合按钮
|
||
|
||
## 概述
|
||
|
||
组合按钮(SplitButton)是将一个主操作按钮和一个下拉菜单按钮合并在一起的复合控件。左侧是主按钮(Primary Button),点击执行主操作;右侧是下拉触发按钮(Secondary Button),点击或悬浮弹出菜单供用户选择更多操作。
|
||
|
||
SplitButton 对应 Ant Design 的 `Dropdown.Button`(带 `type` 属性的变体)。AtomUI 的 `SplitButton` 完整复刻了 [Ant Design 5.0 Dropdown.Button](https://ant.design/components/dropdown-cn) 的设计规范,在 .NET / Avalonia 平台上提供与 Ant Design React 版一致的视觉体验和交互行为。
|
||
|
||
---
|
||
|
||
## 设计原理
|
||
|
||
### Ant Design 的 Dropdown.Button 设计哲学
|
||
|
||
Ant Design 的 `Dropdown.Button` 呈现为左右两部分:左侧主按钮可触发 `onClick`,右侧小按钮弹出下拉菜单。这种设计解决了一个常见的交互问题:**用户既需要一个快速执行主操作的入口,又需要一个展开更多可选操作的途径**。典型场景包括:
|
||
|
||
| 场景 | 示例 |
|
||
|---|---|
|
||
| 主操作 + 更多操作 | 「提交」+ 下拉「保存草稿 / 导出 PDF / 打印」 |
|
||
| 默认选项 + 替代选项 | 「下载」+ 下拉「下载为 PDF / Word / Excel」 |
|
||
| 快捷操作 + 完整菜单 | 「回复」+ 下拉「回复全部 / 转发 / 标记」 |
|
||
|
||
**两种按钮类型**(通过 `type` 属性控制):
|
||
|
||
| 类型 | 设计意图 | 典型用途 |
|
||
|---|---|---|
|
||
| ⚪️ **Default** | 中等视觉权重,白色背景 + 边框,适合并列操作场景 | 「更多操作」「选项」 |
|
||
| 🔵 **Primary** | 视觉权重最高,实心填充主色调,强调主操作 | 「提交」+ 下拉更多 |
|
||
|
||
**状态修饰**(可与类型自由组合):
|
||
|
||
| 状态 | 设计意图 |
|
||
|---|---|
|
||
| ⚠️ **危险(Danger)** | 红色系警示用户该操作具有破坏性 |
|
||
| 🚫 **禁用(Disabled)** | 降低不透明度 + 灰色调,操作不可用 |
|
||
|
||
### Avalonia ContentControl 基础能力
|
||
|
||
AtomUI 的 `SplitButton` 继承自 Avalonia 框架的 `Avalonia.Controls.ContentControl`。理解 ContentControl 的基础能力有助于理解 SplitButton 在其之上做了哪些扩展。
|
||
|
||
**Avalonia ContentControl 的核心职责:**
|
||
|
||
`ContentControl` 是 Avalonia 中可容纳任意内容的基础模板控件。它的继承链为:
|
||
|
||
```
|
||
Control → TemplatedControl → ContentControl
|
||
```
|
||
|
||
作为 `ContentControl`,SplitButton 能容纳任意内容(文本、图标、甚至复杂布局),并通过 `ContentPresenter` 在模板中呈现。
|
||
|
||
**ContentControl 提供的基础属性:**
|
||
|
||
| 属性 | 说明 |
|
||
|---|---|
|
||
| `Content` | 控件内容,可以是文本字符串,也可以是任意控件 |
|
||
| `ContentTemplate` | 内容模板,用于自定义内容的呈现方式 |
|
||
| `HorizontalContentAlignment` | 内容水平对齐方式 |
|
||
| `VerticalContentAlignment` | 内容垂直对齐方式 |
|
||
|
||
**ContentControl 提供的基础伪类:**
|
||
|
||
| 伪类 | 说明 |
|
||
|---|---|
|
||
| `:pointerover` | 鼠标悬浮在控件上 |
|
||
| `:disabled` | `IsEnabled == false` |
|
||
| `:focus` | 控件获得焦点 |
|
||
| `:focus-visible` | 通过键盘(Tab)获得焦点 |
|
||
|
||
### 独立的控件设计
|
||
|
||
与 `DropdownButton`(继承自 `Button`)不同,`SplitButton` 直接继承自 `ContentControl`。这是因为它需要管理两个独立的内部 Button(`PrimaryButton` 和 `SecondaryButton`),它们有各自的交互状态和样式。SplitButton 自身承担以下职责:
|
||
|
||
- **Command 绑定**:实现 `ICommandSource` 接口,将 `Command`/`CommandParameter` 绑定到主按钮的点击行为
|
||
- **尺寸协调**:统一管理两个内部 Button 的 `SizeType`
|
||
- **圆角分配**:在 `ArrangeOverride` 中精确计算和分配左右按钮的圆角
|
||
- **Flyout 管理**:通过 `FlyoutStateHelper` 管理弹出层,`AnchorTarget` 设置为 `SecondaryButton`
|
||
- **键盘交互**:支持 Space/Enter 触发主按钮、Alt+Down/F4 打开 Flyout、Escape 关闭 Flyout
|
||
|
||
### AtomUI 的扩展设计
|
||
|
||
AtomUI `SplitButton` 在 Avalonia ContentControl 的基础上,遵循 Ant Design 5.0 规范做了以下扩展:
|
||
|
||
| 扩展能力 | 实现方式 | 设计动机 |
|
||
|---|---|---|
|
||
| **两种按钮类型** | `IsPrimaryButtonType` 布尔属性 → `EffectiveButtonType` 内部属性驱动内部 Button 样式 | 对齐 Ant Design 的 `type` 属性,支持 Primary / Default |
|
||
| **三种尺寸** | `ISizeTypeAware` 接口 + `SizeType` 共享属性 | 全局统一的 Large / Middle / Small 尺寸系统 |
|
||
| **危险状态** | `IsDanger` 属性透传至内部 Button | 红色系警示样式 |
|
||
| **图标支持** | `Icon` 属性(主按钮图标)+ `OpenIndicator` 属性(下拉按钮图标) | 灵活的图标配置 |
|
||
| **Flyout 弹出** | `FlyoutStateHelper` + `Flyout` 属性 + `TriggerType` | 完整的下拉菜单管理 |
|
||
| **弹出位置** | `Placement` / `PlacementAnchor` / `PlacementGravity` | 灵活的弹出方向控制 |
|
||
| **触发方式** | `TriggerType`(Click / Hover) | 支持点击和悬浮两种触发模式 |
|
||
| **点击波纹** | `IWaveSpiritAwareControl` + 内部 Button 各自的 `WaveSpiritDecorator` | 复刻 Ant Design 的 Wave 点击涟漪效果 |
|
||
| **过渡动画** | `IsMotionEnabled` 透传至内部 Button 和 Flyout | 背景色、前景色、边框色平滑过渡 |
|
||
| **紧凑空间** | `ICompactSpaceAware` 接口 | 在 `Space.Compact` 容器中自动调整圆角和边框 |
|
||
| **Command 绑定** | `ICommandSource` 接口 | 完整的 MVVM Command 绑定支持 |
|
||
| **键盘快捷键** | `HotKey` 属性 | 键盘快捷键触发主操作 |
|
||
| **Design Token** | 复用 `ButtonToken` + `RegisterTokenResourceScope` | 所有视觉值从全局/组件级 Token 派生 |
|
||
|
||
---
|
||
|
||
## 功能详解
|
||
|
||
### 内部按钮结构
|
||
|
||
SplitButton 的模板包含两个 AtomUI `Button` 实例:
|
||
- **PrimaryButton**(`PART_PrimaryButton`):左侧主按钮,接收 `Content`、`ContentTemplate`、`Icon`,触发 `Click` 事件和 `Command`
|
||
- **SecondaryButton**(`PART_SecondaryButton`):右侧下拉按钮,显示 `OpenIndicator` 图标(默认 `EllipsisOutlined`),负责触发 Flyout
|
||
|
||
两个按钮共享以下属性(通过 `TemplateBinding` 同步):
|
||
- `SizeType`:尺寸保持一致
|
||
- `IsDanger`:危险样式同步
|
||
- `ButtonType`:通过 `EffectiveButtonType` 统一驱动
|
||
- `IsMotionEnabled` / `IsWaveSpiritEnabled`:动画行为同步
|
||
- `IsEnabled`:启用/禁用状态同步
|
||
|
||
### 主按钮类型(IsPrimaryButtonType)
|
||
|
||
通过 `IsPrimaryButtonType` 控制是否使用 Primary 样式:
|
||
- `true`:左右两个按钮都使用 `ButtonType.Primary`(蓝色背景),中间绘制白色分隔线
|
||
- `false`(默认):左右两个按钮都使用 `ButtonType.Default`(白色背景 + 边框)
|
||
|
||
当 `IsPrimaryButtonType=True` 时,SplitButton 在 `Render` 方法中使用 `SplitSeparatorBrush` 绘制两个按钮之间的分隔线背景,确保视觉上无缝衔接。
|
||
|
||
### 圆角分配
|
||
|
||
SplitButton 通过 `ConfigureButtonCornerRadius` 方法智能地将 `CornerRadius` 分配给两个内部按钮:
|
||
- **PrimaryButton**:仅保留左侧圆角 `(TopLeft, 0, 0, BottomLeft)`
|
||
- **SecondaryButton**:仅保留右侧圆角 `(0, TopRight, BottomRight, 0)`
|
||
- 这确保了组合后的外观是一个整体的圆角矩形
|
||
|
||
当控件处于 CompactSpace 中时,会先通过 `CompactSpace.CalculateEffectiveCornerRadius` 计算有效圆角,再进行左右分配。
|
||
|
||
### 布局精排(ArrangeOverride)
|
||
|
||
SplitButton 在 `ArrangeOverride` 中对 SecondaryButton 进行微调布局:
|
||
- **Default 样式**(`IsPrimaryButtonType=false`):SecondaryButton 向左偏移一个边框宽度,使两个按钮的边框重叠,视觉上呈现为一条分隔线而非双边框
|
||
- **Primary 样式**(`IsPrimaryButtonType=true`):SecondaryButton 向右收缩一个边框宽度,消除 Primary 模式下的边框冗余
|
||
|
||
### Flyout 管理
|
||
|
||
SplitButton 使用 `FlyoutStateHelper` 管理弹出层的生命周期:
|
||
|
||
- **AnchorTarget** 设置为 `SecondaryButton`,确保 Flyout 从右侧按钮弹出
|
||
- 支持 `MenuFlyout` 类型,内含 `MenuItem` 菜单项
|
||
- 属性透传:`Placement`、`PlacementAnchor`、`PlacementGravity`、`IsShowArrow`、`IsPointAtCenter`、`GutterToFlyout`、`IsMotionEnabled` 等属性通过 `BindUtils.RelayBind` 同步到 Flyout 实例
|
||
- 当 Flyout 为 `MenuFlyout` 时,禁用其内置的鼠标点击检测(`IsDetectMouseClickEnabled = false`),由 SplitButton 自身管理关闭逻辑
|
||
|
||
### ZIndex 管理
|
||
|
||
模板中的两个内部 Button 通过 ZIndex 控制叠放顺序:
|
||
- 正常状态:`NormalZIndex = 1000`
|
||
- 悬浮状态:`ActivatedZIndex = 2000`
|
||
|
||
这确保当鼠标悬浮在某个按钮上时,该按钮的边框和阴影不会被相邻按钮遮挡。
|
||
|
||
### 紧凑空间(Compact Space)
|
||
|
||
SplitButton 实现了 `ICompactSpaceAware` 接口。当放置在 `Space.Compact` 容器中时:
|
||
- 容器通知 SplitButton 其 `CompactSpaceItemPosition`(First / Middle / Last / OnlyOne)和 `CompactSpaceOrientation`(Horizontal / Vertical)
|
||
- SplitButton 根据位置先计算自身的有效圆角,再分配给左右内部 Button
|
||
- 边框厚度通过 `GetBorderThicknessForCompactSpace` 方法返回,供容器计算相邻控件的边距
|
||
|
||
---
|
||
|
||
## 与 Ant Design 规范的对齐程度
|
||
|
||
| 特性 | Ant Design (React) | AtomUI (Avalonia) | 对齐情况 |
|
||
|---|---|---|---|
|
||
| 左右分离式设计 | ✅ `Dropdown.Button` | ✅ `SplitButton` | ✅ 完全对齐 |
|
||
| 主按钮 Primary/Default | ✅ `type` prop | ✅ `IsPrimaryButtonType` | ✅ 完全对齐 |
|
||
| 危险样式 | ✅ `danger` | ✅ `IsDanger` | ✅ 完全对齐 |
|
||
| 尺寸 | ✅ `size` | ✅ `SizeType` | ✅ 完全对齐 |
|
||
| 触发方式 | ✅ `trigger` | ✅ `TriggerType` | ✅ 完全对齐 |
|
||
| 弹出位置 | ✅ `placement` | ✅ `Placement` | ✅ 完全对齐 |
|
||
| 主按钮点击事件 | ✅ `onClick` | ✅ `Click` 事件 | ✅ 完全对齐 |
|
||
| 主按钮图标 | ✅ `icon` | ✅ `Icon` | ✅ 完全对齐 |
|
||
| 自定义下拉图标 | ✅ `buttonsRender` | ✅ `OpenIndicator` | ✅ 对齐(实现方式不同,效果一致) |
|
||
| 禁用 | ✅ `disabled` | ✅ `IsEnabled` | ✅ 完全对齐 |
|
||
| 下拉箭头 | ✅ `arrow` | ✅ `IsShowArrow` | ✅ 完全对齐 |
|
||
| 悬浮延迟 | ✅ `mouseEnterDelay` / `mouseLeaveDelay` | ✅ `MouseEnterDelay` / `MouseLeaveDelay` | ✅ 完全对齐 |
|
||
| 点击波纹 | ✅ Wave 组件 | ✅ 内部 Button 自带 `WaveSpiritDecorator` | ✅ 完全对齐 |
|
||
| Command 绑定 | — | ✅ `ICommandSource` | ➕ AtomUI 增强(桌面端特有) |
|
||
| 键盘快捷键 | — | ✅ `HotKey` + 键盘导航 | ➕ AtomUI 增强(桌面端特有) |
|
||
| `href` / `target` | ✅ 链接跳转 | ❌ 不适用 | — 桌面端无需 HTML 超链接语义 |
|
||
|
||
---
|
||
|
||
## 继承关系
|
||
|
||
```
|
||
Avalonia.Controls.Primitives.TemplatedControl
|
||
└── Avalonia.Controls.ContentControl
|
||
└── AtomUI.Desktop.Controls.SplitButton
|
||
├── implements ICommandSource
|
||
├── implements ISizeTypeAware
|
||
├── implements IWaveSpiritAwareControl
|
||
└── implements ICompactSpaceAware
|
||
```
|
||
|
||
`SplitButton` 不继承自 `Button`,而是直接继承自 `ContentControl`,通过在模板中组合两个独立的 `Button` 实例来实现左右分离的交互模式。
|
||
|
||
**各层级职责划分:**
|
||
|
||
| 层级 | 提供的能力 |
|
||
|---|---|
|
||
| `ContentControl` | 任意内容容纳、`Content` / `ContentTemplate`、`ContentPresenter` 模板化呈现 |
|
||
| `AtomUI.Desktop.Controls.SplitButton` | Ant Design 视觉体系(两种类型/三种尺寸)、Design Token 集成(复用 ButtonToken)、内部双 Button 布局协调、Flyout 弹出管理、Command 绑定、圆角分配、ZIndex 管理、紧凑空间适配 |
|
||
|
||
**实现的共享接口:**
|
||
|
||
| 接口 | 定义位置 | 作用 |
|
||
|---|---|---|
|
||
| `ICommandSource` | `Avalonia.Input` | 支持 `Command` / `CommandParameter` MVVM 命令绑定 |
|
||
| `ISizeTypeAware` | `AtomUI.Controls.Shared` | 支持 `SizeType`(Small / Middle / Large)三种尺寸切换 |
|
||
| `IWaveSpiritAwareControl` | `AtomUI.Controls.Shared` | 控制内部 Button 的点击涟漪(Wave)动画效果 |
|
||
| `ICompactSpaceAware` | `AtomUI.Controls.Shared` | 在 `Space.Compact` 中使用时自动调整圆角和边框 |
|
||
|
||
---
|
||
|
||
## 源码位置
|
||
|
||
| 层级 | 文件路径 | 说明 |
|
||
|---|---|---|
|
||
| 控件类 | `src/AtomUI.Desktop.Controls/Buttons/SplitButton.cs` | 组合按钮实现 |
|
||
| Token 定义 | `src/AtomUI.Desktop.Controls/Buttons/ButtonToken.cs` | 组件级 Design Token(复用 Button 的 Token) |
|
||
| 主题模板 | `src/AtomUI.Desktop.Controls/Buttons/Themes/SplitButtonTheme.axaml` | ControlTheme AXAML |
|
||
| 模板常量 | `src/AtomUI.Desktop.Controls/Buttons/Themes/SplitButtonThemeConstants.cs` | 模板部件名称常量 |
|
||
| Gallery 示例 | `controlgallery/AtomUIGallery/ShowCases/Views/General/SplitButtonShowCase.axaml` | 使用范例 |
|
||
|
||
---
|
||
|
||
## 模板结构
|
||
|
||
SplitButton 的 ControlTemplate 采用 DockPanel 水平布局,包含两个独立的 AtomUI Button:
|
||
|
||
```
|
||
DockPanel#PART_MainLayout (水平布局容器)
|
||
├── Button#PART_PrimaryButton (主按钮 — 左侧)
|
||
│ ├── Content = {TemplateBinding Content}
|
||
│ ├── ContentTemplate = {TemplateBinding ContentTemplate}
|
||
│ ├── Icon = {TemplateBinding Icon}
|
||
│ ├── SizeType = {TemplateBinding SizeType}
|
||
│ ├── IsDanger = {TemplateBinding IsDanger}
|
||
│ ├── ButtonType = {TemplateBinding EffectiveButtonType}
|
||
│ ├── IsMotionEnabled = {TemplateBinding IsMotionEnabled}
|
||
│ └── IsWaveSpiritEnabled = {TemplateBinding IsWaveSpiritEnabled}
|
||
└── Button#PART_SecondaryButton (下拉按钮 — 右侧)
|
||
├── Icon = {TemplateBinding OpenIndicator} ← 默认 EllipsisOutlined
|
||
├── SizeType = {TemplateBinding SizeType}
|
||
├── IsDanger = {TemplateBinding IsDanger}
|
||
├── ButtonType = {TemplateBinding EffectiveButtonType}
|
||
├── IsMotionEnabled = {TemplateBinding IsMotionEnabled}
|
||
└── IsWaveSpiritEnabled = {TemplateBinding IsWaveSpiritEnabled}
|
||
```
|
||
|
||
**设计理由:**
|
||
- **内部使用完整 Button**:两个内部 Button 是完整的 AtomUI `Button` 实例,自带波纹效果、过渡动画、加载图标等全部 Button 能力,无需在 SplitButton 层重复实现。
|
||
- **DockPanel 布局**:使用 DockPanel 实现水平排列,PrimaryButton 占据主要空间,SecondaryButton 停靠在右侧。
|
||
- **ArrangeOverride 微调**:通过 `ArrangeOverride` 对 SecondaryButton 进行亚像素级的位置调整,实现边框重叠(Default 模式)或边框消除(Primary 模式)。
|
||
- **ZIndex 动态管理**:悬浮状态的按钮 ZIndex 提升至 2000,确保边框和阴影正确叠放。
|
||
|
||
### 模板部件常量
|
||
|
||
| 常量名 | 值 | 说明 |
|
||
|---|---|---|
|
||
| `SplitButtonThemeConstants.MainLayoutPart` | `"PART_MainLayout"` | 根布局面板(DockPanel) |
|
||
| `SplitButtonThemeConstants.PrimaryButtonPart` | `"PART_PrimaryButton"` | 左侧主按钮 |
|
||
| `SplitButtonThemeConstants.SecondaryButtonPart` | `"PART_SecondaryButton"` | 右侧下拉按钮 |
|
||
| `SplitButtonThemeConstants.NormalZIndex` | `1000` | 正常状态 ZIndex |
|
||
| `SplitButtonThemeConstants.ActivatedZIndex` | `2000` | 悬浮/激活状态 ZIndex |
|