Browse Source

Merge pull request #5953 from MarchingCube/refactor-transitions

Specialized observables for transitions
pull/6014/head
Dariusz Komosiński 5 years ago
committed by GitHub
parent
commit
02aad98e01
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 20
      src/Avalonia.Animation/AnimatorDrivenTransition.cs
  2. 32
      src/Avalonia.Animation/AnimatorTransitionObservable.cs
  3. 9
      src/Avalonia.Animation/Clock.cs
  4. 12
      src/Avalonia.Animation/ClockBase.cs
  5. 6
      src/Avalonia.Animation/Transition.cs
  6. 60
      src/Avalonia.Animation/TransitionInstance.cs
  7. 58
      src/Avalonia.Animation/TransitionObservableBase.cs
  8. 13
      src/Avalonia.Animation/Transitions/DoubleTransition.cs
  9. 13
      src/Avalonia.Animation/Transitions/FloatTransition.cs
  10. 13
      src/Avalonia.Animation/Transitions/IntegerTransition.cs
  11. 13
      src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs
  12. 12
      src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs
  13. 13
      src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs
  14. 13
      src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs
  15. 13
      src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs
  16. 13
      src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs
  17. 18
      src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs
  18. 13
      src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs
  19. 78
      tests/Avalonia.Benchmarks/Animations/TransitionBenchmark.cs

20
src/Avalonia.Animation/AnimatorDrivenTransition.cs

@ -0,0 +1,20 @@
using System;
using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
/// <summary>
/// <see cref="Transition{T}"/> using an <see cref="Animator{T}"/> to transition between values.
/// </summary>
/// <typeparam name="T">Type of the transitioned value.</typeparam>
/// <typeparam name="TAnimator">Type of the animator.</typeparam>
public abstract class AnimatorDrivenTransition<T, TAnimator> : Transition<T> where TAnimator : Animator<T>, new()
{
private static readonly TAnimator s_animator = new TAnimator();
public override IObservable<T> DoTransition(IObservable<double> progress, T oldValue, T newValue)
{
return new AnimatorTransitionObservable<T, TAnimator>(s_animator, progress, Easing, oldValue, newValue);
}
}
}

32
src/Avalonia.Animation/AnimatorTransitionObservable.cs

@ -0,0 +1,32 @@
using System;
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
namespace Avalonia.Animation
{
/// <summary>
/// Transition observable based on an <see cref="Animator{T}"/> producing a value.
/// </summary>
/// <typeparam name="T">Type of the transitioned value.</typeparam>
/// <typeparam name="TAnimator">Type of the animator.</typeparam>
public class AnimatorTransitionObservable<T, TAnimator> : TransitionObservableBase<T> where TAnimator : Animator<T>
{
private readonly TAnimator _animator;
private readonly Easing _easing;
private readonly T _oldValue;
private readonly T _newValue;
public AnimatorTransitionObservable(TAnimator animator, IObservable<double> 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);
}
}
}

9
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<IGlobalClock>();
private IDisposable _parentSubscription;
private readonly IDisposable _parentSubscription;
public Clock()
:this(GlobalClock)
public Clock() : this(GlobalClock)
{
}

