diff --git a/src/Avalonia.Animation/AnimatorDrivenTransition.cs b/src/Avalonia.Animation/AnimatorDrivenTransition.cs new file mode 100644 index 0000000000..88c8ec5ec1 --- /dev/null +++ b/src/Avalonia.Animation/AnimatorDrivenTransition.cs @@ -0,0 +1,20 @@ +using System; +using Avalonia.Animation.Animators; + +namespace Avalonia.Animation +{ + /// + /// using an to transition between values. + /// + /// Type of the transitioned value. + /// Type of the animator. + public abstract 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, Easing, oldValue, newValue); + } + } +} diff --git a/src/Avalonia.Animation/AnimatorTransitionObservable.cs b/src/Avalonia.Animation/AnimatorTransitionObservable.cs new file mode 100644 index 0000000000..3cc185179b --- /dev/null +++ b/src/Avalonia.Animation/AnimatorTransitionObservable.cs @@ -0,0 +1,32 @@ +using System; +using Avalonia.Animation.Animators; +using Avalonia.Animation.Easings; + +namespace Avalonia.Animation +{ + /// + /// Transition observable based on an producing a value. + /// + /// Type of the transitioned value. + /// Type of the animator. + public class AnimatorTransitionObservable : TransitionObservableBase where TAnimator : Animator + { + private readonly TAnimator _animator; + private readonly Easing _easing; + private readonly T _oldValue; + private readonly T _newValue; + + public AnimatorTransitionObservable(TAnimator animator, IObservable progress, Easing easing, T oldValue, T newValue) : base(progress, easing) + { + _animator = animator; + _easing = easing; + _oldValue = oldValue; + _newValue = newValue; + } + + protected override T ProduceValue(double progress) + { + return _animator.Interpolate(progress, _oldValue, _newValue); + } + } +} diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs index bea6c75982..5c2b7ce0dd 100644 --- a/src/Avalonia.Animation/Clock.cs +++ b/src/Avalonia.Animation/Clock.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Reactive.Linq; -using System.Text; -using Avalonia.Reactive; namespace Avalonia.Animation { @@ -10,10 +6,9 @@ namespace Avalonia.Animation { public static IClock GlobalClock => AvaloniaLocator.Current.GetService(); - private IDisposable _parentSubscription; + private readonly IDisposable _parentSubscription; - public Clock() - :this(GlobalClock) + public Clock() : this(GlobalClock) { } diff --git a/src/Avalonia.Animation/ClockBase.cs b/src/Avalonia.Animation/ClockBase.cs index a2b29e728e..c6e5a363be 100644 --- a/src/Avalonia.Animation/ClockBase.cs +++ b/src/Avalonia.Animation/ClockBase.cs @@ -1,16 +1,11 @@ using System; -using System.Collections.Generic; -using System.Reactive.Linq; -using System.Text; using Avalonia.Reactive; namespace Avalonia.Animation { public class ClockBase : IClock { - private ClockObservable _observable; - - private IObservable _connectedObservable; + private readonly ClockObservable _observable; private TimeSpan? _previousTime; private TimeSpan _internalTime; @@ -18,7 +13,6 @@ namespace Avalonia.Animation protected ClockBase() { _observable = new ClockObservable(); - _connectedObservable = _observable.Publish().RefCount(); } protected bool HasSubscriptions => _observable.HasSubscriptions; @@ -58,10 +52,10 @@ namespace Avalonia.Animation public IDisposable Subscribe(IObserver observer) { - return _connectedObservable.Subscribe(observer); + return _observable.Subscribe(observer); } - private class ClockObservable : LightweightObservableBase + private sealed class ClockObservable : LightweightObservableBase { public bool HasSubscriptions { get; private set; } public void Pulse(TimeSpan time) => PublishNext(time); 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..b522d1961e 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -1,8 +1,5 @@ -using Avalonia.Metadata; using System; -using System.Reactive.Linq; -using Avalonia.Animation.Easings; -using Avalonia.Animation.Utils; +using System.Runtime.ExceptionServices; using Avalonia.Reactive; using Avalonia.Utilities; @@ -11,13 +8,13 @@ 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; private TimeSpan _duration; private readonly IClock _baseClock; - private IClock _clock; + private TransitionClock _clock; public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration) { @@ -75,9 +72,56 @@ namespace Avalonia.Animation protected override void Subscribed() { - _clock = new Clock(_baseClock); - _timerSubscription = _clock.Subscribe(TimerTick); + _clock = new TransitionClock(_baseClock); + _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); + } + + /// + /// TODO: This clock is still fairly expensive due to implementation. + /// + private sealed class TransitionClock : ClockBase, IObserver + { + private readonly IDisposable _parentSubscription; + + public TransitionClock(IClock parent) + { + _parentSubscription = parent.Subscribe(this); + } + + protected override void Stop() + { + _parentSubscription.Dispose(); + } + + void IObserver.OnNext(TimeSpan value) + { + Pulse(value); + } + + void IObserver.OnCompleted() + { + } + + void IObserver.OnError(Exception error) + { + ExceptionDispatchInfo.Capture(error).Throw(); + } + } } } diff --git a/src/Avalonia.Animation/TransitionObservableBase.cs b/src/Avalonia.Animation/TransitionObservableBase.cs new file mode 100644 index 0000000000..c4ac803135 --- /dev/null +++ b/src/Avalonia.Animation/TransitionObservableBase.cs @@ -0,0 +1,58 @@ +using System; +using Avalonia.Animation.Easings; +using Avalonia.Reactive; + +#nullable enable + +namespace Avalonia.Animation +{ + /// + /// Provides base for observables implementing transitions. + /// + /// Type of the transitioned value. + public abstract class TransitionObservableBase : SingleSubscriberObservableBase, IObserver + { + private readonly Easing _easing; + private readonly IObservable _progress; + private IDisposable? _progressSubscription; + + protected TransitionObservableBase(IObservable progress, Easing easing) + { + _progress = progress; + _easing = easing; + } + + /// + /// Produces value at given progress time point. + /// + /// Transition progress. + protected abstract T ProduceValue(double progress); + + protected override void Subscribed() + { + _progressSubscription = _progress.Subscribe(this); + } + + protected override void Unsubscribed() + { + _progressSubscription?.Dispose(); + } + + void IObserver.OnCompleted() + { + PublishCompleted(); + } + + void IObserver.OnError(Exception error) + { + PublishError(error); + } + + void IObserver.OnNext(double value) + { + double progress = _easing.Ease(value); + + PublishNext(ProduceValue(progress)); + } + } +} diff --git a/src/Avalonia.Animation/Transitions/DoubleTransition.cs b/src/Avalonia.Animation/Transitions/DoubleTransition.cs index d5bb1aac20..7232d87863 100644 --- a/src/Avalonia.Animation/Transitions/DoubleTransition.cs +++ b/src/Avalonia.Animation/Transitions/DoubleTransition.cs @@ -1,6 +1,3 @@ -using System; -using System.Reactive.Linq; - using Avalonia.Animation.Animators; namespace Avalonia.Animation @@ -8,15 +5,7 @@ namespace Avalonia.Animation /// /// Transition class that handles with types. /// - public class DoubleTransition : Transition + public class DoubleTransition : AnimatorDrivenTransition { - private static readonly DoubleAnimator s_animator = new DoubleAnimator(); - - /// - public override IObservable DoTransition(IObservable progress, double oldValue, double newValue) - { - return progress - .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue)); - } } } diff --git a/src/Avalonia.Animation/Transitions/FloatTransition.cs b/src/Avalonia.Animation/Transitions/FloatTransition.cs index 37b644fa96..a96db8ba5b 100644 --- a/src/Avalonia.Animation/Transitions/FloatTransition.cs +++ b/src/Avalonia.Animation/Transitions/FloatTransition.cs @@ -1,6 +1,3 @@ -using System; -using System.Reactive.Linq; - using Avalonia.Animation.Animators; namespace Avalonia.Animation @@ -8,15 +5,7 @@ namespace Avalonia.Animation /// /// Transition class that handles with types. /// - public class FloatTransition : Transition + public class FloatTransition : AnimatorDrivenTransition { - private static readonly FloatAnimator s_animator = new FloatAnimator(); - - /// - public override IObservable DoTransition(IObservable progress, float oldValue, float newValue) - { - return progress - .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue)); - } } } diff --git a/src/Avalonia.Animation/Transitions/IntegerTransition.cs b/src/Avalonia.Animation/Transitions/IntegerTransition.cs index 223b2ba531..343da7b689 100644 --- a/src/Avalonia.Animation/Transitions/IntegerTransition.cs +++ b/src/Avalonia.Animation/Transitions/IntegerTransition.cs @@ -1,6 +1,3 @@ -using System; -using System.Reactive.Linq; - using Avalonia.Animation.Animators; namespace Avalonia.Animation @@ -8,15 +5,7 @@ namespace Avalonia.Animation /// /// Transition class that handles with types. /// - public class IntegerTransition : Transition + public class IntegerTransition : AnimatorDrivenTransition { - private static readonly Int32Animator s_animator = new Int32Animator(); - - /// - public override IObservable DoTransition(IObservable progress, int oldValue, int newValue) - { - return progress - .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue)); - } } } diff --git a/src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs index 008613fb40..8a070836e9 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs @@ -1,6 +1,3 @@ -using System; -using System.Reactive.Linq; - using Avalonia.Animation.Animators; using Avalonia.Media; @@ -9,15 +6,7 @@ namespace Avalonia.Animation /// /// Transition class that handles with type. /// - public class BoxShadowsTransition : Transition + public class BoxShadowsTransition : AnimatorDrivenTransition { - private static readonly BoxShadowsAnimator s_animator = new BoxShadowsAnimator(); - - /// - public override IObservable DoTransition(IObservable progress, BoxShadows oldValue, BoxShadows newValue) - { - return progress - .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue)); - } } } diff --git a/src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs new file mode 100644 index 0000000000..9b925a3779 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs @@ -0,0 +1,12 @@ +using Avalonia.Animation.Animators; +using Avalonia.Media; + +namespace Avalonia.Animation +{ + /// + /// Transition class that handles with type. + /// + public class ColorTransition : AnimatorDrivenTransition + { + } +} diff --git a/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs index 9ffdf53694..62b77a64e1 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs @@ -1,6 +1,3 @@ -using System; -using System.Reactive.Linq; - using Avalonia.Animation.Animators; namespace Avalonia.Animation @@ -8,15 +5,7 @@ namespace Avalonia.Animation /// /// Transition class that handles with type. /// - public class CornerRadiusTransition : Transition + public class CornerRadiusTransition : AnimatorDrivenTransition { - private static readonly CornerRadiusAnimator s_animator = new CornerRadiusAnimator(); - - /// - public override IObservable DoTransition(IObservable progress, CornerRadius oldValue, CornerRadius newValue) - { - return progress - .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue)); - } } } diff --git a/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs index fbe24c6d55..0985aaa8f8 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs @@ -1,6 +1,3 @@ -using System; -using System.Reactive.Linq; - using Avalonia.Animation.Animators; namespace Avalonia.Animation @@ -8,15 +5,7 @@ namespace Avalonia.Animation /// /// Transition class that handles with type. /// - public class PointTransition : Transition + public class PointTransition : AnimatorDrivenTransition { - private static readonly PointAnimator s_animator = new PointAnimator(); - - /// - public override IObservable DoTransition(IObservable progress, Point oldValue, Point newValue) - { - return progress - .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue)); - } } } diff --git a/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs index 464f83bec7..6e59fb3631 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs @@ -1,6 +1,3 @@ -using System; -using System.Reactive.Linq; - using Avalonia.Animation.Animators; namespace Avalonia.Animation @@ -8,15 +5,7 @@ namespace Avalonia.Animation /// /// Transition class that handles with type. /// - public class SizeTransition : Transition + public class SizeTransition : AnimatorDrivenTransition { - private static readonly SizeAnimator s_animator = new SizeAnimator(); - - /// - public override IObservable DoTransition(IObservable progress, Size oldValue, Size newValue) - { - return progress - .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue)); - } } } diff --git a/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs index 9fb3380780..f50929348f 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs @@ -1,6 +1,3 @@ -using System; -using System.Reactive.Linq; - using Avalonia.Animation.Animators; namespace Avalonia.Animation @@ -8,15 +5,7 @@ namespace Avalonia.Animation /// /// Transition class that handles with type. /// - public class ThicknessTransition : Transition + public class ThicknessTransition : AnimatorDrivenTransition { - private static readonly ThicknessAnimator s_animator = new ThicknessAnimator(); - - /// - public override IObservable DoTransition(IObservable progress, Thickness oldValue, Thickness newValue) - { - return progress - .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue)); - } } } diff --git a/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs index 104acb71ad..73fc23ad99 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, Easing, oldTransform, newTransform); } } } diff --git a/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs index 5038117faa..596989fbae 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs @@ -1,6 +1,3 @@ -using System; -using System.Reactive.Linq; - using Avalonia.Animation.Animators; namespace Avalonia.Animation @@ -8,15 +5,7 @@ namespace Avalonia.Animation /// /// Transition class that handles with type. /// - public class VectorTransition : Transition + public class VectorTransition : AnimatorDrivenTransition { - private static readonly VectorAnimator s_animator = new VectorAnimator(); - - /// - public override IObservable DoTransition(IObservable progress, Vector oldValue, Vector newValue) - { - return progress - .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue)); - } } } diff --git a/tests/Avalonia.Benchmarks/Animations/TransitionBenchmark.cs b/tests/Avalonia.Benchmarks/Animations/TransitionBenchmark.cs new file mode 100644 index 0000000000..f74457a924 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Animations/TransitionBenchmark.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Runtime.CompilerServices; +using Avalonia.Animation; +using Avalonia.Animation.Animators; +using Avalonia.Layout; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Animations +{ + [MemoryDiagnoser] + public class TransitionBenchmark + { + private readonly AddValueObserver _observer; + private readonly List _producedValues; + private readonly Subject _timeProducer; + private readonly DoubleTransition _transition; + + public TransitionBenchmark() + { + _transition = new DoubleTransition + { + Duration = TimeSpan.FromMilliseconds(FrameCount), Property = Layoutable.WidthProperty + }; + + _timeProducer = new Subject(); + _producedValues = new List(FrameCount); + + _observer = new AddValueObserver(_producedValues); + } + + [Params(10, 100)] public int FrameCount { get; set; } + + [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] + public void NewTransition() + { + var transitionObs = _transition.DoTransition(_timeProducer, 0, 1); + + _producedValues.Clear(); + + using var transitionSub = transitionObs.Subscribe(_observer); + + for (int i = 0; i < FrameCount; i++) + { + _timeProducer.OnNext(i / 1000d); + } + + 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); + } + } + } +}