13 KiB
全局动画与涟漪效果开关使用指南
1. 概述
AtomUI 提供应用级别的全局开关,用于统一控制所有控件的过渡动画(EnableMotion)和点击涟漪效果(EnableWaveSpirit)。开发者可在运行时随时切换这两个开关,所有已实现 IMotionAwareControl 或 IWaveSpiritAwareControl 接口的控件会自动响应。
前置阅读:
适用场景
| 场景 | 操作 |
|---|---|
无障碍模式(prefers-reduced-motion) |
关闭全局动画 |
| 低端设备性能优化 | 关闭全局动画 |
| 用户偏好设置 | 提供开关选项,运行时切换 |
| 仅关闭涟漪效果,保留过渡动画 | 单独关闭涟漪 |
2. Application 扩展方法(推荐方式)
AtomUI.Controls.Shared 提供了 ApplicationExtensions 类(命名空间 AtomUI.Controls),封装了对 ThemeManager 的访问,是应用层控制动画开关的推荐入口。
2.1 API 一览
// 命名空间:AtomUI.Controls(来自 AtomUI.Controls.Shared)
public static class ApplicationExtensions
{
// ===== 动画控制 =====
public static bool IsMotionEnabled(this Application application);
public static void SetMotionEnabled(this Application application, bool enabled);
// ===== 涟漪控制 =====
public static bool IsWaveSpiritEnabled(this Application application);
public static void SetWaveSpiritEnabled(this Application application, bool enabled);
// ===== 其他主题控制(参考) =====
public static void SetDarkThemeMode(this Application application, bool enabled);
public static void SetCompactThemeMode(this Application application, bool enabled);
public static void SetLanguageVariant(this Application application, LanguageVariant variant);
}
2.2 基本用法
using AtomUI.Controls;
using Avalonia;
// 获取当前应用实例
var application = Application.Current!;
// 关闭全局动画(过渡动画 + 涟漪效果全部禁用)
application.SetMotionEnabled(false);
// 重新开启全局动画
application.SetMotionEnabled(true);
// 仅关闭涟漪效果,保留过渡动画
application.SetWaveSpiritEnabled(false);
// 查询当前状态
bool motionOn = application.IsMotionEnabled();
bool waveOn = application.IsWaveSpiritEnabled();
注意:这些扩展方法内部通过
AvaloniaLocator获取ThemeManager实例。必须在UseAtomUI()初始化完成之后才能调用。
3. 两个开关的关系
EnableMotion 和 EnableWaveSpirit 是两个独立的 Seed Token,但在逻辑上存在层级关系:
EnableMotion = false → 所有过渡动画停止 + 涟漪效果停止
EnableMotion = true → 过渡动画恢复,涟漪是否播放取决于 EnableWaveSpirit
EnableWaveSpirit = false → 仅涟漪效果停止,过渡动画不受影响
EnableWaveSpirit = true → 涟漪效果恢复(前提:EnableMotion 也为 true)
这是因为 IWaveSpiritAwareControl 继承自 IMotionAwareControl,涟漪控件在触发 Play() 前会检查 IsWaveSpiritEnabled,而涟漪本身也依赖动画系统。因此在实践中:
| EnableMotion | EnableWaveSpirit | 过渡动画 | 涟漪效果 |
|---|---|---|---|
true |
true |
✅ 启用 | ✅ 启用 |
true |
false |
✅ 启用 | ❌ 禁用 |
false |
true |
❌ 禁用 | ❌ 禁用 |
false |
false |
❌ 禁用 | ❌ 禁用 |
最佳实践:关闭动画时同步关闭涟漪,开启动画时恢复涟漪。Gallery 中的
WorkspaceWindow实现了这一逻辑(见第 5 节)。
4. 内部传播机制
了解全局开关的传播链路有助于调试和排查问题。
4.1 传播链路
Application.SetMotionEnabled(false)
│
▼
ThemeManager.IsMotionEnabled = false ← AvaloniaProperty 变更
│
▼
ThemeManager.OnPropertyChanged()
→ ConfigureEnableMotion()
→ globalResourceDictionary[SharedTokenKind.EnableMotion] = false ← 修改全局 Token 资源
│
▼
所有通过 DynamicResource 绑定到 SharedTokenKind.EnableMotion 的控件自动获得新值
│
▼
控件的 IsMotionEnabledProperty 值变更
→ OnPropertyChanged() → ConfigureTransitions(true)
→ Transitions = null (禁用过渡动画)
4.2 ThemeManager 核心代码
// ThemeManager 中的属性变更处理
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
// ...
if (change.Property == IsMotionEnabledProperty)
{
ConfigureEnableMotion();
}
else if (change.Property == IsWaveSpiritEnabledProperty)
{
ConfigureEnableWaveSpirit();
}
}
// 将属性值写入全局 Token 资源字典
private void ConfigureEnableMotion()
{
var themeResource = Resources.ThemeDictionaries[ThemeVariant];
if (themeResource is ResourceDictionary globalResourceDictionary)
{
globalResourceDictionary[SharedTokenKind.EnableMotion] = IsMotionEnabled;
}
}
private void ConfigureEnableWaveSpirit()
{
var themeResource = Resources.ThemeDictionaries[ThemeVariant];
if (themeResource is ResourceDictionary globalResourceDictionary)
{
globalResourceDictionary[SharedTokenKind.EnableWaveSpirit] = IsWaveSpiritEnabled;
}
}
4.3 控件端的自动绑定
控件在构造函数中调用 ConfigureMotionBindingStyle() 或 ConfigureWaveSpiritBindingStyle(),内部通过 Style 将属性绑定到 SharedTokenKind.EnableMotion / SharedTokenKind.EnableWaveSpirit 动态资源。当 ThemeManager 修改全局资源字典中的值时,绑定链自动更新控件属性。
ThemeManager ──写入──► ResourceDictionary[SharedTokenKind.EnableMotion]
│
DynamicResource 绑定
│
▼
控件 IsMotionEnabledProperty ←── 自动更新
5. 完整示例:Gallery WorkspaceWindow 实现
AtomUI Gallery 的 WorkspaceWindow 通过菜单项提供了运行时切换动画和涟漪的完整示例。
5.1 AXAML 菜单定义
<atom:MenuItem Header="主题">
<!-- 动画开关 -->
<atom:MenuItem Header="开启动画"
ToggleType="CheckBox"
IsChecked="True"
Tag="{x:Static local:WindowMenuItemKind.Motion}"/>
<!-- 涟漪开关 -->
<atom:MenuItem Header="开启波浪动画"
ToggleType="CheckBox"
IsChecked="True"
Tag="{x:Static local:WindowMenuItemKind.WaveSpirit}"/>
</atom:MenuItem>
5.2 菜单事件处理
private void HandleMenuItemCheckChanged(object? sender, RoutedEventArgs e)
{
if (e.Source is MenuItem menuItem && menuItem.Tag is WindowMenuItemKind kind)
{
var application = Application.Current!;
if (kind == WindowMenuItemKind.Motion)
{
// 关闭动画时,同步关闭涟漪开关的 UI 状态
if (menuItem.Parent is MenuItem themeMenuItem)
{
foreach (var item in themeMenuItem.Items)
{
if (item is MenuItem child &&
child.Tag is WindowMenuItemKind childKind &&
childKind == WindowMenuItemKind.WaveSpirit)
{
if (!menuItem.IsChecked)
{
child.IsChecked = false;
}
}
}
}
// 调用全局 API
application.SetMotionEnabled(menuItem.IsChecked);
}
else if (kind == WindowMenuItemKind.WaveSpirit)
{
application.SetWaveSpiritEnabled(menuItem.IsChecked);
}
}
}
关键设计点:
- 使用
ToggleType="CheckBox"让菜单项具备切换能力 - 通过
Tag区分不同的菜单项类型 - 关闭动画(Motion)时,同步将涟漪(WaveSpirit)菜单项置为 unchecked——因为涟漪依赖动画系统
- 最终调用
application.SetMotionEnabled()/application.SetWaveSpiritEnabled()完成全局切换
6. 在应用启动时配置默认值
6.1 通过 Seed Token 默认值
EnableMotion 和 EnableWaveSpirit 在 DesignToken.Seed 中的默认值均为 true:
[DesignTokenKind(DesignTokenKind.Seed)]
public bool EnableMotion { get; set; } = true;
[DesignTokenKind(DesignTokenKind.Seed)]
public bool EnableWaveSpirit { get; set; } = true;
因此,应用启动后默认所有动画和涟漪效果均为开启状态。
6.2 在 OnFrameworkInitializationCompleted 中修改
如果需要在启动时默认关闭动画(例如针对特定设备),可在应用初始化完成后设置:
public override void OnFrameworkInitializationCompleted()
{
base.OnFrameworkInitializationCompleted();
// 在低端设备上默认关闭动画
if (IsLowEndDevice())
{
Application.Current!.SetMotionEnabled(false);
}
}
7. 高级场景
7.1 监听动画状态变化
如果需要在动画状态变化时执行自定义逻辑,可以通过 ThemeManager 属性监听:
var themeManager = Application.Current!.GetThemeManager();
if (themeManager is AvaloniaObject avaloniaObject)
{
avaloniaObject.GetObservable(ThemeManager.IsMotionEnabledProperty)
.Subscribe(isEnabled =>
{
// 动画状态变化时的自定义逻辑
Debug.WriteLine($"Motion enabled: {isEnabled}");
});
}
7.2 与暗色模式 / 紧凑模式的组合
动画和涟漪开关与主题模式完全独立,可以自由组合:
var app = Application.Current!;
// 暗色 + 紧凑 + 无动画
app.SetDarkThemeMode(true);
app.SetCompactThemeMode(true);
app.SetMotionEnabled(false);
切换暗色/紧凑模式不会影响动画开关状态,反之亦然。
7.3 主题切换时的行为
当通过 Application.RequestedThemeVariant 切换主题(如从 Default 切换到 Dark+Compact)时,ThemeManager 会重新应用当前的 IsMotionEnabled 和 IsWaveSpiritEnabled 值到新主题的资源字典中:
// ThemeManager.ConfigureThemeVariant() 中
if (oldTheme != null)
{
ConfigureEnableMotion(); // 将当前值写入新主题资源
ConfigureEnableWaveSpirit(); // 将当前值写入新主题资源
}
因此,动画开关状态在主题切换后保持不变。
8. API 参考
ApplicationExtensions(AtomUI.Controls 命名空间)
| 方法 | 说明 |
|---|---|
GetThemeManager(this Application) |
获取 IThemeManager 实例 |
IsMotionEnabled(this Application) |
查询全局动画是否开启 |
SetMotionEnabled(this Application, bool) |
设置全局动画开关 |
IsWaveSpiritEnabled(this Application) |
查询全局涟漪是否开启 |
SetWaveSpiritEnabled(this Application, bool) |
设置全局涟漪开关 |
IsDarkThemeMode(this Application) |
查询是否暗色模式 |
SetDarkThemeMode(this Application, bool) |
设置暗色模式 |
IsCompactThemeMode(this Application) |
查询是否紧凑模式 |
SetCompactThemeMode(this Application, bool) |
设置紧凑模式 |
IThemeManager 属性
| 属性 | 类型 | 说明 |
|---|---|---|
IsMotionEnabled |
bool |
全局动画开关 |
IsWaveSpiritEnabled |
bool |
全局涟漪开关 |
IsDarkThemeMode |
bool |
暗色模式开关 |
IsCompactThemeMode |
bool |
紧凑模式开关 |
Seed Token
| Token | 类型 | 默认值 | 说明 |
|---|---|---|---|
EnableMotion |
bool |
true |
全局动画开关 |
EnableWaveSpirit |
bool |
true |
全局涟漪开关 |
9. 常见问题
Q: 调用 SetMotionEnabled(false) 后部分控件动画仍然存在?
A: 检查该控件是否实现了 IMotionAwareControl 接口,以及是否在构造函数中调用了 ConfigureMotionBindingStyle() 或 ConfigureWaveSpiritBindingStyle()。只有完成了这些步骤的控件才会响应全局开关。
Q: 关闭动画后,涟漪效果是否也会关闭?
A: 是的。涟漪控件在触发 Play() 前会检查 IsWaveSpiritEnabled,而 IsWaveSpiritEnabled 的绑定链中不直接依赖 EnableMotion。但在实践中,涟漪控件内部的动画引擎(WaveSpiritDecorator 的尺寸和透明度动画)本身就是过渡动画,当控件的 IsMotionEnabled 为 false 时,涟漪的视觉效果自然不会播放。建议在关闭 EnableMotion 时同步设置 EnableWaveSpirit = false,保持状态一致(参考 Gallery 示例)。
Q: 可以在 AXAML 中直接控制单个控件的动画吗?
A: 可以。每个实现了 IMotionAwareControl 的控件都暴露了 IsMotionEnabled 属性,可以在 AXAML 中直接设置:
<!-- 禁用此按钮的过渡动画 -->
<atom:Button IsMotionEnabled="False" Content="No Animation" />
但请注意,直接设置会覆盖全局 Token 绑定。如果后续需要恢复全局控制,需要清除该属性的本地值。
Q: UseAtomUI() 之前可以调用 SetMotionEnabled() 吗?
A: 不可以。SetMotionEnabled() 内部通过 AvaloniaLocator 获取 ThemeManager,而 ThemeManager 在 UseAtomUI() 中才会创建并注册。在此之前调用会导致空引用。