Files

15 KiB
Raw Permalink Blame History

SplitButton 组合按钮

概述

组合按钮(SplitButton)是将一个主操作按钮和一个下拉菜单按钮合并在一起的复合控件。左侧是主按钮(Primary Button),点击执行主操作;右侧是下拉触发按钮(Secondary Button),点击或悬浮弹出菜单供用户选择更多操作。

SplitButton 对应 Ant Design 的 Dropdown.Button(带 type 属性的变体)。AtomUI 的 SplitButton 完整复刻了 Ant Design 5.0 Dropdown.Button 的设计规范,在 .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