Browse Source

Implement CompositionAnimation ICompositionAnimation support

xaml_integrated_comp_animations
Max Katz 1 week ago
parent
commit
9e49a4c5c4
  1. 1
      src/Avalonia.Base/Animation/Animatable.cs
  2. 12
      src/Avalonia.Base/Animation/Animation.cs
  3. 129
      src/Avalonia.Base/Animation/CompositionAnimations/Animations.cs
  4. 26
      src/Avalonia.Base/Animation/CompositionAnimations/CompositionAnimation.cs
  5. 97
      src/Avalonia.Base/Animation/CompositionAnimations/ExplicitAnimationCollection.cs
  6. 24
      src/Avalonia.Base/Animation/IAnimation.cs
  7. 2
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  8. 11
      src/Avalonia.Base/Styling/StyleInstance.cs
  9. 44
      src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml
  10. 9
      src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml

1
src/Avalonia.Base/Animation/Animatable.cs

@ -5,7 +5,6 @@ using System.Collections.Specialized;
using System.Linq;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Rendering.Composition;
#nullable enable

12
src/Avalonia.Base/Animation/Animation.cs

@ -13,7 +13,7 @@ namespace Avalonia.Animation
/// <summary>
/// Tracks the progress of an animation.
/// </summary>
public sealed partial class Animation : AvaloniaObject, IAnimation
public sealed partial class Animation : AvaloniaObject, IPropertyAnimation
{
/// <summary>
/// Defines the <see cref="Duration"/> property.
@ -268,10 +268,10 @@ namespace Avalonia.Animation
return (newAnimatorInstances, subscriptions);
}
IDisposable IAnimation.Apply(Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete)
IDisposable IPropertyAnimation.Apply(Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete)
=> Apply(control, clock, match, onComplete);
/// <inheritdoc cref="IAnimation.Apply"/>
/// <inheritdoc cref="IPropertyAnimation.Apply"/>
internal IDisposable Apply(Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete)
{
var (animators, subscriptions) = InterpretKeyframes(control);
@ -320,16 +320,16 @@ namespace Avalonia.Animation
public Task RunAsync(Animatable control, CancellationToken cancellationToken = default) =>
RunAsync(control, null, cancellationToken);
/// <inheritdoc cref="IAnimation.RunAsync"/>
/// <inheritdoc cref="IPropertyAnimation.RunAsync"/>
internal Task RunAsync(Animatable control, IClock? clock)
{
return RunAsync(control, clock, default);
}
Task IAnimation.RunAsync(Animatable control, IClock? clock, CancellationToken cancellationToken)
Task IPropertyAnimation.RunAsync(Animatable control, IClock? clock, CancellationToken cancellationToken)
=> RunAsync(control, clock, cancellationToken);
/// <inheritdoc cref="IAnimation.RunAsync"/>
/// <inheritdoc cref="IPropertyAnimation.RunAsync"/>
internal Task RunAsync(Animatable control, IClock? clock, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)

129
src/Avalonia.Base/Animation/CompositionAnimations/Animations.cs

@ -1,129 +0,0 @@
using Avalonia.Collections;
using Avalonia.Rendering.Composition;
namespace Avalonia.Animation
{
public static class Animations
{
// public static readonly AttachedProperty<ImplicitAnimationCollection?> ImplicitAnimationsProperty =
// AvaloniaProperty.RegisterAttached<Visual, ImplicitAnimationCollection?>(
// "ImplicitAnimations",
// typeof(Animations));
public static readonly AttachedProperty<ExplicitAnimationCollection?> ExplicitAnimationsProperty =
AvaloniaProperty.RegisterAttached<Visual, ExplicitAnimationCollection?>(
"ExplicitAnimations",
typeof(Animations));
//
// public static void SetImplicitAnimations(Visual visual, ImplicitAnimationCollection? value)
// {
// visual?.SetValue(ImplicitAnimationsProperty, value);
// }
public static void SetExplicitAnimations(Visual visual, ExplicitAnimationCollection? value)
{
visual?.SetValue(ExplicitAnimationsProperty, value);
}
// public static ImplicitAnimationCollection? GetImplicitAnimations(Visual visual) => visual.GetValue(ImplicitAnimationsProperty);
public static ExplicitAnimationCollection? GetExplicitAnimations(Visual visual) => visual.GetValue(ExplicitAnimationsProperty);
public static readonly AttachedProperty<bool> EnableAnimationsProperty =
AvaloniaProperty.RegisterAttached<Visual, bool>(
"EnableAnimations",
typeof(Animations), defaultValue: true, inherits: true);
public static void SetEnableAnimations(Visual visual, bool value)
{
visual?.SetValue(EnableAnimationsProperty, value);
}
public static bool GetEnableAnimations(Visual visual) => visual.GetValue(EnableAnimationsProperty);
static Animations()
{
//ImplicitAnimationsProperty.Changed.AddClassHandler<Visual>(OnAnimationsPropertyChanged);
ExplicitAnimationsProperty.Changed.AddClassHandler<Visual>(OnAnimationsPropertyChanged);
EnableAnimationsProperty.Changed.AddClassHandler<Visual>(OnAnimationsPropertyChanged);
}
private static void OnAnimationsPropertyChanged(Visual visual, AvaloniaPropertyChangedEventArgs args)
{
void AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
Invalidate(visual);
}
// if (args.Property == ImplicitAnimationsProperty)
// {
// // if (args.OldValue is ImplicitAnimationCollection oldImplicitSet)
// // {
// // oldImplicitSet.Invalidated -= (s, e) => UpdateAnimations(s as AvaloniaList<CompositionAnimation>, visual);
// // visual.AttachedToVisualTree -= AttachedToVisualTree;
// // }
// //
// //
// // if (args.NewValue is ImplicitAnimationCollection newImplicitSet)
// // {
// // newImplicitSet.Invalidated += (s, e) => UpdateAnimations(s as AvaloniaList<CompositionAnimation>, visual);
// // UpdateAnimations(newImplicitSet, visual);
// // visual.AttachedToVisualTree += AttachedToVisualTree;
// // }
// }
// else
if (args.Property == ExplicitAnimationsProperty)
{
if (args.OldValue is ExplicitAnimationCollection oldExplicitSet)
{
oldExplicitSet.Detach(visual);
oldExplicitSet.Invalidated -= (s, e) => UpdateAnimations(s as AvaloniaList<CompositionAnimation>, visual);
visual.AttachedToVisualTree -= AttachedToVisualTree;
}
if (args.NewValue is ExplicitAnimationCollection newExplicitSet)
{
newExplicitSet.Invalidated += (s, e) => UpdateAnimations(s as AvaloniaList<CompositionAnimation>, visual);
UpdateAnimations(newExplicitSet, visual);
visual.AttachedToVisualTree += AttachedToVisualTree;
}
}
else if (args.Property == EnableAnimationsProperty)
{
Invalidate(visual);
}
}
private static void Invalidate(Visual? visual)
{
if (visual == null)
return;
//UpdateAnimations(GetImplicitAnimations(visual), visual);
var explicitAnimationCollection = GetExplicitAnimations(visual);
explicitAnimationCollection?.Detach(visual);
UpdateAnimations(explicitAnimationCollection, visual);
}
private static void UpdateAnimations(AvaloniaList<CompositionAnimation>? collections, Visual visual)
{
// if (collections is ImplicitAnimationCollection implicitCollection)
// {
// if (ElementComposition.GetElementVisual(visual) is { } compositionVisual)
// {
// compositionVisual.ImplicitAnimations =
// GetEnableAnimations(visual) ? implicitCollection.GetAnimations(visual) : null;
// }
// }
// else
if (collections is ExplicitAnimationCollection explicitCollection)
{
if (ElementComposition.GetElementVisual(visual) is { } compositionVisual && GetEnableAnimations(visual))
{
explicitCollection.Attach(visual);
}
}
}
}
}

