Files

257 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Button 按钮
## 概述
按钮(Button)是图形界面中最基础、最常见的交互控件,用于触发一个即时操作。用户通过点击按钮来启动业务逻辑,如提交表单、打开对话框、执行删除等。按钮是用户意图到应用响应之间的桥梁——它既承载视觉提示(告诉用户「这里可以点击」),又承载交互反馈(告诉用户「你的操作已被接受」)。
AtomUI 的 `Button` 控件完整复刻了 [Ant Design 5.0 Button](https://ant.design/components/button-cn) 的设计规范,在 .NET / Avalonia 平台上提供与 Ant Design React 版一致的视觉体验和交互行为。
---
## 设计原理
### Ant Design 的按钮设计哲学
Ant Design 对按钮的定位是:**「标记一个(或封装一组)操作命令,响应用户点击行为,触发相应的业务逻辑」**。为了帮助用户在不同场景中快速识别操作的优先级和性质,Ant Design 建立了一套完整的按钮分类体系:
**五种按钮类型**(按视觉权重从高到低排列):
| 类型 | 设计意图 | 典型用途 |
|---|---|---|
| 🔵 **主按钮(Primary)** | 视觉权重最高,实心填充主色调。一个操作区域**只应有一个**主按钮,引导用户完成核心操作 | 「提交」「确认」「保存」 |
| ⚪️ **默认按钮(Default)** | 中等视觉权重,白色背景 + 边框。用于同一区域中**无主次之分**的多个操作 | 「取消」「重置」「上一步」 |
| 😶 **虚线按钮(Dashed)** | 虚线边框传达「未完成 / 可添加」语义,暗示用户此处可以补充内容 | 「+ 添加条目」「上传文件」 |
| 🔤 **文本按钮(Text)** | 最低视觉权重,无边框无背景,仅保留文字。用于最次级操作,减少视觉干扰 | 「编辑」「更多」内联操作 |
| 🔗 **链接按钮(Link)** | 外观类似超链接,暗示"导航"而非"操作"。适合跳转类交互 | 「查看详情」「忘记密码?」 |
**四种状态修饰**(可与上述五种类型自由组合):
| 状态 | 设计意图 |
|---|---|
| ⚠️ **危险(Danger)** | 使用红色系警示用户该操作具有破坏性(删除、移动、修改权限),通常配合二次确认 |
| 👻 **幽灵(Ghost)** | 背景透明,文字/边框使用浅色或主题色。专为复杂背景场景设计(首页横幅、产品展示页) |
| 🚫 **禁用(Disabled)** | 降低不透明度 + 灰色调,告诉用户该操作当前不可用,通常需配合提示文案说明原因 |
| 🔃 **加载中(Loading)** | 显示旋转图标 + 降低不透明度,用于异步操作的等待反馈,同时防止用户重复提交 |
### Avalonia Button 基础能力
AtomUI 的 `Button` 继承自 Avalonia 框架的 `Avalonia.Controls.Button`。理解 Avalonia Button 的基础能力有助于理解 AtomUI 在其之上做了哪些扩展。
**Avalonia Button 的核心职责:**
Avalonia 的 `Button` 是一个标准的 `ContentControl`(内容控件),它实现了 `ICommandSource` 和 `IClickableControl` 接口。其核心行为是:**响应指针操作,在指针按下时提供视觉按压反馈(`:pressed` 伪类),在指针释放时触发 `Click` 事件**。它的继承链为:
```
Control → TemplatedControl → ContentControl → Button
```
作为 `ContentControl`,Button 能容纳任意内容(文本、图标、甚至复杂布局),并通过 `ContentPresenter` 在模板中呈现。Button 还提供了完整的命令绑定(MVVM `Command` / `CommandParameter`)、键盘快捷键(`HotKey`)、`IsDefault` / `IsCancel` 键盘激活、以及 `Flyout` 弹出等基础设施。
**Avalonia Button 提供的基础属性:**
| 属性 | 说明 |
|---|---|
| `Content` | 按钮内容,可以是文本字符串,也可以是任意控件 |
| `Command` / `CommandParameter` | MVVM 命令绑定,按钮点击时调用 `ICommand.Execute`;当 `CanExecute` 返回 `false` 时自动禁用按钮 |
| `ClickMode` | 控制 `Click` 事件的触发时机:`Release`(指针释放时,默认)或 `Press`(指针按下时) |
| `HotKey` | 键盘快捷键(`KeyGesture`),注册后按下对应组合键等同于点击按钮 |
| `IsDefault` | 设为 `true` 时,用户按 **Enter** 键等同于点击该按钮(常用于对话框「确认」按钮) |
| `IsCancel` | 设为 `true` 时,用户按 **Escape** 键等同于点击该按钮(常用于对话框「取消」按钮) |
| `IsPressed` | 只读属性,指示按钮当前是否处于按压状态 |
| `Flyout` | 附加弹出层,点击按钮时自动展开 `Flyout` / `MenuFlyout` |
**Avalonia Button 提供的基础伪类:**
| 伪类 | 说明 |
|---|---|
| `:pointerover` | 鼠标悬浮在按钮上 |
| `:pressed` | 按钮被按下 |
| `:disabled` | `IsEnabled == false` |
| `:focus` | 按钮获得焦点 |
| `:focus-visible` | 通过键盘(Tab)获得焦点 |
| `:flyout-open` | 附加的 Flyout 处于打开状态 |
> **重要提示**:应始终使用 `Click` 事件而非 `PointerPressed` 来处理按钮点击。`Click` 是 Button 专属的高级语义事件;而 `PointerPressed` 是底层输入事件,Button 内部会将其标记为 `IsHandled = true`,外部通常无法接收到。
### AtomUI 的扩展设计
AtomUI `Button` 在 Avalonia Button 的基础上,遵循 Ant Design 5.0 规范做了以下扩展:
| 扩展能力 | 实现方式 | 设计动机 |
|---|---|---|
| **五种按钮类型** | `ButtonType` 枚举 + 伪类驱动样式 | 对齐 Ant Design 的 `type` 属性,支持 Primary / Default / Dashed / Text / Link |
| **三种按钮形状** | `ButtonShape` 枚举 + `MeasureOverride` 动态计算 | 对齐 Ant Design 的 `shape`,在布局阶段自动调整尺寸和圆角 |
| **三种尺寸** | `ISizeTypeAware` 接口 + `SizeType` 共享属性 | 全局统一的 Large / Middle / Small 尺寸系统 |
| **危险/幽灵状态** | `IsDanger` / `IsGhost` 属性 + 主题样式变体 | 通过 Token 系统自动切换颜色方案 |
| **加载状态** | `IsLoading` 属性 + 模板内 `LoadingOutlined` 图标 | 旋转加载图标 + 降低不透明度,防止重复操作 |
| **图标支持** | `Icon` 属性(`PathIcon` 类型)+ `IconPresenter` | 内置图标栏位,无需手动组装 StackPanel + Icon |
| **点击波纹** | `IWaveSpiritAwareControl` + `WaveSpiritDecorator` | 复刻 Ant Design 的 Wave 点击涟漪效果 |
| **过渡动画** | `IsMotionEnabled` + `Transitions` 动态配置 | 背景色、前景色、边框色平滑过渡 |
| **紧凑空间** | `ICompactSpaceAware` 接口 | 在 `Space.Compact` 容器中自动调整圆角和边框 |
| **表单集成** | `IFormItemAware` 接口 | 可参与 FormItem 验证流程 |
| **Design Token** | `ButtonToken` + `RegisterTokenResourceScope` | 所有视觉值从全局/组件级 Token 派生,支持主题切换 |
---
## 功能详解
### 按钮类型(ButtonType)
按钮类型通过 `ButtonType` 属性设置,每种类型对应不同的伪类(`:primary`、`:default`、`:dashed`、`:text`、`:link`),ControlTheme 通过这些伪类应用不同的视觉样式。
不同类型的模板差异:
- **Default / Primary / Link / Text**:使用标准 `Border` 作为主框架。
- **Dashed**:使用自定义 `DashedBorder` 控件绘制虚线边框,通过 `StrokeDashArray = [4, 2]` 实现虚线效果。
不同类型的阴影差异:
- **Default / Dashed**:有 `DefaultShadow` 底部微阴影,增强立体感。
- **Primary**:有 `PrimaryShadow` 底部微阴影。
- **Text / Link**:无阴影,保持轻量感。
不同类型的波纹差异:
- **Primary / Default / Dashed**:按下释放时触发 WaveSpirit 波纹动画。
- **Text / Link**:不触发波纹(这两种类型视觉权重低,波纹会造成过度干扰)。
### 按钮形状(ButtonShape)
形状通过 `MeasureOverride` 在布局阶段动态计算:
| 形状 | 布局行为 | 圆角计算 |
|---|---|---|
| `Default` | 宽度 = `max(内容宽度, 高度)`,保证最小正方形 | 由 Token 中 `BorderRadius` / `BorderRadiusSM` / `BorderRadiusLG` 控制 |
| `Circle` | 强制 `宽度 = 高度`,呈正圆形 | `CornerRadius = 高度`(完全圆角) |
| `Round` | 宽度 = `max(内容宽度, 高度 × 1.5)`,呈胶囊形 | `CornerRadius = 高度`(两端完全圆角) |
形状还影响波纹类型:`Default` → `RoundRectWave`,`Round` → `PillWave`,`Circle` → `CircleWave`。
### 加载状态(IsLoading)
当 `IsLoading = true` 时:
1. `:loading` 伪类被激活
2. 按钮图标(`PART_ButtonIcon`)隐藏,替换为旋转的 `LoadingOutlined` 图标
3. 整体不透明度降至 `SharedToken.OpacityLoading`
4. 波纹效果被禁止(`IsPressed` 变化时检查 `IsLoading`)
### 过渡动画(Transitions)
动画在 `OnLoaded` 时配置,`OnUnloaded` 时清除(避免不可见控件消耗资源)。不同 `ButtonType` 配置不同的过渡属性组合:
| ButtonType | 过渡属性 |
|---|---|
| Primary(非 Ghost) | `Background` |
| Primary(Ghost) | `Background` + `BorderBrush` + `Foreground` |
| Default / Dashed | `Background` + `BorderBrush` + `Foreground` |
| Link | `Background` + `Foreground` |
| Text | `Background` |
### 紧凑空间(Compact Space)
当 Button 放置在 `Space.Compact` 容器中时:
- 容器通知每个按钮其 `CompactSpaceItemPosition`(First / Middle / Last / OnlyOne)和 `CompactSpaceOrientation`(Horizontal / Vertical)
- Button 根据位置自动裁剪圆角(例如水平排列的中间按钮圆角为 0)
- Primary 类型按钮始终处于活跃 ZIndex 层,确保边框不被相邻按钮遮挡
---
## 与 Ant Design 规范的对齐程度
| 特性 | Ant Design (React) | AtomUI (Avalonia) | 对齐情况 |
|---|---|---|---|
| 五种类型 `type` | ✅ `primary / default / dashed / text / link` | ✅ `ButtonType` 枚举 | ✅ 完全对齐 |
| 三种形状 `shape` | ✅ `default / circle / round` | ✅ `ButtonShape` 枚举 | ✅ 完全对齐 |
| 三种尺寸 `size` | ✅ `large / medium / small` | ✅ `SizeType` 属性 | ✅ 完全对齐 |
| 危险 `danger` | ✅ 布尔属性 | ✅ `IsDanger` 属性 | ✅ 完全对齐 |
| 幽灵 `ghost` | ✅ 布尔属性 | ✅ `IsGhost` 属性 | ✅ 完全对齐 |
| 加载中 `loading` | ✅ 布尔/对象 | ✅ `IsLoading` 布尔 | ⚠️ 不支持延迟加载(`{ delay }` 语法) |
| 图标 `icon` | ✅ ReactNode | ✅ `PathIcon` 属性 | ✅ 对齐(类型不同,语义一致) |
| Block 模式 | ✅ `block` 属性 | ✅ `HorizontalAlignment="Stretch"` | ✅ 通过 Avalonia 原生布局实现 |
| 点击波纹 | ✅ Wave 组件 | ✅ `WaveSpiritDecorator` | ✅ 完全对齐 |
| 颜色+变体 `color` / `variant` | ✅ 5.21.0 新增 | ❌ 暂未支持 | ⚠️ 通过 `ButtonType` 覆盖主要场景 |
| 图标位置 `iconPlacement` | ✅ `start / end` | ❌ 暂仅支持左侧 | ⚠️ 待支持 |
| `href` / `target` | ✅ 链接跳转 | ❌ 不适用 | — 桌面端无需 HTML 超链接语义 |
| `htmlType` | ✅ 原生表单提交 | ❌ 不适用 | — 非 Web 平台 |
---
## 继承关系
```
Avalonia.Controls.Primitives.TemplatedControl
└── Avalonia.Controls.ContentControl
└── Avalonia.Controls.Button (ICommandSource, IClickableControl)
└── AtomUI.Desktop.Controls.Button
├── implements ISizeTypeAware
├── implements IWaveSpiritAwareControl
├── implements ICompactSpaceAware
└── implements IFormItemAware
```
`Button` 通过 `using AvaloniaButton = Avalonia.Controls.Button;` 别名引用 Avalonia 原生 Button,避免类名冲突。
**各层级职责划分:**
| 层级 | 提供的能力 |
|---|---|
| `ContentControl` | 任意内容容纳、`Content` / `ContentTemplate`、`ContentPresenter` 模板化呈现 |
| `Avalonia.Controls.Button` | 指针交互 → Click 事件、`ICommand` 绑定、`IsPressed` 状态、`HotKey` 快捷键、`Flyout` 弹出、`IsDefault` / `IsCancel` 键盘激活、无障碍支持 |
| `AtomUI.Desktop.Controls.Button` | Ant Design 视觉体系(五种类型/三种形状/三种尺寸)、Design Token 集成、WaveSpirit 波纹、加载状态、危险/幽灵样式、紧凑空间适配、表单集成 |
**实现的共享接口:**
| 接口 | 定义位置 | 作用 |
|---|---|---|
| `ISizeTypeAware` | `AtomUI.Controls.Shared` | 支持 `SizeType`(Small / Middle / Large)三种尺寸切换 |
| `IWaveSpiritAwareControl` | `AtomUI.Controls.Shared` | 支持点击涟漪(Wave)动画效果 |
| `ICompactSpaceAware` | `AtomUI.Controls.Shared` | 在 `Space.Compact` 中使用时自动调整圆角和边框 |
| `IFormItemAware` | `AtomUI.Controls.Shared` | 可作为 `FormItem` 的子控件参与表单验证 |
---
## 源码位置
| 层级 | 文件路径 | 说明 |
|---|---|---|
| 控件类 | `src/AtomUI.Desktop.Controls/Buttons/Button.cs` | 桌面端 Button 具体实现 |
| 伪类常量 | `src/AtomUI.Controls/Buttons/ButtonPseudoClass.cs` | 共享伪类定义(跨平台) |
| Token 定义 | `src/AtomUI.Desktop.Controls/Buttons/ButtonToken.cs` | 组件级 Design Token |
| 主题模板 | `src/AtomUI.Desktop.Controls/Buttons/Themes/ButtonTheme.axaml` | ControlTheme AXAML |
| 主题代码 | `src/AtomUI.Desktop.Controls/Buttons/Themes/ButtonTheme.cs` | 主题 code-behind |
| 模板常量 | `src/AtomUI.Desktop.Controls/Buttons/Themes/ButtonThemeConstants.cs` | 模板部件名称常量 |
| Gallery 示例 | `controlgallery/AtomUIGallery/ShowCases/Views/General/ButtonShowCase.axaml` | 使用范例 |
---
## 模板结构
Button 的 ControlTemplate 采用分层 Panel 布局,每一层各司其职:
```
Panel
├── WaveSpiritDecorator (PART_WaveSpirit) ← 点击波纹效果层(最底层,不影响内容)
├── Border#ShadowsFrame ← 阴影层(独立于主框架,避免 BoxShadow 被裁剪)
├── Border#Frame (或 DashedBorder#Frame) ← 主框架(承载背景、边框和全部内容)
│ └── DockPanel (PART_RootLayout) ← 内容布局容器
│ ├── LoadingOutlined (PART_LoadingIcon) ← 加载旋转图标(DockPanel.Dock="Left")
│ ├── IconPresenter (PART_ButtonIcon) ← 按钮图标展示器(DockPanel.Dock="Left")
│ └── ContentPresenter (PART_ContentPresenter) ← 文本/自定义内容(填充剩余空间)
└── Border#FocusVisual ← 焦点指示器(最顶层,仅键盘焦点时可见,ZIndex=1)
```
**分层设计理由:**
- **波纹层独立**:`WaveSpiritDecorator` 位于最底层,波纹动画在按钮边界外扩散时不会被主框架裁剪。
- **阴影层独立**:`BoxShadow` 放在单独的 `Border#ShadowsFrame` 上,避免阴影与虚线边框(Dashed 类型)冲突。
- **焦点层独立**:`Border#FocusVisual` 使用 `ZIndex=1` 和负 `Margin` 绘制在按钮外围,仅在 `:focus-visible` 时显示。
> 注意:当 `ButtonType=Dashed` 时,主框架使用 `DashedBorder` 代替标准 `Border`,通过 `StrokeDashArray = [4, 2]` 绘制虚线边框效果。
### 模板部件常量
| 常量名 | 值 | 说明 |
|---|---|---|
| `ButtonThemeConstants.WaveSpiritPart` | `"PART_WaveSpirit"` | 波纹动画装饰器 |
| `ButtonThemeConstants.RootLayoutPart` | `"PART_RootLayout"` | 根布局面板 |
| `ButtonThemeConstants.LoadingIconPart` | `"PART_LoadingIcon"` | 加载旋转图标 |
| `ButtonThemeConstants.ButtonIconPart` | `"PART_ButtonIcon"` | 按钮图标展示器 |
| `ButtonThemeConstants.ContentPresenterPart` | `"PART_ContentPresenter"` | 内容展示器 |