diff --git a/src/Avalonia.Animation/AnimatorDrivenTransition.cs b/src/Avalonia.Animation/AnimatorDrivenTransition.cs new file mode 100644 index 0000000000..eb4d6f978d --- /dev/null +++ b/src/Avalonia.Animation/AnimatorDrivenTransition.cs @@ -0,0 +1,15 @@ +using System; +using Avalonia.Animation.Animators; + +namespace Avalonia.Animation +{ + public class AnimatorDrivenTransition : Transition where TAnimator : Animator, new() + { + private static readonly TAnimator s_animator = new TAnimator(); + + public override IObservable DoTransition(IObservable progress, T oldValue, T newValue) + { + return new AnimatorTransitionObservable(s_animator, progress, oldValue, newValue); + } + } +} diff --git a/src/Avalonia.Animation/AnimatorTransitionObservable.cs b/src/Avalonia.Animation/AnimatorTransitionObservable.cs new file mode 100644 index 0000000000..4f888e1f15 --- /dev/null +++ b/src/Avalonia.Animation/AnimatorTransitionObservable.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia.Animation.Animators; + +namespace Avalonia.Animation +{ + public class AnimatorTransitionObservable : TransitionObservableBase where TAnimator : Animator + { + private readonly TAnimator _animator; + private readonly T _oldValue; + private readonly T _newValue; + + public AnimatorTransitionObservable(TAnimator animator, IObservable progress, T oldValue, T newValue) : base(progress) + { + _animator = animator; + _oldValue = oldValue; + _newValue = newValue; + } + + protected override T ProduceValue(double progress) + { + return _animator.Interpolate(progress, _oldValue, _newValue); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Animation/Transition`1.cs b/src/Avalonia.Animation/Transition.cs similarity index 96% rename from src/Avalonia.Animation/Transition`1.cs rename to src/Avalonia.Animation/Transition.cs index 4542a137e5..4115c95c0f 100644 --- a/src/Avalonia.Animation/Transition`1.cs +++ b/src/Avalonia.Animation/Transition.cs @@ -1,7 +1,5 @@ -using System; -using System.Reactive.Linq; +using System; using Avalonia.Animation.Easings; -using Avalonia.Animation.Utils; namespace Avalonia.Animation { @@ -56,4 +54,4 @@ namespace Avalonia.Animation return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation); } } -} +} \ No newline at end of file diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index 5184341324..58dd71e8a5 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -1,8 +1,4 @@ -using Avalonia.Metadata; using System; -using System.Reactive.Linq; -using Avalonia.Animation.Easings; -using Avalonia.Animation.Utils; using Avalonia.Reactive; using Avalonia.Utilities; @@ -11,7 +7,7 @@ namespace Avalonia.Animation /// /// Handles the timing and lifetime of a . /// - internal class TransitionInstance : SingleSubscriberObservableBase + internal class TransitionInstance : SingleSubscriberObservableBase, IObserver { private IDisposable _timerSubscription; private TimeSpan _delay; @@ -76,8 +72,23 @@ namespace Avalonia.Animation protected override void Subscribed() { _clock = new Clock(_baseClock); - _timerSubscription = _clock.Subscribe(TimerTick); + _timerSubscription = _clock.Subscribe(this); PublishNext(0.0d); } + + void IObserver.OnCompleted() + { + PublishCompleted(); + } + + void IObserver.OnError(Exception error) + { + PublishError(error); + } + + void IObserver.OnNext(TimeSpan value) + { + TimerTick(value); + } } } diff --git a/src/Avalonia.Animation/TransitionObservableBase.cs b/src/Avalonia.Animation/TransitionObservableBase.cs new file mode 100644 index 0000000000..e18b6859f5 --- /dev/null +++ b/src/Avalonia.Animation/TransitionObservableBase.cs @@ -0,0 +1,45 @@ +using System; +using Avalonia.Reactive; + +#nullable enable + +namespace Avalonia.Animation +{ + public abstract class TransitionObservableBase : SingleSubscriberObservableBase, IObserver + { + private readonly IObservable _progress; + private IDisposable? _progressSubscription; + + protected TransitionObservableBase(IObservable progress) + { + _progress = progress; + } + + protected override void Unsubscribed() + { + _progressSubscription?.Dispose(); + } + + protected override void Subscribed() + { + _progressSubscription = _progress.Subscribe(this); + } + + protected abstract T ProduceValue(double progress); + + void IObserver.OnCompleted() + { + PublishCompleted(); + } + + void IObserver.OnError(Exception error) + { + PublishError(error); + } + + void IObserver.OnNext(double value) + { + PublishNext(ProduceValue(value)); + } + } +} diff --git a/src/Avalonia.Animation/Transitions/DoubleTransition.cs b/src/Avalonia.Animation/Transitions/DoubleTransition.cs index d5bb1aac20..28f630eb98 100644 --- a/src/Avalonia.Animation/Transitions/DoubleTransition.cs +++ b/src/Avalonia.Animation/Transitions/DoubleTransition.cs @@ -1,6 +1,5 @@ using System; using System.Reactive.Linq; - using Avalonia.Animation.Animators; namespace Avalonia.Animation @@ -8,7 +7,11 @@ namespace Avalonia.Animation /// /// Transition class that handles with types. /// - public class DoubleTransition : Transition + public class DoubleTransition : AnimatorDrivenTransition + { + } + + public class DoubleTransitionOld : Transition { private static readonly DoubleAnimator s_animator = new DoubleAnimator(); diff --git a/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs index 104acb71ad..ca1528a561 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs @@ -1,28 +1,26 @@ using System; -using System.Reactive.Linq; using Avalonia.Animation.Animators; using Avalonia.Media; +using Avalonia.Media.Transformation; + +#nullable enable namespace Avalonia.Animation { public class TransformOperationsTransition : Transition { - private static readonly TransformOperationsAnimator _operationsAnimator = new TransformOperationsAnimator(); + private static readonly TransformOperationsAnimator s_operationsAnimator = new TransformOperationsAnimator(); - public override IObservable DoTransition(IObservable progress, + public override IObservable DoTransition( + IObservable progress, ITransform oldValue, ITransform newValue) { var oldTransform = TransformOperationsAnimator.EnsureOperations(oldValue); var newTransform = TransformOperationsAnimator.EnsureOperations(newValue); - return progress - .Select(p => - { - var f = Easing.Ease(p); - - return _operationsAnimator.Interpolate(f, oldTransform, newTransform); - }); + return new AnimatorTransitionObservable( + s_operationsAnimator, progress, oldTransform, newTransform); } } } diff --git a/tests/Avalonia.Benchmarks/Animations/TransitionBenchmark.cs b/tests/Avalonia.Benchmarks/Animations/TransitionBenchmark.cs new file mode 100644 index 0000000000..16aad0c21b --- /dev/null +++ b/tests/Avalonia.Benchmarks/Animations/TransitionBenchmark.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reactive.Subjects; +using System.Runtime.CompilerServices; +using Avalonia.Animation; +using Avalonia.Layout; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Animations +{ + [MemoryDiagnoser] + public class TransitionBenchmark + { + private readonly DoubleTransition _transition; + private readonly DoubleTransitionOld _oldTransition; + private readonly int _frameCount; + private readonly Subject _timeProducer; + private readonly List _producedValues; + + public TransitionBenchmark() + { + _frameCount = 100; + + _oldTransition = new DoubleTransitionOld + { + Duration = TimeSpan.FromMilliseconds(_frameCount), Property = Layoutable.WidthProperty + }; + + _transition = new DoubleTransition + { + Duration = TimeSpan.FromMilliseconds(_frameCount), Property = Layoutable.WidthProperty + }; + + _timeProducer = new Subject(); + _producedValues = new List(_frameCount); + } + + [Benchmark(Baseline = true)] + [MethodImpl(MethodImplOptions.NoInlining)] + public void OldTransition() + { + TransitionCommon(_oldTransition); + } + + [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] + public void NewTransition() + { + TransitionCommon(_transition); + } + + private void TransitionCommon(Transition transition) + { + var transitionObs = transition.DoTransition(_timeProducer, 0, 1); + + _producedValues.Clear(); + + using var transitionSub = transitionObs.Subscribe(new AddValueObserver(_producedValues)); + + for (int i = 0; i < _frameCount; i++) + { + _timeProducer.OnNext(TimeSpan.FromMilliseconds(i).TotalSeconds); + } + + Debug.Assert(_producedValues.Count == _frameCount); + } + + private class AddValueObserver : IObserver + { + private readonly List _values; + + public AddValueObserver(List values) + { + _values = values; + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } + + public void OnNext(double value) + { + _values.Add(value); + } + } + } +}