26
src/Avalonia.Base/Animation/CompositionAnimations/CompositionAnimation.cs

@ -5,15 +5,14 @@ using Avalonia.Animation.Easings;
using Avalonia.Collections;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Reactive;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Animations;
namespace Avalonia.Animation
{
public abstract class CompositionAnimation : AvaloniaObject, ICompositionTransition
public abstract class CompositionAnimation : AvaloniaObject, ICompositionTransition, ICompositionAnimation
{
public event EventHandler? AnimationInvalidated;
public static readonly StyledProperty<bool> IsEnabledProperty = AvaloniaProperty.Register<CompositionAnimation, bool>(
nameof(IsEnabled), defaultValue: true);
@ -35,6 +34,8 @@ namespace Avalonia.Animation
private Visual? _attachedVisual;
private KeyFrameAnimation? _animation;
public event EventHandler? AnimationInvalidated;
[Content]
public AvaloniaList<CompositionKeyFrame> Children { get; } = new();
@ -74,6 +75,25 @@ namespace Avalonia.Animation
set => SetValue(StopBehaviorProperty, value);
}
IDisposable ICompositionAnimation.Apply(Visual parent)
{
var subscription = IsEnabledProperty.Changed
.Where(args => args.GetNewValue<bool>())
.Subscribe(_ =>
{
if (GetCompositionAnimation(parent) is KeyFrameAnimation { Target: not null } newAnimation)
{
Attach(parent, newAnimation);
}
});
return Disposable.Create(() =>
{
subscription.Dispose();
Detach();
});
}
Rendering.Composition.Animations.CompositionAnimation? ICompositionTransition.GetCompositionAnimation(Visual parent)
{
return !IsEnabled ? null : GetCompositionAnimation(parent);

97
src/Avalonia.Base/Animation/CompositionAnimations/ExplicitAnimationCollection.cs

@ -1,97 +0,0 @@
using System;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Animations;
namespace Avalonia.Animation
{
public class ExplicitAnimationCollection : AvaloniaList<CompositionAnimation>
{
internal event EventHandler? Invalidated;
private Visual? _visual;
public ExplicitAnimationCollection()
{
this.CollectionChanged += OnCollectionChanged;
}
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems is { } oldItems)
{
foreach (var oldItem in oldItems)
{
if (oldItem is CompositionAnimation explicitAnimation)
{
explicitAnimation.Detach();
explicitAnimation.AnimationInvalidated -= ResetAnimation;
}
}
}
if (e.NewItems is { } newItems)
{
foreach (var newItem in newItems)
{
if (newItem is CompositionAnimation explicitAnimation)
{
explicitAnimation.AnimationInvalidated += ResetAnimation;
}
}
}
OnAnimationInvalidated();
}
private void ResetAnimation(object? sender, EventArgs e)
{
if(sender is CompositionAnimation animation && _visual is { } visual)
{
animation.Detach();
AttachAnimation(visual, animation);
}
}
private void OnAnimationInvalidated()
{
Invalidated?.Invoke(this, EventArgs.Empty);
}
internal void Detach(Visual visual)
{
foreach (var animation in this)
{
animation.Detach();
}
}
internal void Attach(Visual visual)
{
_visual = visual;
var compositionVisual = ElementComposition.GetElementVisual(visual);
if (compositionVisual == null)
return;
Detach(visual);
foreach (var animation in this)
{
AttachAnimation(visual, animation);
}
}
private static void AttachAnimation(Visual visual, CompositionAnimation animation)
{
// if (animation.GetCompositionAnimationInternal(visual) is KeyFrameAnimation newAnimation
// && newAnimation.Target is { } target)
// {
// animation.Attach(visual, newAnimation);
// }
}
}
}

