feat:Redesign the usage pattern of WHAnimationHelper once more to deliver a superior axaml editing and consumption experience.

This commit is contained in:
望尘空忧
2025-08-10 16:37:03 +08:00
parent c801c4eec4
commit b9e104a8b8
4 changed files with 160 additions and 93 deletions

View File

@@ -8,6 +8,7 @@
xmlns:u="https://irihi.tech/ursa"
xmlns:vm="using:Ursa.Demo.ViewModels"
xmlns:iri="https://irihi.tech/shared"
xmlns:views="using:Ursa.Demo.Pages"
d:DesignHeight="450"
d:DesignWidth="800"
x:CompileBindings="True"
@@ -39,7 +40,10 @@
IsHorizontalCollapsed="{Binding #collapse.IsChecked, Mode=OneWay}"
ItemsSource="{Binding MenuItems}"
SelectedItem="{Binding SelectedMenuItem}"
SubMenuBinding="{Binding Children}">
SubMenuBinding="{Binding Children}"
u:WHAnimationHelper.TriggerAvaloniaProperty="{x:Static u:NavMenu.IsHorizontalCollapsedProperty}"
u:WHAnimationHelper.CreateAnimation="{x:Static views:NavMenuDemo.NavMenuAnimation}"
u:WHAnimationHelper.EnableWHAnimation="True">
<u:NavMenu.Styles>
<Style x:DataType="vm:MenuItem" Selector="u|NavMenuItem">
<Setter Property="IsSeparator" Value="{Binding IsSeparator}" />

View File

@@ -1,5 +1,11 @@
using Avalonia.Controls;
using System;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Styling;
using Ursa.Controls;
using Ursa.Helpers;
@@ -7,17 +13,42 @@ namespace Ursa.Demo.Pages;
public partial class NavMenuDemo : UserControl
{
private readonly NavMenuAnimationHelper _animationHelper;
public NavMenuDemo()
{
InitializeComponent();
_animationHelper = new NavMenuAnimationHelper(menu);
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
_animationHelper.Start();
}
public static WHAnimationHelperCreateAnimationDelegate NavMenuAnimation { get; } =
(_, oldDesiredSize, newDesiredSize) =>
{
if (oldDesiredSize.Width > newDesiredSize.Width)
newDesiredSize = newDesiredSize.WithWidth(newDesiredSize.Width + 20);
return new Animation
{
Duration = TimeSpan.FromMilliseconds(300),
Easing = new CubicEaseInOut(),
FillMode = FillMode.None,
Children =
{
new KeyFrame
{
Cue = new Cue(0.0),
Setters =
{
new Setter(WidthProperty, oldDesiredSize.Width),
new Setter(HeightProperty, oldDesiredSize.Height)
}
},
new KeyFrame
{
Cue = new Cue(1.0),
Setters =
{
new Setter(WidthProperty, newDesiredSize.Width),
new Setter(HeightProperty, newDesiredSize.Height)
}
}
}
};
};
}

View File

@@ -1,46 +0,0 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Styling;
using Ursa.Controls;
namespace Ursa.Helpers;
public class NavMenuAnimationHelper(NavMenu control) : WHAnimationHelper(control, NavMenu.IsHorizontalCollapsedProperty)
{
protected override Animation CreateAnimation(Size oldValue, Size newValue)
{
if (oldValue.Width > newValue.Width)
{
newValue = newValue.WithWidth(newValue.Width + 20);
}
return new Animation
{
Duration = TimeSpan.FromMilliseconds(300),
Easing = new CubicEaseInOut(),
FillMode = FillMode.None,
Children =
{
new KeyFrame
{
Cue = new Cue(0.0),
Setters =
{
new Setter(Layoutable.WidthProperty, oldValue.Width)
}
},
new KeyFrame
{
Cue = new Cue(1.0),
Setters =
{
new Setter(Layoutable.WidthProperty, newValue.Width)
}
}
}
};
}
}

View File

