Merge pull request #609 from WCKYWCKF/pr5

Add Animation to NavMenu
This commit is contained in:
Dong Bin
2025-08-20 01:27:47 +08:00
committed by GitHub
7 changed files with 388 additions and 32 deletions

View File

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

View File

@@ -1,4 +1,13 @@
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;
namespace Ursa.Demo.Pages; namespace Ursa.Demo.Pages;

View File

@@ -12,27 +12,27 @@
<ControlTemplate TargetType="u:NavMenu"> <ControlTemplate TargetType="u:NavMenu">
<Grid RowDefinitions="Auto, *, Auto"> <Grid RowDefinitions="Auto, *, Auto">
<ContentPresenter Content="{TemplateBinding Header}" /> <ContentPresenter Content="{TemplateBinding Header}" />
<ScrollViewer <ScrollViewer
Grid.Row="1" Grid.Row="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Disabled"
AllowAutoHide="True" AllowAutoHide="True"
VerticalScrollBarVisibility="Auto"> VerticalScrollBarVisibility="Auto">
<ScrollViewer.Styles> <ScrollViewer.Styles>
<Style Selector="ScrollViewer /template/ ScrollBar"> <Style Selector="ScrollViewer /template/ ScrollBar">
<Setter Property="Opacity" Value="0" /> <Setter Property="Opacity" Value="0" />
</Style> </Style>
<Style Selector="ScrollViewer:pointerover"> <Style Selector="ScrollViewer:pointerover">
<Style Selector="^ /template/ ScrollBar#PART_HorizontalScrollBar"> <Style Selector="^ /template/ ScrollBar#PART_HorizontalScrollBar">
<Setter Property="Opacity" Value="1" /> <Setter Property="Opacity" Value="1" />
</Style> </Style>
<Style Selector="^ /template/ ScrollBar#PART_VerticalScrollBar"> <Style Selector="^ /template/ ScrollBar#PART_VerticalScrollBar">
<Setter Property="Opacity" Value="1" /> <Setter Property="Opacity" Value="1" />
</Style> </Style>
</Style> </Style>
</ScrollViewer.Styles> </ScrollViewer.Styles>
<ItemsPresenter Name="PART_ItemsPresenter" ItemsPanel="{TemplateBinding ItemsPanel}" /> <ItemsPresenter Name="PART_ItemsPresenter" ItemsPanel="{TemplateBinding ItemsPanel}" />
</ScrollViewer> </ScrollViewer>
<ContentPresenter Grid.Row="2" Content="{TemplateBinding Footer}" /> <ContentPresenter Grid.Row="2" Content="{TemplateBinding Footer}" />
</Grid> </Grid>
</ControlTemplate> </ControlTemplate>
@@ -44,6 +44,15 @@
<Setter Property="Width" Value="{Binding $self.CollapseWidth}" /> <Setter Property="Width" Value="{Binding $self.CollapseWidth}" />
<Setter Property="Grid.IsSharedSizeScope" Value="False" /> <Setter Property="Grid.IsSharedSizeScope" Value="False" />
</Style> </Style>
<Style Selector="^.enable_animation">
<Setter Property="u:SizeAnimationHelper.TriggerAvaloniaProperty"
Value="{x:Static u:NavMenu.IsHorizontalCollapsedProperty}">
</Setter>
<Setter Property="u:SizeAnimationHelper.CreateAnimation"
Value="{DynamicResource NavMenuWidthAnimationGenerator}">
</Setter>
<Setter Property="u:SizeAnimationHelper.EnableWHAnimation" Value="True"></Setter>
</Style>
</ControlTheme> </ControlTheme>
<ControlTemplate x:Key="DefaultNavMenuItemTemplate" TargetType="u:NavMenuItem"> <ControlTemplate x:Key="DefaultNavMenuItemTemplate" TargetType="u:NavMenuItem">
@@ -134,13 +143,15 @@
<Transitions> <Transitions>
<DoubleTransition Easing="QuadraticEaseIn" Property="Height" Duration="0.25" /> <DoubleTransition Easing="QuadraticEaseIn" Property="Height" Duration="0.25" />
<DoubleTransition Easing="QuadraticEaseOut" Property="Opacity" Duration="0.25" /> <DoubleTransition Easing="QuadraticEaseOut" Property="Opacity" Duration="0.25" />
<TransformOperationsTransition Easing="QuadraticEaseInOut" Property="RenderTransform" Duration="0.25" /> <TransformOperationsTransition Easing="QuadraticEaseInOut" Property="RenderTransform"
Duration="0.25" />
</Transitions> </Transitions>
</ItemsPresenter.Transitions> </ItemsPresenter.Transitions>
</ItemsPresenter> </ItemsPresenter>
<LayoutTransformControl.Transitions> <LayoutTransformControl.Transitions>
<Transitions> <Transitions>
<TransformOperationsTransition Easing="QuadraticEaseInOut" Property="LayoutTransform" Duration="0.15" Delay="0.1" /> <TransformOperationsTransition Easing="QuadraticEaseInOut" Property="LayoutTransform"
Duration="0.15" Delay="0.1" />
</Transitions> </Transitions>
</LayoutTransformControl.Transitions> </LayoutTransformControl.Transitions>
</LayoutTransformControl> </LayoutTransformControl>
@@ -183,12 +194,12 @@
<Style Selector="^ /template/ ContentPresenter#PART_HeaderPresenter:pointerover"> <Style Selector="^ /template/ ContentPresenter#PART_HeaderPresenter:pointerover">
<Setter Property="Foreground" Value="{DynamicResource ListBoxItemPointeroverForeground}" /> <Setter Property="Foreground" Value="{DynamicResource ListBoxItemPointeroverForeground}" />
</Style> </Style>
<Style Selector="^:focus /template/ Border#PART_Border"> <Style Selector="^:focus /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource ListBoxItemPointeroverBackground}" /> <Setter Property="Background" Value="{DynamicResource ListBoxItemPointeroverBackground}" />
</Style> </Style>
<Style Selector="^:focus /template/ ContentPresenter#PART_HeaderPresenter"> <Style Selector="^:focus /template/ ContentPresenter#PART_HeaderPresenter">
<Setter Property="Foreground" Value="{DynamicResource ListBoxItemPointeroverForeground}" /> <Setter Property="Foreground" Value="{DynamicResource ListBoxItemPointeroverForeground}" />
</Style> </Style>
<Style Selector="^:horizontal-collapsed:first-level"> <Style Selector="^:horizontal-collapsed:first-level">
<Setter Property="HorizontalAlignment" Value="Stretch" /> <Setter Property="HorizontalAlignment" Value="Stretch" />
<Style Selector="^ /template/ Border#PART_Border"> <Style Selector="^ /template/ Border#PART_Border">