24
src/Avalonia.Base/Animation/IAnimation.cs

@ -8,8 +8,30 @@ namespace Avalonia.Animation
/// <summary>
/// Interface for Animation objects
/// </summary>
[NotClientImplementable]
[NotClientImplementable, PrivateApi]
public interface IAnimation
{
}
[NotClientImplementable, PrivateApi]
public interface ICompositionAnimation : IAnimation
{
/// <summary>
/// Occurs when the transition is invalidated and needs to be re-applied.
/// </summary>
event EventHandler? AnimationInvalidated;
/// <summary>
/// Apply the animation to the specified visual and return a disposable to remove it.
/// </summary>
IDisposable Apply(Visual parent);
}
/// <summary>
/// Interface for Animation objects
/// </summary>
[NotClientImplementable, PrivateApi]
public interface IPropertyAnimation : IAnimation
{
/// <summary>
/// Apply the animation to the specified control and run it when <paramref name="match" /> produces <c>true</c>.

2
src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs

@ -84,7 +84,7 @@ namespace Avalonia.Input
protected override void PointerPressed(PointerPressedEventArgs e)
{
if (Target != null && Target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
if (Target != null && Target is Visual visual)
{
var position = e.GetPosition(visual);

11
src/Avalonia.Base/Styling/StyleInstance.cs

@ -72,7 +72,16 @@ namespace Avalonia.Styling
_animationTrigger ??= new LightweightSubject<bool>();
_animationApplyDisposables ??= new List<IDisposable>();
foreach (var animation in _animations)
_animationApplyDisposables.Add(animation.Apply(animatable, null, _animationTrigger));
{
if (animation is IPropertyAnimation propertyAnimation)
{
_animationApplyDisposables.Add(propertyAnimation.Apply(animatable, null, _animationTrigger));
}
else if (animation is ICompositionAnimation compositionAnimation && animatable is Visual visual)
{
_animationApplyDisposables.Add(compositionAnimation.Apply(visual));
}
}
if (_activator is null)
_animationTrigger.OnNext(true);

44
src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml

@ -18,27 +18,29 @@
Data="M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z"
Width="{DynamicResource RefreshVisualizerIndicatorSize}"
Height="{DynamicResource RefreshVisualizerIndicatorSize}">
<Animations.ExplicitAnimations>
<ExplicitAnimationCollection>
<RotationAngleCompositionAnimation IterationBehavior="Forever"
StopBehavior="LeaveCurrentValue"
IsEnabled="{Binding $parent[RefreshVisualizer].TemplateSettings.IsRotationAnimating}"
Duration="0:0:0.5">
<CompositionKeyFrame NormalizedProgressKey="0"
Value="{Binding $parent[RefreshVisualizer].TemplateSettings.RotationStartAngle}"
Easing="LinearEasing" />
<CompositionKeyFrame NormalizedProgressKey="1"
Value="{Binding $parent[RefreshVisualizer].TemplateSettings.RotationEndAngle}"
Easing="LinearEasing" />
</RotationAngleCompositionAnimation>
<ScaleCompositionAnimation Duration="0:0:0.3" IsEnabled="{Binding $parent[RefreshVisualizer].TemplateSettings.TriggerScaleAnimation}" >
<CompositionKeyFrame NormalizedProgressKey="0.5"
Value="{Binding $parent[RefreshVisualizer].TemplateSettings.ScaleStartValue}" />
<CompositionKeyFrame NormalizedProgressKey="1"
Value="{Binding $parent[RefreshVisualizer].TemplateSettings.ScaleEndValue}" />
</ScaleCompositionAnimation>
</ExplicitAnimationCollection>
</Animations.ExplicitAnimations>
<PathIcon.Styles>
<Style Selector="PathIcon">
<Style.Animations>
<RotationAngleCompositionAnimation IterationBehavior="Forever"
StopBehavior="LeaveCurrentValue"
IsEnabled="{Binding $parent[RefreshVisualizer].TemplateSettings.IsRotationAnimating}"
Duration="0:0:0.5">
<CompositionKeyFrame NormalizedProgressKey="0"
Value="{Binding $parent[RefreshVisualizer].TemplateSettings.RotationStartAngle}"
Easing="LinearEasing" />
<CompositionKeyFrame NormalizedProgressKey="1"
Value="{Binding $parent[RefreshVisualizer].TemplateSettings.RotationEndAngle}"
Easing="LinearEasing" />
</RotationAngleCompositionAnimation>
<ScaleCompositionAnimation Duration="0:0:0.3" IsEnabled="{Binding $parent[RefreshVisualizer].TemplateSettings.TriggerScaleAnimation}" >
<CompositionKeyFrame NormalizedProgressKey="0.5"
Value="{Binding $parent[RefreshVisualizer].TemplateSettings.ScaleStartValue}" />
<CompositionKeyFrame NormalizedProgressKey="1"
Value="{Binding $parent[RefreshVisualizer].TemplateSettings.ScaleEndValue}" />
</ScaleCompositionAnimation>
</Style.Animations>
</Style>
</PathIcon.Styles>
</PathIcon>
</Template>
</Setter>

9
src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml

@ -1,6 +1,7 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal">
<ControlTheme x:Key="{x:Type RefreshVisualizer}"
TargetType="RefreshVisualizer">
<Setter Property="IsTabStop"
@ -20,8 +21,8 @@
Data="M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z"
Width="24"
Height="24">
<Animations.ExplicitAnimations>
<ExplicitAnimationCollection>
<PathIcon.Styles>
<Style Selector="PathIcon">
<RotationAngleCompositionAnimation IterationBehavior="Forever"
StopBehavior="LeaveCurrentValue"
IsEnabled="{Binding $parent[RefreshVisualizer].TemplateSettings.IsRotationAnimating}"
@ -39,8 +40,8 @@
<CompositionKeyFrame NormalizedProgressKey="1"
Value="{Binding $parent[RefreshVisualizer].TemplateSettings.ScaleEndValue}" />
</ScaleCompositionAnimation>
</ExplicitAnimationCollection>
</Animations.ExplicitAnimations>
</Style>
</PathIcon.Styles>
</PathIcon>
</Template>
</Setter>

Loading…
Cancel
Save