11 KiB
自定义控件接入动画与涟漪效果开发指南
1. 概述
本文档面向控件开发者,详细说明如何让自定义控件支持 AtomUI 的动画控制系统(IMotionAwareControl)和点击涟漪效果系统(IWaveSpiritAwareControl),使其与 AtomUI 内置控件表现一致。
前置阅读:动画与涟漪效果控制系统设计原理
相关文档:全局动画与涟漪效果开关使用指南
2. 快速判断:你的控件需要实现哪个接口?
你的控件有动画效果吗?(过渡动画、展开/折叠、渐变等)
│
├── 是 → 实现 IMotionAwareControl
│ │
│ └── 控件可点击且需要涟漪反馈?(按钮、复选框、开关等)
│ │
│ ├── 是 → 改为实现 IWaveSpiritAwareControl(已包含 IMotionAwareControl)
│ └── 否 → 保持 IMotionAwareControl 即可
│
└── 否 → 不需要实现任何动画接口
3. 实现 IMotionAwareControl(仅动画控制)
3.1 属性注册
using AtomUI.Controls;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls.Primitives;
namespace MyApp.Controls;
public class MyAnimatedControl : TemplatedControl, IMotionAwareControl
{
// ===== 使用 AddOwner 注册共享属性 =====
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<MyAnimatedControl>();
public bool IsMotionEnabled
{
get => GetValue(IsMotionEnabledProperty);
set => SetValue(IsMotionEnabledProperty, value);
}
}
3.2 绑定全局 Token
在控件构造函数中调用扩展方法,将 IsMotionEnabled 绑定到全局 EnableMotion Token:
public MyAnimatedControl()
{
// 自动绑定 IsMotionEnabled ← SharedToken.EnableMotion
this.ConfigureMotionBindingStyle();
}
3.3 根据 IsMotionEnabled 管理过渡动画
在控件中根据 IsMotionEnabled 动态启用/禁用过渡动画:
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
ConfigureTransitions(false);
}
protected override void OnUnloaded(RoutedEventArgs e)
{
base.OnUnloaded(e);
Transitions = null;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (IsLoaded && change.Property == IsMotionEnabledProperty)
{
ConfigureTransitions(true);
}
}
private void ConfigureTransitions(bool force)
{
if (IsMotionEnabled)
{
if (force || Transitions == null)
{
Transitions = [
TransitionUtils.CreateTransition<SolidColorBrushTransition>(BackgroundProperty),
TransitionUtils.CreateTransition<SolidColorBrushTransition>(BorderBrushProperty)
];
}
}
else
{
Transitions = null; // ← 关闭动画时移除所有过渡
}
}
关键模式:
OnLoaded中首次配置过渡(force = false,仅在 Transitions 为 null 时设置)OnUnloaded中清除过渡(避免脱离视觉树后仍有动画资源)IsMotionEnabledProperty变化时强制重新配置(force = true)IsMotionEnabled == false时,设置Transitions = null完全禁用过渡
4. 实现 IWaveSpiritAwareControl(动画 + 涟漪)
4.1 属性注册
using AtomUI.Controls;
using AtomUI.Controls.Primitives;
using Avalonia;
using Avalonia.Controls.Primitives;
namespace MyApp.Controls;
public class MyClickableControl : TemplatedControl, IWaveSpiritAwareControl
{
// ===== IMotionAwareControl 属性(继承自接口) =====
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<MyClickableControl>();
// ===== IWaveSpiritAwareControl 属性 =====
public static readonly StyledProperty<bool> IsWaveSpiritEnabledProperty =
WaveSpiritAwareControlProperty.IsWaveSpiritEnabledProperty.AddOwner<MyClickableControl>();
public bool IsMotionEnabled
{
get => GetValue(IsMotionEnabledProperty);
set => SetValue(IsMotionEnabledProperty, value);
}
public bool IsWaveSpiritEnabled
{
get => GetValue(IsWaveSpiritEnabledProperty);
set => SetValue(IsWaveSpiritEnabledProperty, value);
}
}
4.2 绑定全局 Token
使用 ConfigureWaveSpiritBindingStyle(同时绑定 EnableMotion 和 EnableWaveSpirit):
public MyClickableControl()
{
// 同时绑定 IsMotionEnabled ← EnableMotion 和 IsWaveSpiritEnabled ← EnableWaveSpirit
this.ConfigureWaveSpiritBindingStyle();
}
⚠️ 只需调用
ConfigureWaveSpiritBindingStyle,无需额外调用ConfigureMotionBindingStyle,因为前者已包含后者的绑定。
4.3 在 AXAML 模板中放置 WaveSpiritDecorator
<ControlTheme x:Key="{x:Type local:MyClickableControl}" TargetType="local:MyClickableControl">
<Setter Property="Template">
<ControlTemplate>
<Panel>
<!-- 涟漪装饰器必须放在最底层 -->
<atom:WaveSpiritDecorator Name="PART_WaveSpirit"
CornerRadius="{TemplateBinding CornerRadius}"
WaveType="RoundRectWave" />
<!-- 控件主体内容在涟漪之上 -->
<Border Name="Frame"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<ContentPresenter Content="{TemplateBinding Content}" />
</Border>
</Panel>
</ControlTemplate>
</Setter>
<Setter Property="ClipToBounds" Value="False" />
</ControlTheme>
要点:
WaveSpiritDecorator必须放在Panel的第一个子元素位置(最底层),涟漪在控件内容之下扩散Name必须为"PART_WaveSpirit"(使用WaveSpiritDecorator.WaveSpiritPart常量)CornerRadius绑定到控件的圆角,确保涟漪形状与控件匹配WaveType设置涟漪形状(RoundRectWave/CircleWave/PillWave)- 控件的
ClipToBounds必须为False,否则涟漪向外扩散的部分会被裁剪
4.4 在代码中获取并触发涟漪
private WaveSpiritDecorator? _waveSpiritDecorator;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_waveSpiritDecorator = e.NameScope.Find<WaveSpiritDecorator>("PART_WaveSpirit");
}
// 在用户交互时触发涟漪
private void OnClicked()
{
if (IsWaveSpiritEnabled)
{
Dispatcher.UIThread.Post(() =>
{
_waveSpiritDecorator?.Play();
});
}
}
触发时机的最佳实践:
| 控件类型 | 触发时机 | 示例 |
|---|---|---|
| 按钮 | IsPressed 从 true 变为 false(释放时) |
Button |
| 复选框/开关 | IsChecked 变化时 |
CheckBox、ToggleSwitch |
| 单选按钮 | 选中时 | RadioButton |
4.5 动态切换涟漪形状
如果控件形状可变(如 Button 支持 Default/Round/Circle),需要在形状变化时更新涟漪类型:
// 注册 WaveSpiritType 属性
internal static readonly StyledProperty<WaveSpiritType> WaveSpiritTypeProperty =
WaveSpiritAwareControlProperty.WaveSpiritTypeProperty.AddOwner<MyClickableControl>();
// 在形状变化时更新
private void ConfigureWaveSpiritType()
{
WaveSpiritType = Shape switch
{
MyShape.Default => WaveSpiritType.RoundRectWave,
MyShape.Round => WaveSpiritType.PillWave,
MyShape.Circle => WaveSpiritType.CircleWave,
_ => WaveSpiritType.RoundRectWave
};
}
并在模板中使用 {TemplateBinding WaveSpiritType} 动态绑定。
4.6 自定义涟漪颜色
默认情况下,涟漪颜色为 ColorPrimary(主色)。某些场景需要自定义涟漪颜色(如 Danger 按钮使用红色涟漪):
// 在触发涟漪前设置自定义颜色
if (IsDanger)
{
_waveSpiritDecorator.WaveBrush = Foreground; // 或 Background
}
Dispatcher.UIThread.Post(() => _waveSpiritDecorator?.Play());
5. 在 Form 中使用
5.1 IsMotionEnabled 自动级联
当控件放入 Form 中时,如果实现了 IMotionAwareControl,FormItem 会自动将 IsMotionEnabled 从 Form 级联到控件:
<atom:Form IsMotionEnabled="False">
<!-- 内部所有实现 IMotionAwareControl 的控件自动禁用动画 -->
<atom:FormItem LabelText="Name" FieldName="name">
<atom:LineEdit /> <!-- IsMotionEnabled 自动为 False -->
</atom:FormItem>
</atom:Form>
5.2 IsWaveSpiritEnabled 不通过 Form 级联
涟漪效果由全局 Token 直接控制,不参与 Form 级联。在 Form 中放置按钮时,按钮的涟漪效果始终由全局 EnableWaveSpirit Token 决定。
6. 实现检查清单
IMotionAwareControl 接入
- 实现
IMotionAwareControl接口 - 使用
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<T>()注册属性 - 在构造函数中调用
this.ConfigureMotionBindingStyle() - 在
OnLoaded中初始化 Transitions - 在
OnUnloaded中清除 Transitions - 在
IsMotionEnabledProperty变化时重新配置 Transitions IsMotionEnabled == false时设置Transitions = null
IWaveSpiritAwareControl 接入
- 实现
IWaveSpiritAwareControl接口(自动包含IMotionAwareControl) - 使用
AddOwner<T>()注册IsMotionEnabledProperty和IsWaveSpiritEnabledProperty - 在构造函数中调用
this.ConfigureWaveSpiritBindingStyle()(不要额外调用ConfigureMotionBindingStyle) - 在 AXAML 模板中放置
<atom:WaveSpiritDecorator Name="PART_WaveSpirit" /> WaveSpiritDecorator放在 Panel 的最底层- 控件
ClipToBounds设为False - 在
OnApplyTemplate中获取_waveSpiritDecorator引用 - 在用户交互时判断
IsWaveSpiritEnabled后调用_waveSpiritDecorator?.Play() - 使用
Dispatcher.UIThread.Post包裹Play()调用
常见错误
| 错误 | 后果 | 解决方案 |
|---|---|---|
使用 Register 而非 AddOwner |
Form 级联绑定失败 | 使用 AddOwner<T>() |
未调用 ConfigureMotionBindingStyle / ConfigureWaveSpiritBindingStyle |
控件不响应全局动画开关 | 在构造函数中调用 |
IWaveSpiritAwareControl 控件同时调用两个扩展方法 |
IsMotionEnabled 被绑定两次 |
只调用 ConfigureWaveSpiritBindingStyle |
WaveSpiritDecorator 不在 Panel 最底层 |
涟漪遮挡控件内容 | 放在 Panel 的第一个子元素位置 |
ClipToBounds 为 True |
涟漪扩散被裁剪 | 设为 False |
OnUnloaded 中未清除 Transitions |
脱离视觉树后仍持有动画资源 | 设置 Transitions = null |
未判断 IsWaveSpiritEnabled 就调用 Play() |
全局关闭涟漪后仍触发 | 先判断 IsWaveSpiritEnabled |
未使用 Dispatcher.UIThread.Post |
可能在属性变更回调中触发动画导致异常 | 使用 Post 延迟到下一帧 |