View File

@@ -26,6 +26,8 @@ public class SemiTheme : Styles
public SemiTheme(IServiceProvider? provider = null) public SemiTheme(IServiceProvider? provider = null)
{ {
AvaloniaXamlLoader.Load(provider, this); AvaloniaXamlLoader.Load(provider, this);
Resources.MergedDictionaries.Add(new SizeAnimations.DefaultSizeAnimations());
Resources.MergedDictionaries.Add(new SizeAnimations.NavMenuSizeAnimations());
} }
public static ThemeVariant Aquatic => new(nameof(Aquatic), ThemeVariant.Dark); public static ThemeVariant Aquatic => new(nameof(Aquatic), ThemeVariant.Dark);
@@ -95,4 +97,4 @@ public class SemiTheme : Styles
if (!_localeToResource.TryGetValue(culture, out var resources)) return; if (!_localeToResource.TryGetValue(culture, out var resources)) return;
foreach (var kv in resources) element.Resources[kv.Key] = kv.Value; foreach (var kv in resources) element.Resources[kv.Key] = kv.Value;
} }
} }

View File

@@ -0,0 +1,105 @@
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Styling;
using Ursa.Helpers;
namespace Ursa.Themes.Semi.SizeAnimations;
public partial class DefaultSizeAnimations : ResourceDictionary
{
public const string WidthAnimationGeneratorKey = "WidthAnimationGenerator";
public const string HeightAnimationGeneratorKey = "HeightAnimationGenerator";
public const string WidthHeightAnimationGeneratorKey = "WidthHeightAnimationGenerator";
public DefaultSizeAnimations()
{
Add(WidthAnimationGeneratorKey, WidthAnimationGenerator);
Add(HeightAnimationGeneratorKey, HeightAnimationGenerator);
Add(WidthHeightAnimationGeneratorKey, WidthHeightAnimationGenerator);
}
private readonly SizeAnimationHelperAnimationGeneratorDelegate WidthAnimationGenerator =
(_, oldDesiredSize, newDesiredSize) => 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, oldDesiredSize.Width)
}
},
new KeyFrame
{
Cue = new Cue(1.0),
Setters =
{
new Setter(Layoutable.WidthProperty, newDesiredSize.Width)
}
}
}
};
private readonly SizeAnimationHelperAnimationGeneratorDelegate HeightAnimationGenerator =
(_, oldDesiredSize, newDesiredSize) => new Animation
{
Duration = TimeSpan.FromMilliseconds(300),
Easing = new CubicEaseInOut(),
FillMode = FillMode.None,
Children =
{
new KeyFrame
{
Cue = new Cue(0.0),
Setters =
{
new Setter(Layoutable.HeightProperty, oldDesiredSize.Height)
}
},
new KeyFrame
{
Cue = new Cue(1.0),
Setters =
{
new Setter(Layoutable.HeightProperty, newDesiredSize.Height)
}
}
}
};
private readonly SizeAnimationHelperAnimationGeneratorDelegate WidthHeightAnimationGenerator =
(_, oldDesiredSize, newDesiredSize) => 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, oldDesiredSize.Width),
new Setter(Layoutable.HeightProperty, oldDesiredSize.Height)
}
},
new KeyFrame
{
Cue = new Cue(1.0),
Setters =
{
new Setter(Layoutable.WidthProperty, newDesiredSize.Width),
new Setter(Layoutable.HeightProperty, newDesiredSize.Height)
}
}
}
};
}

