27 KiB
AtomUI 动效系统开发者指南
本文档面向 AtomUI 框架开发者和控件库使用者,详细说明如何使用预置动效、创建自定义动效、在控件中集成动效系统,以及最佳实践。所有动效设计均遵循 Ant Design 动效三原则:自然(Natural)、高效(Performant)、克制(Concise)。
0. 动效选择指南
在为控件选择动效之前,请先回答以下问题(基于 Ant Design 动效设计原则):
0.1 是否需要动效?
遵循 克制 原则 — 只在有明确目的时添加动效:
| 场景 | 是否需要动效 | 理由 |
|---|---|---|
| 弹层出现/消失 | ✅ 需要 | 描述层级关系,建立空间关联 |
| 列表项增删 | ✅ 需要 | 解释发生了什么,保持上下文 |
| 按钮点击反馈 | ✅ 需要 | 提供反馈,确认用户操作 |
| 普通文本更新 | ❌ 不需要 | 没有空间变化,动效无意义 |
| 高频数据刷新 | ❌ 不需要 | 影响性能,干扰用户 |
0.2 选择哪种动效?
根据 Ant Design 过渡设计模式 选择:
| 交互场景 | 推荐动效 | Ant Design 对应 | 设计意图 |
|---|---|---|---|
| 浮层从触发点弹出 | ZoomBigIn/Out |
zoom-big |
描述层级关系(从触发点缩放) |
| 面板从边缘展开 | ZoomUp/Down/Left/RightIn/Out |
zoom-up/down/left/right |
保持上下文(方向性缩放) |
| 内容区域展开/折叠 | SlideUpIn/Out 或 Expand/Collapse |
slide-up / ant-motion-collapse |
保持上下文(空间连续性) |
| 元素从屏幕外滑入 | MoveUp/Down/Left/RightIn/Out |
move-* |
描述空间位置关系 |
| 简单的显示/隐藏 | FadeIn/Out |
fade |
最小化干扰 |
0.3 选择什么缓动曲线?
遵循 Ant Design 的 高效 原则和 不对称进出场 规律:
进入 (In) → 使用 Ease-Out 曲线(快速出现 → 缓慢到位)— 物体"跃入"
退出 (Out) → 使用 Ease-In 曲线(缓慢启动 → 快速消失)— 物体"收回"
| 动效类别 | 进入缓动 | 退出缓动 | 对应 Ant Design Token |
|---|---|---|---|
| Zoom | CircularEaseOut |
CircularEaseInOut |
motionEaseOutCirc / motionEaseInOutCirc |
| Slide | QuinticEaseOut |
QuinticEaseIn |
motionEaseOutQuint / motionEaseInQuint |
| Move | CircularEaseOut |
CircularEaseInOut |
motionEaseOutCirc / motionEaseInOutCirc |
| Collapse | CubicEaseInOut |
CubicEaseInOut |
motionEaseInOut |
| Fade | LinearEasing |
LinearEasing |
linear |
1. 使用预置动效
1.1 基本用法:在控件中播放动效
最简单的场景 — 在一个已有的 MotionActor 上播放预置动效:
// 1. 获取 MotionActor(通常来自 ControlTheme 模板)
var actor = this.FindDescendantOfType<MotionActor>();
// 2. 创建 Motion
var motion = new ZoomBigInMotion(
duration: TimeSpan.FromMilliseconds(200),
easing: new CircularEaseOut()
);
// 3. 播放
motion.Run(actor,
aboutToStart: () => { /* 动画即将开始 */ },
completedAction: () => { /* 动画已完成 */ }
);
1.2 异步用法
var motion = new FadeInMotion(TimeSpan.FromMilliseconds(300));
await motion.RunAsync(actor, aboutToStart: () => actor.IsVisible = true);
// 到达这里时动画已完成
1.3 在 ControlTheme 中放置 MotionActor
在 AXAML ControlTheme 模板中,将需要动画的内容包裹在 MotionActor 中:
<ControlTheme x:Class="AtomUI.Desktop.Controls.Themes.MyControlTheme"
TargetType="atom:MyControl">
<Setter Property="Template">
<ControlTemplate>
<atomp:MotionActor x:Name="PART_MotionActor">
<!-- 被动画的内容 -->
<Border Background="{TemplateBinding Background}">
<ContentPresenter Content="{TemplateBinding Content}" />
</Border>
</atomp:MotionActor>
</ControlTemplate>
</Setter>
</ControlTheme>
命名空间:
atomp:对应AtomUI.Controls.Primitives(MotionActor所在命名空间)。
在代码中获取 Actor:
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_motionActor = e.NameScope.Find<MotionActor>("PART_MotionActor");
}
2. 为控件添加动效支持
2.1 实现 IMotionAwareControl
任何需要响应全局动画开关的控件都应实现 IMotionAwareControl:
using AtomUI.Controls;
using AtomUI.MotionScene;
public class MyControl : TemplatedControl, IMotionAwareControl
{
// 1. 注册 IsMotionEnabled 属性
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<MyControl>();
public bool IsMotionEnabled
{
get => GetValue(IsMotionEnabledProperty);
set => SetValue(IsMotionEnabledProperty, value);
}
public MyControl()
{
// 2. 自动绑定全局 EnableMotion Token
this.ConfigureMotionBindingStyle();
}
private void PlayOpenAnimation()
{
// 3. 在播放动画前检查开关
if (!IsMotionEnabled || _motionActor == null)
{
// 跳过动画,直接设置最终状态
SetFinalState();
return;
}
var motion = new SlideUpInMotion(/* duration */);
motion.Run(_motionActor, completedAction: () => { /* ... */ });
}
}
2.2 使用 MotionDuration 属性
如果控件需要自定义动画时长(从 Token 派生或允许使用者覆盖):
public static readonly StyledProperty<TimeSpan> MotionDurationProperty =
MotionAwareControlProperty.MotionDurationProperty.AddOwner<MyControl>();
public TimeSpan MotionDuration
{
get => GetValue(MotionDurationProperty);
set => SetValue(MotionDurationProperty, value);
}
并在 Theme 或 Instance Style 中绑定到 Token:
// 在构造函数中
var style = new Style();
style.Add(MotionDurationProperty, SharedTokenKind.MotionDurationMid);
this.Styles.Add(style);
2.3 暴露可替换的 Motion 属性
允许控件使用者替换默认动效:
public static readonly StyledProperty<AbstractMotion?> OpenMotionProperty =
AvaloniaProperty.Register<MyControl, AbstractMotion?>(nameof(OpenMotion));
public static readonly StyledProperty<AbstractMotion?> CloseMotionProperty =
AvaloniaProperty.Register<MyControl, AbstractMotion?>(nameof(CloseMotion));
// 使用时
var motion = OpenMotion ?? new ZoomBigInMotion();
motion.Duration = MotionDuration;
motion.Run(_motionActor, ...);
使用者可以在 XAML 中替换:
<atom:MyControl>
<atom:MyControl.OpenMotion>
<motionScene:FadeInMotion Duration="0:0:0.5" />
</atom:MyControl.OpenMotion>
</atom:MyControl>
3. 创建自定义动效
3.1 Transition 模式(简单两点过渡)
适用于只有起始和结束两个状态的简单动效:
using AtomUI.MotionScene;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
public class MyCustomFadeZoomInMotion : AbstractMotion
{
public MyCustomFadeZoomInMotion(TimeSpan? duration = null, Easing? easing = null)
: base(duration ?? TimeSpan.FromMilliseconds(250),
easing ?? new CubicEaseOut(),
FillMode.Forward)
{
// SpiritType 默认为 Transition,无需设置
}
protected override void ConfigureTransitions()
{
// 调用基类创建 Opacity + MotionTransform 的默认 Transition
base.ConfigureTransitions();
// 设置变换原点
RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
}
protected override void ConfigureMotionStartValue(BaseMotionActor actor)
{
actor.Opacity = 0.0;
actor.MotionTransform = BuildScaleTransform(0.5);
}
protected override void ConfigureMotionEndValue(BaseMotionActor actor)
{
actor.Opacity = 1.0;
actor.MotionTransform = BuildScaleTransform(1.0);
}
}
要点:
base.ConfigureTransitions()已经创建了NotifiableDoubleTransition(Opacity)和NotifiableTransformOperationsTransition(MotionTransform),通常无需额外添加。- 使用
BuildScaleTransform()、BuildTranslateTransform()等辅助方法构造变换。
3.2 Animation 模式(多关键帧动画)
适用于需要中间状态的复杂动效:
public class MyBounceInMotion : AbstractMotion
{
public MyBounceInMotion(TimeSpan? duration = null)
: base(duration ?? TimeSpan.FromMilliseconds(400),
new CubicEaseOut(),
FillMode.Forward)
{
// 关键:切换到 Animation 驱动模式
SpiritType = MotionSpiritType.Animation;
}
protected override void ConfigureAnimation()
{
var animation = CreateAnimation(); // 使用基类辅助方法
// 关键帧 1: 0% — 初始状态
var startFrame = new KeyFrame { Cue = new Cue(0.0) };
startFrame.Setters.Add(new Setter
{
Property = Visual.OpacityProperty,
Value = 0.0
});
startFrame.Setters.Add(new Setter
{
Property = BaseMotionActor.MotionTransformOperationsProperty,
Value = BuildScaleTransform(0.3)
});
animation.Children.Add(startFrame);
// 关键帧 2: 60% — 弹性过冲
var overshootFrame = new KeyFrame { Cue = new Cue(0.6) };
overshootFrame.Setters.Add(new Setter
{
Property = Visual.OpacityProperty,
Value = 0.8
});
overshootFrame.Setters.Add(new Setter
{
Property = BaseMotionActor.MotionTransformOperationsProperty,
Value = BuildScaleTransform(1.1) // 过冲到 110%
});
animation.Children.Add(overshootFrame);
// 关键帧 3: 100% — 最终状态
var endFrame = new KeyFrame { Cue = new Cue(1.0) };
endFrame.Setters.Add(new Setter
{
Property = Visual.OpacityProperty,
Value = 1.0
});
endFrame.Setters.Add(new Setter
{
Property = BaseMotionActor.MotionTransformOperationsProperty,
Value = BuildScaleTransform(1.0)
});
animation.Children.Add(endFrame);
// 设置变换原点
RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
// 添加到 Animations 列表
Animations.Add(animation);
}
protected override void NotifyPreStart(BaseMotionActor actor)
{
base.NotifyPreStart(actor);
// 在动画开始前设置初始状态
actor.Opacity = 0.0;
}
}
要点:
- Animation 模式必须设置
SpiritType = MotionSpiritType.Animation。- 使用
CreateAnimation()辅助方法创建预配置了 Duration/Easing/FillMode 的 Animation。- 关键帧使用
MotionTransformOperationsProperty(而不是MotionTransformProperty),因为 Animation 需要 TransformOperations 类型。NotifyPreStart可用于在动画开始前设置初始可见状态。
3.3 使用辅助方法构建 Transform
AbstractMotion 提供了以下静态辅助方法:
// 等比缩放
protected static ITransform BuildScaleTransform(double scale)
protected static ITransform BuildScaleTransform(double scaleX, double scaleY)
// 仅 X 轴缩放
protected static ITransform BuildScaleXTransform(double scale)
// 仅 Y 轴缩放
protected static ITransform BuildScaleYTransform(double scale)
// 位移
protected static ITransform BuildTranslateTransform(double offsetX, double offsetY)
// 缩放 + 位移组合
protected static ITransform BuildTranslateScaleAndTransform(
double scaleX, double scaleY, double offsetX, double offsetY)
3.4 为 SceneLayer 计算尺寸
如果自定义动效涉及位移(Translate),需要重写 SceneLayer 尺寸计算方法,确保浮动渲染层有足够空间:
internal override Size CalculateSceneSize(Size motionTargetSize)
{
// 位移动画需要扩大渲染区域
return motionTargetSize.WithHeight(motionTargetSize.Height * 2);
}
internal override Point CalculateScenePosition(Size motionTargetSize, Point motionTargetPosition)
{
// 调整渲染层的起始位置
return motionTargetPosition.WithY(motionTargetPosition.Y - motionTargetSize.Height);
}
4. 使用 Transition 系统(属性过渡)
除了 MotionScene 中的 Motion 动效,AtomUI 还提供了丰富的属性 Transition 工具,用于控件状态变化时的平滑过渡(如 hover 时颜色变化)。
4.1 TransitionUtils — Token 感知的 Transition 工厂
using AtomUI.Controls;
// 创建一个与 Token 时长绑定的过渡
var transition = TransitionUtils.CreateTransition<DoubleTransition>(
targetProperty: OpacityProperty,
durationResourceKey: SharedTokenKind.MotionDurationFast, // 从 Token 获取时长
easing: new CubicEaseOut()
);
// 在控件中使用
Transitions = new Transitions
{
TransitionUtils.CreateTransition<DoubleTransition>(
OpacityProperty, SharedTokenKind.MotionDurationFast),
TransitionUtils.CreateTransition<SolidColorBrushTransition>(
ForegroundProperty, SharedTokenKind.MotionDurationMid),
TransitionUtils.CreateTransition<BoxShadowsTransition>(
BoxShadowProperty, SharedTokenKind.MotionDurationFast)
};
4.2 可用的 Transition 类型
| 类型 | 属性类型 | 来源 |
|---|---|---|
DoubleTransition |
double |
Avalonia 内置 |
ThicknessTransition |
Thickness |
Avalonia 内置 |
CornerRadiusTransition |
CornerRadius |
Avalonia 内置 |
TransformOperationsTransition |
TransformOperations |
Avalonia 内置 |
SolidColorBrushTransition |
IBrush |
AtomUI 自定义 — 支持渐变笔刷 |
BoxShadowsTransition |
BoxShadows |
AtomUI 自定义 — 多阴影插值 |
4.3 时长 Token 参考
| Token | 默认值 | 适用场景 |
|---|---|---|
SharedTokenKind.MotionDurationFast |
100ms | 小型元素状态变化(按钮 hover、图标旋转) |
SharedTokenKind.MotionDurationMid |
200ms | 中型元素变化(面板切换、内容过渡) |
SharedTokenKind.MotionDurationSlow |
300ms | 大型面板动画(Drawer、Modal) |
SharedTokenKind.MotionDurationVerySlow |
800ms | 特殊长动画 |
5. WaveSpirit 点击涟漪效果
Ant Design 标志性的点击涟漪源自"树叶飘浮在水面"的设计隐喻 — 按下时叶子下沉,释放时反弹,产生涟漪扩散。这是 Ant Design 自然 原则的标杆实现。原版使用
box-shadow: 0 0 0 6px currentcolor+ opacity 0.2→0 的 CSS 过渡实现(见components/_util/wave/style.ts,使用motionEaseOutCirc缓动),AtomUI 使用独立的尺寸 + 不透明度双动画管线精确复刻。
5.1 在 ControlTheme 中添加涟漪
在 AXAML 模板中添加 WaveSpiritDecorator:
<Grid>
<!-- 主要内容 -->
<Border x:Name="PART_RootBorder" ...>
<ContentPresenter ... />
</Border>
<!-- 涟漪层(覆盖在内容之上) -->
<atomp:WaveSpiritDecorator x:Name="PART_WaveSpirit" />
</Grid>
5.2 在代码中触发涟漪
private WaveSpiritDecorator? _waveSpiritDecorator;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_waveSpiritDecorator = e.NameScope.Find<WaveSpiritDecorator>("PART_WaveSpirit");
}
protected override void OnClick()
{
base.OnClick();
_waveSpiritDecorator?.Play();
}
5.3 WaveSpirit 类型
| 类型 | 效果 | 适用控件 |
|---|---|---|
RoundRectWave |
圆角矩形扩散波纹 | Button、Tag 等矩形控件 |
PillWave |
药丸形扩散波纹 | 全圆角按钮 |
CircleWave |
圆形扩散波纹 | CircleButton、Radio |
6. 常见模式与最佳实践
6.1 Popup 开关动画模式
AtomUI 的 Popup 类提供了完整的动效感知开关方法:
// 使用 MotionAwareOpen/Close 代替原生 Open/Close
popup.MotionAwareOpen(opened: () => { /* Popup 已完全打开 */ });
popup.MotionAwareClose(closed: () => { /* Popup 已完全关闭 */ });
// 异步版本
await popup.MotionAwareOpenAsync();
await popup.MotionAwareCloseAsync();
// 通过属性绑定控制(MVVM 友好)
popup.IsMotionAwareOpen = true; // 触发 MotionAwareOpen
popup.IsMotionAwareOpen = false; // 触发 MotionAwareClose
6.2 折叠/展开动画模式
以 CollapseItem 为例:
private void ShowExpandContent()
{
if (!IsMotionEnabled || _contentMotionActor == null) return;
var motion = new SlideUpInMotion(MotionDuration, new CubicEaseOut());
motion.Run(_contentMotionActor, completedAction: () =>
{
// 动画完成后的状态设置
});
}
private void HideExpandContent()
{
if (!IsMotionEnabled || _contentMotionActor == null) return;
var motion = new SlideUpOutMotion(MotionDuration, new CubicEaseIn());
motion.Run(_contentMotionActor, completedAction: () =>
{
// 动画完成后隐藏内容
});
}
6.3 方向感知动画模式
以 Drawer 为例,根据放置方向选择不同 Motion:
AbstractMotion motion;
switch (Placement)
{
case Direction.Left:
motion = isOpen
? new MoveLeftInMotion(actor.DesiredSize.Width, duration, new CubicEaseOut())
: new MoveLeftOutMotion(actor.DesiredSize.Width, duration, new CubicEaseOut());
break;
case Direction.Right:
motion = isOpen
? new MoveRightInMotion(actor.DesiredSize.Width, duration, new CubicEaseOut())
: new MoveRightOutMotion(actor.DesiredSize.Width, duration, new CubicEaseOut());
break;
case Direction.Top:
motion = isOpen
? new MoveUpInMotion(actor.DesiredSize.Height, duration, new CubicEaseOut())
: new MoveUpOutMotion(actor.DesiredSize.Height, duration, new CubicEaseOut());
break;
case Direction.Bottom:
motion = isOpen
? new MoveDownInMotion(actor.DesiredSize.Height, duration, new CubicEaseOut())
: new MoveDownOutMotion(actor.DesiredSize.Height, duration, new CubicEaseOut());
break;
}
motion.Run(_motionActor, completedAction: () => { /* ... */ });
6.4 防止动画状态竞争
当用户快速操作可能导致打开/关闭动画重叠时,使用状态标志保护:
private bool _openAnimating;
private bool _closeAnimating;
public void AnimatedOpen()
{
// 防止重入
if (_openAnimating || _closeAnimating) return;
_openAnimating = true;
var motion = new ZoomBigInMotion();
motion.Run(_motionActor, completedAction: () =>
{
_openAnimating = false;
});
}
Popup 类中还实现了更复杂的 "关闭请求排队" 模式:
// 当打开动画未完成时收到关闭请求
if (_openAnimating)
{
RequestCloseWhereAnimationCompleted = true; // 标记
return;
}
// 在打开动画完成回调中检查
completedAction: () =>
{
_openAnimating = false;
if (RequestCloseWhereAnimationCompleted)
{
RequestCloseWhereAnimationCompleted = false;
Dispatcher.UIThread.Post(() => MotionAwareClose()); // 延迟执行关闭
}
}
6.5 超时兜底
为防止动画完成回调因极端情况未被触发,使用 DispatcherTimer 超时兜底:
var completedFuncCalled = false;
// 超时兜底 — 时长 * 1.2
_timeoutDisposable = DispatcherTimer.RunOnce(() =>
{
if (!completedFuncCalled)
{
completedFuncCalled = true;
// 强制执行完成逻辑
}
}, motion.Duration * 1.2);
// 正常完成路径
motion.Run(actor, completedAction: () =>
{
_timeoutDisposable?.Dispose();
if (!completedFuncCalled)
{
completedFuncCalled = true;
// 正常完成逻辑
}
});
7. 注意事项
7.1 线程安全
所有动画 API 必须在 UI 线程 上调用。AbstractMotion.Run() 内部使用 Dispatcher.UIThread 确保执行上下文正确。
7.2 Follow 模式限制
处于 Follow 模式(actor.Follow(target))的 Actor 不能直接播放 Motion,否则会抛出异常。需要先调用 actor.UnFollow() 解除绑定。
7.3 Out 动效的状态重置
所有 *OutMotion(退出动效)在 NotifyCompleted 中会重置 Actor 状态:
protected override void NotifyCompleted(BaseMotionActor actor)
{
actor.Opacity = 1.0; // 恢复不透明
actor.MotionTransform = null; // 清除变换
}
这确保 Actor 在下次复用时处于干净状态。如果你的自定义 Out Motion 没有这个重置,可能导致 Actor 在后续使用中仍然是不可见的。
7.4 避免硬编码时长
始终使用 Design Token 派生的时长(对应 Ant Design 的 motionDurationFast / motionDurationMid / motionDurationSlow):
// ❌ 不推荐
motion.Duration = TimeSpan.FromMilliseconds(200);
// ✅ 推荐
motion.Duration = MotionDuration; // 来自 Token 绑定的属性
或在创建 Transition 时使用 SharedTokenKind:
TransitionUtils.CreateTransition<DoubleTransition>(
OpacityProperty,
SharedTokenKind.MotionDurationFast // 从全局 Token 获取时长
);
7.5 EnableMotion 全局开关
当 EnableMotion = false 时,控件应跳过所有动画直接到达最终状态。这对应 Ant Design 的 motion: false Seed Token 和 @media (prefers-reduced-motion: reduce) 无障碍降级:
if (!IsMotionEnabled)
{
// 直接设置最终状态,不播放动画
_contentPanel.IsVisible = true;
_contentPanel.Opacity = 1.0;
return;
}
// 正常播放动画...
7.6 遵循 Ant Design 动效三原则
在为控件添加动效时,请始终检查是否符合以下原则:
| 原则 | 检查点 |
|---|---|
| 自然 | 缓动曲线是否模拟了真实物理运动?进入用 Ease-Out、退出用 Ease-In? |
| 高效 | 动画时长是否足够短?是否使用了正确的 MotionDuration* Token?退出是否比进入更快? |
| 克制 | 动效是否有明确目的?是否存在不必要的装饰性动画?是否避免了同时播放过多动画? |
7.7 退出动画的 HitTest 处理
Ant Design 在退出动画激活时设置 pointerEvents: 'none',防止正在消失的元素响应鼠标事件。在 AtomUI 中,退出动效应在动画开始时设置 IsHitTestVisible = false:
// 在退出动画开始前
motion.Run(_motionActor,
aboutToStart: () =>
{
_motionActor.IsHitTestVisible = false; // 禁用鼠标事件
},
completedAction: () =>
{
_motionActor.IsVisible = false;
_motionActor.IsHitTestVisible = true; // 恢复
});
8. 完整示例:自定义控件的动效集成
以下是一个完整的控件动效集成示例,展示所有最佳实践:
using AtomUI.Controls;
using AtomUI.Controls.Primitives;
using AtomUI.MotionScene;
using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
namespace AtomUI.Desktop.Controls;
public class ExpandablePanel : TemplatedControl, IMotionAwareControl
{
#region 公共属性
public static readonly StyledProperty<bool> IsExpandedProperty =
AvaloniaProperty.Register<ExpandablePanel, bool>(nameof(IsExpanded));
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<ExpandablePanel>();
public static readonly StyledProperty<TimeSpan> MotionDurationProperty =
MotionAwareControlProperty.MotionDurationProperty.AddOwner<ExpandablePanel>();
public static readonly StyledProperty<AbstractMotion?> ExpandMotionProperty =
AvaloniaProperty.Register<ExpandablePanel, AbstractMotion?>(nameof(ExpandMotion));
public static readonly StyledProperty<AbstractMotion?> CollapseMotionProperty =
AvaloniaProperty.Register<ExpandablePanel, AbstractMotion?>(nameof(CollapseMotion));
public bool IsExpanded
{
get => GetValue(IsExpandedProperty);
set => SetValue(IsExpandedProperty, value);
}
public bool IsMotionEnabled
{
get => GetValue(IsMotionEnabledProperty);
set => SetValue(IsMotionEnabledProperty, value);
}
public TimeSpan MotionDuration
{
get => GetValue(MotionDurationProperty);
set => SetValue(MotionDurationProperty, value);
}
public AbstractMotion? ExpandMotion
{
get => GetValue(ExpandMotionProperty);
set => SetValue(ExpandMotionProperty, value);
}
public AbstractMotion? CollapseMotion
{
get => GetValue(CollapseMotionProperty);
set => SetValue(CollapseMotionProperty, value);
}
#endregion
private MotionActor? _motionActor;
private bool _animating;
public ExpandablePanel()
{
// 自动绑定全局 EnableMotion Token
this.ConfigureMotionBindingStyle();
// 绑定 MotionDuration 到 Token
var style = new Style();
style.Add(MotionDurationProperty, SharedTokenKind.MotionDurationMid);
Styles.Add(style);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_motionActor = e.NameScope.Find<MotionActor>("PART_MotionActor");
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsExpandedProperty)
{
if (change.GetNewValue<bool>())
PlayExpandAnimation();
else
PlayCollapseAnimation();
}
}
private void PlayExpandAnimation()
{
if (_animating || _motionActor == null) return;
if (!IsMotionEnabled)
{
_motionActor.IsVisible = true;
return;
}
_animating = true;
_motionActor.IsVisible = true;
var motion = ExpandMotion ?? new ExpandMotion(Direction.Top, MotionDuration, new CubicEaseOut());
if (MotionDuration != TimeSpan.Zero)
motion.Duration = MotionDuration;
motion.Run(_motionActor, completedAction: () => { _animating = false; });
}
private void PlayCollapseAnimation()
{
if (_animating || _motionActor == null) return;
if (!IsMotionEnabled)
{
_motionActor.IsVisible = false;
return;
}
_animating = true;
var motion = CollapseMotion ?? new CollapseMotion(Direction.Top, MotionDuration, new CubicEaseIn());
if (MotionDuration != TimeSpan.Zero)
motion.Duration = MotionDuration;
motion.Run(_motionActor, completedAction: () =>
{
_motionActor.IsVisible = false;
_animating = false;
});
}
}