12
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<TimeSpan> _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<TimeSpan> observer)
{
return _connectedObservable.Subscribe(observer);
return _observable.Subscribe(observer);
}
private class ClockObservable : LightweightObservableBase<TimeSpan>
private sealed class ClockObservable : LightweightObservableBase<TimeSpan>
{
public bool HasSubscriptions { get; private set; }
public void Pulse(TimeSpan time) => PublishNext(time);

6
src/Avalonia.Animation/Transition`1.cs → 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<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
}
}
}
}

60
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
/// <summary>
/// Handles the timing and lifetime of a <see cref="Transition{T}"/>.
/// </summary>
internal class TransitionInstance : SingleSubscriberObservableBase<double>
internal class TransitionInstance : SingleSubscriberObservableBase<double>, IObserver<TimeSpan>
{
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<TimeSpan>.OnCompleted()
{
PublishCompleted();
}
void IObserver<TimeSpan>.OnError(Exception error)
{
PublishError(error);
}
void IObserver<TimeSpan>.OnNext(TimeSpan value)
{
TimerTick(value);
}
/// <summary>
/// TODO: This clock is still fairly expensive due to <see cref="ClockBase"/> implementation.
/// </summary>
private sealed class TransitionClock : ClockBase, IObserver<TimeSpan>
{
private readonly IDisposable _parentSubscription;
public TransitionClock(IClock parent)
{
_parentSubscription = parent.Subscribe(this);
}
protected override void Stop()
{
_parentSubscription.Dispose();
}
void IObserver<TimeSpan>.OnNext(TimeSpan value)
{
Pulse(value);
}
void IObserver<TimeSpan>.OnCompleted()
{
}
void IObserver<TimeSpan>.OnError(Exception error)
{
ExceptionDispatchInfo.Capture(error).Throw();
}
}
}
}

58
src/Avalonia.Animation/TransitionObservableBase.cs

@ -0,0 +1,58 @@
using System;
using Avalonia.Animation.Easings;
using Avalonia.Reactive;
#nullable enable
namespace Avalonia.Animation
{
/// <summary>
/// Provides base for observables implementing transitions.
/// </summary>
/// <typeparam name="T">Type of the transitioned value.</typeparam>
public abstract class TransitionObservableBase<T> : SingleSubscriberObservableBase<T>, IObserver<double>
{
private readonly Easing _easing;
private readonly IObservable<double> _progress;
private IDisposable? _progressSubscription;
protected TransitionObservableBase(IObservable<double> progress, Easing easing)
{
_progress = progress;
_easing = easing;
}
/// <summary>
/// Produces value at given progress time point.
/// </summary>
/// <param name="progress">Transition progress.</param>
protected abstract T ProduceValue(double progress);
protected override void Subscribed()
{
_progressSubscription = _progress.Subscribe(this);
}
protected override void Unsubscribed()
{
_progressSubscription?.Dispose();
}
void IObserver<double>.OnCompleted()
{
PublishCompleted();
}
void IObserver<double>.OnError(Exception error)
{
PublishError(error);
}
void IObserver<double>.OnNext(double value)
{
double progress = _easing.Ease(value);
PublishNext(ProduceValue(progress));
}
}
}

13
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
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="double"/> types.
/// </summary>
public class DoubleTransition : Transition<double>
public class DoubleTransition : AnimatorDrivenTransition<double, DoubleAnimator>
{
private static readonly DoubleAnimator s_animator = new DoubleAnimator();
/// <inheritdocs/>
public override IObservable<double> DoTransition(IObservable<double> progress, double oldValue, double newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
}
}

13
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
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="float"/> types.
/// </summary>
public class FloatTransition : Transition<float>
public class FloatTransition : AnimatorDrivenTransition<float, FloatAnimator>
{
private static readonly FloatAnimator s_animator = new FloatAnimator();
/// <inheritdocs/>
public override IObservable<float> DoTransition(IObservable<double> progress, float oldValue, float newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
}
}

13
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
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="int"/> types.
/// </summary>
public class IntegerTransition : Transition<int>
public class IntegerTransition : AnimatorDrivenTransition<int, Int32Animator>
{
private static readonly Int32Animator s_animator = new Int32Animator();
/// <inheritdocs/>
public override IObservable<int> DoTransition(IObservable<double> progress, int oldValue, int newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
}
}

13
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
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="BoxShadows"/> type.
/// </summary>
public class BoxShadowsTransition : Transition<BoxShadows>
public class BoxShadowsTransition : AnimatorDrivenTransition<BoxShadows, BoxShadowsAnimator>
{
private static readonly BoxShadowsAnimator s_animator = new BoxShadowsAnimator();
/// <inheritdocs/>
public override IObservable<BoxShadows> DoTransition(IObservable<double> progress, BoxShadows oldValue, BoxShadows newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
}
}

12
src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs

@ -0,0 +1,12 @@
using Avalonia.Animation.Animators;
using Avalonia.Media;
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Color"/> type.
/// </summary>
public class ColorTransition : AnimatorDrivenTransition<Color, ColorAnimator>
{
}
}

13
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
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="CornerRadius"/> type.
/// </summary>
public class CornerRadiusTransition : Transition<CornerRadius>
public class CornerRadiusTransition : AnimatorDrivenTransition<CornerRadius, CornerRadiusAnimator>
{
private static readonly CornerRadiusAnimator s_animator = new CornerRadiusAnimator();
/// <inheritdocs/>
public override IObservable<CornerRadius> DoTransition(IObservable<double> progress, CornerRadius oldValue, CornerRadius newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
}
}

13
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
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Point"/> type.
/// </summary>
public class PointTransition : Transition<Point>
public class PointTransition : AnimatorDrivenTransition<Point, PointAnimator>
{
private static readonly PointAnimator s_animator = new PointAnimator();
/// <inheritdocs/>
public override IObservable<Point> DoTransition(IObservable<double> progress, Point oldValue, Point newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
}
}

13
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
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Size"/> type.
/// </summary>
public class SizeTransition : Transition<Size>
public class SizeTransition : AnimatorDrivenTransition<Size, SizeAnimator>
{
private static readonly SizeAnimator s_animator = new SizeAnimator();
/// <inheritdocs/>
public override IObservable<Size> DoTransition(IObservable<double> progress, Size oldValue, Size newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
}
}

13
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
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Thickness"/> type.
/// </summary>
public class ThicknessTransition : Transition<Thickness>
public class ThicknessTransition : AnimatorDrivenTransition<Thickness, ThicknessAnimator>
{
private static readonly ThicknessAnimator s_animator = new ThicknessAnimator();
/// <inheritdocs/>
public override IObservable<Thickness> DoTransition(IObservable<double> progress, Thickness oldValue, Thickness newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
}
}

18
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<ITransform>
{
private static readonly TransformOperationsAnimator _operationsAnimator = new TransformOperationsAnimator();
private static readonly TransformOperationsAnimator s_operationsAnimator = new TransformOperationsAnimator();
public override IObservable<ITransform> DoTransition(IObservable<double> progress,
public override IObservable<ITransform> DoTransition(
IObservable<double> 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<TransformOperations, TransformOperationsAnimator>(
s_operationsAnimator, progress, Easing, oldTransform, newTransform);
}
}
}

13
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
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Vector"/> type.
/// </summary>
public class VectorTransition : Transition<Vector>
public class VectorTransition : AnimatorDrivenTransition<Vector, VectorAnimator>
{
private static readonly VectorAnimator s_animator = new VectorAnimator();
/// <inheritdocs/>
public override IObservable<Vector> DoTransition(IObservable<double> progress, Vector oldValue, Vector newValue)
{
return progress
.Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
}
}

78
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<double> _producedValues;
private readonly Subject<double> _timeProducer;
private readonly DoubleTransition _transition;
public TransitionBenchmark()
{
_transition = new DoubleTransition
{
Duration = TimeSpan.FromMilliseconds(FrameCount), Property = Layoutable.WidthProperty
};
_timeProducer = new Subject<double>();
_producedValues = new List<double>(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<double>
{
private readonly List<double> _values;
public AddValueObserver(List<double> values)
{
_values = values;
}
public void OnCompleted()
{
}
public void OnError(Exception error)
{
}
public void OnNext(double value)
{
_values.Add(value);
}
}
}
}
Loading…
Cancel
Save