From 9e49a4c5c4c706bb6a7f1af7533a002e4140a1e9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 29 Jan 2026 23:53:54 -0800 Subject: [PATCH] Implement CompositionAnimation ICompositionAnimation support --- src/Avalonia.Base/Animation/Animatable.cs | 1 - src/Avalonia.Base/Animation/Animation.cs | 12 +- .../CompositionAnimations/Animations.cs | 129 ------------------ .../CompositionAnimation.cs | 26 +++- .../ExplicitAnimationCollection.cs | 97 ------------- src/Avalonia.Base/Animation/IAnimation.cs | 24 +++- .../PullGestureRecognizer.cs | 2 +- src/Avalonia.Base/Styling/StyleInstance.cs | 11 +- .../Controls/RefreshVisualizer.xaml | 44 +++--- .../Controls/RefreshVisualizer.xaml | 9 +- 10 files changed, 91 insertions(+), 264 deletions(-) delete mode 100644 src/Avalonia.Base/Animation/CompositionAnimations/Animations.cs delete mode 100644 src/Avalonia.Base/Animation/CompositionAnimations/ExplicitAnimationCollection.cs diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index 13f637b6b1..816feed34e 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/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 diff --git a/src/Avalonia.Base/Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs index 0391280ede..6a9ac30fed 100644 --- a/src/Avalonia.Base/Animation/Animation.cs +++ b/src/Avalonia.Base/Animation/Animation.cs @@ -13,7 +13,7 @@ namespace Avalonia.Animation /// /// Tracks the progress of an animation. /// - public sealed partial class Animation : AvaloniaObject, IAnimation + public sealed partial class Animation : AvaloniaObject, IPropertyAnimation { /// /// Defines the property. @@ -268,10 +268,10 @@ namespace Avalonia.Animation return (newAnimatorInstances, subscriptions); } - IDisposable IAnimation.Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete) + IDisposable IPropertyAnimation.Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete) => Apply(control, clock, match, onComplete); - /// + /// internal IDisposable Apply(Animatable control, IClock? clock, IObservable 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); - /// + /// 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); - /// + /// internal Task RunAsync(Animatable control, IClock? clock, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) diff --git a/src/Avalonia.Base/Animation/CompositionAnimations/Animations.cs b/src/Avalonia.Base/Animation/CompositionAnimations/Animations.cs deleted file mode 100644 index 576cb60394..0000000000 --- a/src/Avalonia.Base/Animation/CompositionAnimations/Animations.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Avalonia.Collections; -using Avalonia.Rendering.Composition; - -namespace Avalonia.Animation -{ - public static class Animations - { - // public static readonly AttachedProperty ImplicitAnimationsProperty = - // AvaloniaProperty.RegisterAttached( - // "ImplicitAnimations", - // typeof(Animations)); - - public static readonly AttachedProperty ExplicitAnimationsProperty = - AvaloniaProperty.RegisterAttached( - "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 EnableAnimationsProperty = - AvaloniaProperty.RegisterAttached( - "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(OnAnimationsPropertyChanged); - ExplicitAnimationsProperty.Changed.AddClassHandler(OnAnimationsPropertyChanged); - EnableAnimationsProperty.Changed.AddClassHandler(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, visual); - // // visual.AttachedToVisualTree -= AttachedToVisualTree; - // // } - // // - // // - // // if (args.NewValue is ImplicitAnimationCollection newImplicitSet) - // // { - // // newImplicitSet.Invalidated += (s, e) => UpdateAnimations(s as AvaloniaList, 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, visual); - visual.AttachedToVisualTree -= AttachedToVisualTree; - } - - - if (args.NewValue is ExplicitAnimationCollection newExplicitSet) - { - newExplicitSet.Invalidated += (s, e) => UpdateAnimations(s as AvaloniaList, 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? 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); - } - } - } - } -} diff --git a/src/Avalonia.Base/Animation/CompositionAnimations/CompositionAnimation.cs b/src/Avalonia.Base/Animation/CompositionAnimations/CompositionAnimation.cs index afe87b77bd..93effb43f2 100644 --- a/src/Avalonia.Base/Animation/CompositionAnimations/CompositionAnimation.cs +++ b/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 IsEnabledProperty = AvaloniaProperty.Register( nameof(IsEnabled), defaultValue: true); @@ -35,6 +34,8 @@ namespace Avalonia.Animation private Visual? _attachedVisual; private KeyFrameAnimation? _animation; + public event EventHandler? AnimationInvalidated; + [Content] public AvaloniaList 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()) + .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); diff --git a/src/Avalonia.Base/Animation/CompositionAnimations/ExplicitAnimationCollection.cs b/src/Avalonia.Base/Animation/CompositionAnimations/ExplicitAnimationCollection.cs deleted file mode 100644 index 344c0552b0..0000000000 --- a/src/Avalonia.Base/Animation/CompositionAnimations/ExplicitAnimationCollection.cs +++ /dev/null @@ -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 - { - 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); - // } - } - } -} diff --git a/src/Avalonia.Base/Animation/IAnimation.cs b/src/Avalonia.Base/Animation/IAnimation.cs index a5b8b75b12..f1c6faa87c 100644 --- a/src/Avalonia.Base/Animation/IAnimation.cs +++ b/src/Avalonia.Base/Animation/IAnimation.cs @@ -8,8 +8,30 @@ namespace Avalonia.Animation /// /// Interface for Animation objects /// - [NotClientImplementable] + [NotClientImplementable, PrivateApi] public interface IAnimation + { + } + + [NotClientImplementable, PrivateApi] + public interface ICompositionAnimation : IAnimation + { + /// + /// Occurs when the transition is invalidated and needs to be re-applied. + /// + event EventHandler? AnimationInvalidated; + + /// + /// Apply the animation to the specified visual and return a disposable to remove it. + /// + IDisposable Apply(Visual parent); + } + + /// + /// Interface for Animation objects + /// + [NotClientImplementable, PrivateApi] + public interface IPropertyAnimation : IAnimation { /// /// Apply the animation to the specified control and run it when produces true. diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs index ee7248cd63..b2277a3b17 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs +++ b/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); diff --git a/src/Avalonia.Base/Styling/StyleInstance.cs b/src/Avalonia.Base/Styling/StyleInstance.cs index c397aef8c6..184ce77286 100644 --- a/src/Avalonia.Base/Styling/StyleInstance.cs +++ b/src/Avalonia.Base/Styling/StyleInstance.cs @@ -72,7 +72,16 @@ namespace Avalonia.Styling _animationTrigger ??= new LightweightSubject(); _animationApplyDisposables ??= new List(); 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); diff --git a/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml index a7cee77aaf..eb495792d4 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml +++ b/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}"> - - - - - - - - - - - - + + + diff --git a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml index 8728061b88..29f0d95e76 100644 --- a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml +++ b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml @@ -1,6 +1,7 @@ + - - + + +