12 KiB
AtomUI WaveSpirit 系统 — 开发者指南
本文档面向 AtomUI 控件开发者,介绍如何在自定义控件中集成 WaveSpirit 波浪动画效果。
1. 集成概述
为控件添加 WaveSpirit 波浪动画效果,需要在三个层级协作完成:
| 步骤 | 层级 | 操作 |
|---|---|---|
| 声明接口 | 控件 C# 代码 | 实现 IWaveSpiritAwareControl,注册共享属性 |
| 放置装饰器 | 控件主题 AXAML | 在模板中放置 WaveSpiritDecorator |
| 绑定 Token | 控件主题 AXAML | 设置 IsWaveSpiritEnabled、WaveType 等属性 |
| 触发动画 | 控件 C# 代码 | 在交互事件中调用 _waveSpiritDecorator.Play() |
2. 完整集成步骤
以下以一个假设的 MyButton 控件为例,演示完整集成流程。
2.1 步骤一:实现接口并注册属性
using AtomUI.Controls;
using AtomUI.Controls.Primitives;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using Avalonia.Threading;
namespace AtomUI.Desktop.Controls;
public class MyButton : Button, IWaveSpiritAwareControl
{
// 使用 AddOwner 注册共享属性
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<MyButton>();
public static readonly StyledProperty<bool> IsWaveSpiritEnabledProperty =
WaveSpiritAwareControlProperty.IsWaveSpiritEnabledProperty.AddOwner<MyButton>();
internal static readonly StyledProperty<WaveSpiritType> WaveSpiritTypeProperty =
WaveSpiritAwareControlProperty.WaveSpiritTypeProperty.AddOwner<MyButton>();
// 属性包装器
public bool IsMotionEnabled
{
get => GetValue(IsMotionEnabledProperty);
set => SetValue(IsMotionEnabledProperty, value);
}
public bool IsWaveSpiritEnabled
{
get => GetValue(IsWaveSpiritEnabledProperty);
set => SetValue(IsWaveSpiritEnabledProperty, value);
}
internal WaveSpiritType WaveSpiritType
{
get => GetValue(WaveSpiritTypeProperty);
set => SetValue(WaveSpiritTypeProperty, value);
}
// 持有 WaveSpiritDecorator 引用
private WaveSpiritDecorator? _waveSpiritDecorator;
// 在 OnApplyTemplate 中获取装饰器引用
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_waveSpiritDecorator = e.NameScope.Find<WaveSpiritDecorator>("PART_WaveSpirit");
}
// 在交互事件中触发波浪动画
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsPressedProperty)
{
if (IsWaveSpiritEnabled &&
(change.OldValue as bool? == true))
{
Dispatcher.UIThread.Post(() =>
{
_waveSpiritDecorator?.Play();
});
}
}
}
}
关键要点:
- 使用
AddOwner<T>()复用WaveSpiritAwareControlProperty中定义的共享属性,不要自行注册新的同名属性。 WaveSpiritType属性通常为internal,因为波纹类型由控件内部逻辑或主题决定,不暴露给用户。- 触发时机使用
Dispatcher.UIThread.Post()延迟一帧,确保 UI 状态已完全更新。
2.2 步骤二:AXAML 模板中放置装饰器
<ControlTheme xmlns="https://github.com/avaloniaui"
xmlns:atom="https://atomui.net"
x:Class="AtomUI.Desktop.Controls.Themes.MyButtonTheme"
TargetType="atom:MyButton">
<Style Selector="^:is(atom|MyButton)">
<Setter Property="Template">
<ControlTemplate>
<Panel>
<!-- 波浪装饰器放在最底层 -->
<atom:WaveSpiritDecorator
Name="PART_WaveSpirit"
CornerRadius="{TemplateBinding CornerRadius}"
WaveType="{TemplateBinding WaveSpiritType}" />
<!-- 控件主体内容 -->
<Border Name="Frame"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter Content="{TemplateBinding Content}" />
</Border>
</Panel>
</ControlTemplate>
</Setter>
<!-- 绑定全局 Token -->
<Setter Property="IsMotionEnabled"
Value="{atom:SharedTokenResource EnableMotion}" />
<Setter Property="IsWaveSpiritEnabled"
Value="{atom:SharedTokenResource EnableWaveSpirit}" />
<Setter Property="ClipToBounds" Value="False" />
</Style>
</ControlTheme>
关键要点:
WaveSpiritDecorator必须放在Panel的第一个子元素(最底层 z-order),波纹在控件内容下方绘制。- 使用
{TemplateBinding CornerRadius}将控件圆角同步给装饰器。 - 必须设置
ClipToBounds="False",否则波纹超出边界的部分会被裁剪。 - 通过
{atom:SharedTokenResource EnableWaveSpirit}绑定全局 Token。
2.3 步骤三:按需覆盖 WaveType
通过 Style Selector 设置固定类型:
<Style Selector="^ /template/ atom|WaveSpiritDecorator#PART_WaveSpirit">
<Setter Property="WaveType" Value="RoundRectWave" />
</Style>
或在代码中根据外观动态设置:
private void ConfigureWaveSpiritType()
{
WaveSpiritType waveType = Shape switch
{
ButtonShape.Default => WaveSpiritType.RoundRectWave,
ButtonShape.Round => WaveSpiritType.PillWave,
ButtonShape.Circle => WaveSpiritType.CircleWave,
_ => WaveSpiritType.RoundRectWave
};
WaveSpiritType = waveType;
}
2.4 步骤四(可选):动态覆盖 WaveBrush
默认 WaveBrush 由全局主题绑定为 ColorPrimary。某些场景需覆盖:
if (IsDanger)
{
IBrush? waveBrush = ButtonType == ButtonType.Primary && !IsGhost
? Background : Foreground;
_waveSpiritDecorator.WaveBrush = waveBrush;
}
_waveSpiritDecorator?.Play();
3. 现有控件实现示例
3.1 Button(按钮)
波纹触发时机:按钮释放时(IsPressed 从 true 变为 false)。
波纹类型选择:根据 Shape 属性动态决定。
| Button Shape | WaveSpiritType |
|---|---|
Default |
RoundRectWave |
Round |
PillWave |
Circle |
CircleWave |
触发条件:IsWaveSpiritEnabled && !IsLoading && ButtonType in {Primary, Default, Dashed}
AXAML 模板(ButtonTheme.axaml):
<Panel>
<atom:WaveSpiritDecorator
Name="{x:Static atom:ButtonThemeConstants.WaveSpiritPart}"
CornerRadius="{TemplateBinding EffectiveCornerRadius}"
WaveType="{TemplateBinding WaveSpiritType}" />
<Border Name="Frame" ... />
</Panel>
3.2 CheckBox(复选框)
波纹触发时机:State 变为 Checked 时。
波纹类型:RoundRectWave(固定)。
触发条件:IsWaveSpiritEnabled && IsEnabled && IsMotionEnabled && State == Checked
<Style Selector="^ /template/ atom|WaveSpiritDecorator#PART_WaveSpirit">
<Setter Property="WaveType" Value="RoundRectWave" />
</Style>
3.3 RadioButton(单选按钮)
波纹触发时机:IsChecked 变为 true 时。
波纹类型:CircleWave(固定,因为单选按钮是圆形)。
<Style Selector="^ /template/ atom|WaveSpiritDecorator#PART_WaveSpirit">
<Setter Property="WaveType" Value="CircleWave" />
</Style>
3.4 ToggleSwitch(开关)
波纹触发时机:IsChecked 任意变化时。
波纹类型:PillWave(固定,开关为药丸形)。
特殊处理:需要手动为装饰器设置布局区域:
_waveSpiritDecorator?.Arrange(new Rect(new Point(0, 0), DesiredSize.Deflate(Margin)));
3.5 OptionButton(选项按钮)
波纹触发时机:按钮释放时(IsPressed 从 true 变为 false)。
波纹类型:RoundRectWave(固定)。
4. WaveSpiritDecorator 的 PART 命名约定
internal class WaveSpiritDecorator : Control
{
public const string WaveSpiritPart = "PART_WaveSpirit";
}
在 AXAML 中可以通过两种方式引用:
<!-- 方式一:使用 WaveSpiritDecorator 的常量 -->
<atom:WaveSpiritDecorator
Name="{x:Static atom:WaveSpiritDecorator.WaveSpiritPart}" />
<!-- 方式二:使用控件自己的 ThemeConstants -->
<atom:WaveSpiritDecorator
Name="{x:Static atom:ButtonThemeConstants.WaveSpiritPart}" />
5. 全局开关控制
5.1 通过 AXAML 绑定 Token(推荐)
<Setter Property="IsWaveSpiritEnabled"
Value="{atom:SharedTokenResource EnableWaveSpirit}" />
5.2 通过扩展方法绑定(代码方式)
public MyControl()
{
this.ConfigureWaveSpiritBindingStyle();
}
5.3 运行时切换
themeManager.IsWaveSpiritEnabled = false;
6. 注意事项与最佳实践
6.1 必须设置 ClipToBounds 为 False
<Setter Property="ClipToBounds" Value="False" />
6.2 WaveSpiritDecorator 必须放在 Panel 中
<!-- 正确 -->
<Panel>
<atom:WaveSpiritDecorator ... />
<Border ... />
</Panel>
6.3 CornerRadius 同步
RoundRectWave 类型必须同步控件圆角:
<atom:WaveSpiritDecorator CornerRadius="{TemplateBinding CornerRadius}" />
CircleWave 和 PillWave 不需要设置 CornerRadius。
6.4 动画触发时机
- 按钮类:鼠标释放时触发(
IsPressedtrue 到 false) - 选择类(CheckBox、RadioButton):选中状态变化时触发
- 开关类(ToggleSwitch):
IsChecked任意变化时触发
6.5 不要硬编码 Token 值
波纹参数已通过 WaveSpiritDecoratorTheme.axaml 绑定 Token。如需覆盖,用 Style Selector 设置。唯一例外是 WaveBrush 的动态覆盖(如 Danger 按钮的红色波纹)。
6.6 使用 Dispatcher.UIThread.Post 延迟触发
Dispatcher.UIThread.Post(() => { _waveSpiritDecorator?.Play(); });
6.7 WaveSpiritDecorator 的 Bounds 自适应
装饰器自动监听 Bounds 变化来更新尺寸。对于自定义布局控件(如 ToggleSwitch),需在 ArrangeOverride 中手动设置:
_waveSpiritDecorator?.Arrange(new Rect(new Point(0, 0), DesiredSize.Deflate(Margin)));
7. 速查清单
集成 WaveSpirit 时,请确认以下各项:
- 控件类实现了
IWaveSpiritAwareControl接口 - 使用
AddOwner<T>()注册了IsMotionEnabledProperty和IsWaveSpiritEnabledProperty - 使用
AddOwner<T>()注册了WaveSpiritTypeProperty(如果需要动态切换类型) - 在
OnApplyTemplate中通过NameScope.Find<WaveSpiritDecorator>(...)获取装饰器引用 - AXAML 模板使用
Panel包裹WaveSpiritDecorator和控件主体 WaveSpiritDecorator放在Panel的第一个子元素位置WaveSpiritDecorator的Name使用标准的"PART_WaveSpirit"- 设置了
CornerRadius="{TemplateBinding CornerRadius}"(对 RoundRectWave 类型) - 设置了
WaveType(通过{TemplateBinding}或 Style Selector) - 控件设置了
ClipToBounds="False" - 主题中绑定了
IsWaveSpiritEnabled到{atom:SharedTokenResource EnableWaveSpirit} - 主题中绑定了
IsMotionEnabled到{atom:SharedTokenResource EnableMotion} - 在合适的交互事件中调用了
_waveSpiritDecorator?.Play() - 触发前检查了
IsWaveSpiritEnabled条件