View File

@@ -0,0 +1,52 @@
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Styling;
using Ursa.Helpers;
namespace Ursa.Themes.Semi.SizeAnimations;
public class NavMenuSizeAnimations : ResourceDictionary
{
public const string NavMenuWidthAnimationGeneratorKey = "NavMenuWidthAnimationGenerator";
public NavMenuSizeAnimations()
{
Add(NavMenuWidthAnimationGeneratorKey, NavMenuWidthAnimationGenerator);
}
private readonly SizeAnimationHelperAnimationGeneratorDelegate NavMenuWidthAnimationGenerator =
(_, 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(Layoutable.WidthProperty, oldDesiredSize.Width),
new Setter(Layoutable.HeightProperty, oldDesiredSize.Height)
}
},
new KeyFrame
{
Cue = new Cue(1.0),
Setters =
{
new Setter(Layoutable.WidthProperty, newDesiredSize.Width),
new Setter(Layoutable.HeightProperty, newDesiredSize.Height)
}
}
}
};
};
}

View File

@@ -0,0 +1,175 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices.ComTypes;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Styling;
namespace Ursa.Helpers;
public delegate Animation SizeAnimationHelperAnimationGeneratorDelegate(Control animationTargetControl,
Size oldDesiredSize,
Size newDesiredSize);
public class SizeAnimationHelper : AvaloniaObject
{
public static readonly AttachedProperty<SizeAnimationHelperAnimationGeneratorDelegate> CreateAnimationProperty =
AvaloniaProperty
.RegisterAttached<SizeAnimationHelper, Control, SizeAnimationHelperAnimationGeneratorDelegate>(
"CreateAnimation", CreateAnimationPropertyDefaultValue);
internal static readonly AttachedProperty<CancellationTokenSource?> AnimationCancellationTokenSourceProperty =
AvaloniaProperty.RegisterAttached<SizeAnimationHelper, Control, CancellationTokenSource?>(
"AnimationCancellationTokenSource");
public static readonly AttachedProperty<AvaloniaProperty?> TriggerAvaloniaPropertyProperty =
AvaloniaProperty.RegisterAttached<SizeAnimationHelper, Control, AvaloniaProperty?>("TriggerAvaloniaProperty");
public static readonly AttachedProperty<bool> EnableWHAnimationProperty =
AvaloniaProperty.RegisterAttached<SizeAnimationHelper, Control, bool>("EnableWHAnimation");
static SizeAnimationHelper()
{
EnableWHAnimationProperty.Changed.AddClassHandler<Control>(OnPropertyChanged);
}
private static Animation CreateAnimationPropertyDefaultValue(Control animationTargetControl, Size oldDesiredSize,
Size newDesiredSize)
{
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, oldDesiredSize.Width),
new Setter(Layoutable.HeightProperty, oldDesiredSize.Height)
}
},
new KeyFrame
{
Cue = new Cue(1.0),
Setters =
{
new Setter(Layoutable.WidthProperty, newDesiredSize.Width),
new Setter(Layoutable.HeightProperty, newDesiredSize.Height)
}
}
}
};
}
public static void SetCreateAnimation(Control obj, SizeAnimationHelperAnimationGeneratorDelegate value)
{
obj.SetValue(CreateAnimationProperty, value);
}
public static SizeAnimationHelperAnimationGeneratorDelegate 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)
{
obj.SetValue(EnableWHAnimationProperty, value);
}
public static bool GetEnableWHAnimation(Control obj)
{
return obj.GetValue(EnableWHAnimationProperty);
}
private static void OnPropertyChanged(Control obj, AvaloniaPropertyChangedEventArgs change)
{
if (change.Property != EnableWHAnimationProperty) return;
_ = change.NewValue is bool value ? value : throw new ArgumentNullException();
if (value)
{
var triggerProperty = GetTriggerAvaloniaProperty(obj);
if (triggerProperty == null)
{
throw new InvalidOperationException(
"SizeAnimationHelper requires TriggerAvaloniaProperty to be set when EnableWHAnimation is true.");
}
if (triggerProperty == Visual.BoundsProperty ||
triggerProperty == Layoutable.DesiredSizeProperty)
{
throw new InvalidOperationException(
"SizeAnimationHelper does not support Visual.BoundsProperty or Layoutable.DesiredSizeProperty as trigger property.");
}
if (obj.IsLoaded)
{
obj.PropertyChanged += AnimationTargetOnPropertyChanged;
}
else
{
obj.Loaded += ObjOnLoaded;
}
}
else
{
obj.Loaded -= ObjOnLoaded;
obj.PropertyChanged -= AnimationTargetOnPropertyChanged;
}
void ObjOnLoaded(object? sender, RoutedEventArgs e)
{
obj.PropertyChanged += AnimationTargetOnPropertyChanged;
obj.Loaded -= ObjOnLoaded;
}
}
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);
}
}