@@ -2,51 +2,34 @@
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Ursa.Helpers;
public class WHAnimationHelper(Control control, AvaloniaProperty property)
public delegate Animation WHAnimationHelperCreateAnimationDelegate(Control animationTargetControl, Size oldDesiredSize,
Size newDesiredSize);
public class WHAnimationHelper : AvaloniaObject
{
private CancellationTokenSource? _cancellationTokenSource;
public static readonly AttachedProperty<WHAnimationHelperCreateAnimationDelegate> CreateAnimationProperty =
AvaloniaProperty
.RegisterAttached<WHAnimationHelper, Control, WHAnimationHelperCreateAnimationDelegate>(
"CreateAnimation", CreateAnimationPropertyDefaultValue);
~WHAnimationHelper()
{
_cancellationTokenSource?.Dispose();
}
internal static readonly AttachedProperty<CancellationTokenSource?> AnimationCancellationTokenSourceProperty =
AvaloniaProperty.RegisterAttached<WHAnimationHelper, Control, CancellationTokenSource?>(
"AnimationCancellationTokenSource");
public void Stop()
{
control.PropertyChanged -= AnimationTargetOnPropertyChanged;
}
public static readonly AttachedProperty<AvaloniaProperty?> TriggerAvaloniaPropertyProperty =
AvaloniaProperty.RegisterAttached<WHAnimationHelper, Control, AvaloniaProperty?>("TriggerAvaloniaProperty");
public void Start()
{
control.PropertyChanged += AnimationTargetOnPropertyChanged;
}
public static readonly AttachedProperty<bool> EnableWHAnimationProperty =
AvaloniaProperty.RegisterAttached<WHAnimationHelper, Control, bool>("EnableWHAnimation");
private void AnimationTargetOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender as Control != control ||
e.Property != property ||
control.IsLoaded is false ||
control.IsVisible is false) return;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
var oldValue = control.DesiredSize;
control.UpdateLayout();
var newValue = control.DesiredSize;
control.InvalidateArrange();
var animation = CreateAnimation(oldValue, newValue);
animation.RunAsync(control, _cancellationTokenSource.Token);
}
protected virtual Animation CreateAnimation(Size oldValue, Size newValue)
private static Animation CreateAnimationPropertyDefaultValue(Control animationTargetControl, Size oldDesiredSize,
Size newDesiredSize)
{
return new Animation
{
@@ -60,7 +43,8 @@ public class WHAnimationHelper(Control control, AvaloniaProperty property)
Cue = new Cue(0.0),
Setters =
{
new Setter(Layoutable.WidthProperty, oldValue.Width)
new Setter(Layoutable.WidthProperty, oldDesiredSize.Width),
new Setter(Layoutable.HeightProperty, oldDesiredSize.Height)
}
},
new KeyFrame
@@ -68,10 +52,104 @@ public class WHAnimationHelper(Control control, AvaloniaProperty property)
Cue = new Cue(1.0),
Setters =
{
new Setter(Layoutable.WidthProperty, newValue.Width)
new Setter(Layoutable.WidthProperty, newDesiredSize.Width),
new Setter(Layoutable.HeightProperty, newDesiredSize.Height)
}
}
}
};
}
public static void SetCreateAnimation(Control obj, WHAnimationHelperCreateAnimationDelegate value)
{
obj.SetValue(CreateAnimationProperty, value);
}
public static WHAnimationHelperCreateAnimationDelegate GetCreateAnimation(Control obj)
{
return obj.GetValue(CreateAnimationProperty);
}
internal static void SetAnimationCancellationTokenSource(Control obj, CancellationTokenSource? value)
{
obj.SetValue(AnimationCancellationTokenSourceProperty, value);
}
internal static CancellationTokenSource? GetAnimationCancellationTokenSource(Control obj)
{
return obj.GetValue(AnimationCancellationTokenSourceProperty);
}
public static void SetTriggerAvaloniaProperty(Control obj, AvaloniaProperty? value)
{
obj.SetValue(TriggerAvaloniaPropertyProperty, value);
}
public static AvaloniaProperty? GetTriggerAvaloniaProperty(Control obj)
{
return obj.GetValue(TriggerAvaloniaPropertyProperty);
}
public static void SetEnableWHAnimation(Control obj, bool value)
{
if (value == obj.GetValue(EnableWHAnimationProperty)) return;
obj.SetValue(EnableWHAnimationProperty, value);
if (value)
{
var triggerProperty = GetTriggerAvaloniaProperty(obj);
if (triggerProperty == null)
{
throw new InvalidOperationException(
"WHAnimationHelper requires TriggerAvaloniaProperty to be set when EnableWHAnimation is true.");
}
if (triggerProperty == Visual.BoundsProperty ||
triggerProperty == Layoutable.DesiredSizeProperty)
{
throw new InvalidOperationException(
"WHAnimationHelper does not support Visual.BoundsProperty or Layoutable.DesiredSizeProperty as trigger property.");
}
obj.Loaded += ObjOnLoaded;
}
else
{
obj.Loaded -= ObjOnLoaded;
obj.PropertyChanged -= AnimationTargetOnPropertyChanged;
}
void ObjOnLoaded(object? sender, RoutedEventArgs e)
{
obj.PropertyChanged += AnimationTargetOnPropertyChanged;
obj.Loaded -= ObjOnLoaded;
}
}
public static bool GetEnableWHAnimation(Control obj)
{
return obj.GetValue(EnableWHAnimationProperty);
}
private static void AnimationTargetOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender is not Control control ||
GetEnableWHAnimation(control) is false ||
e.Property != GetTriggerAvaloniaProperty(control) ||
e.Property == Visual.BoundsProperty ||
control.IsLoaded is false ||
control.IsVisible is false) return;
var cancellationTokenSource = GetAnimationCancellationTokenSource(control);
cancellationTokenSource?.Cancel();
cancellationTokenSource?.Dispose();
cancellationTokenSource = new CancellationTokenSource();
SetAnimationCancellationTokenSource(control, cancellationTokenSource);
var oldValue = control.DesiredSize;
control.UpdateLayout();
var newValue = control.DesiredSize;
control.InvalidateArrange();
var animation = GetCreateAnimation(control)(control, oldValue, newValue);
animation.RunAsync(control, cancellationTokenSource.Token);
}
}