diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
index e1dbd20b074..2d90b4bc72b 100644
--- a/samples/RenderDemo/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -49,6 +49,9 @@
+
+
+
diff --git a/samples/RenderDemo/Pages/AnimationSpeedPage.xaml b/samples/RenderDemo/Pages/AnimationSpeedPage.xaml
new file mode 100644
index 00000000000..08e88e53e7a
--- /dev/null
+++ b/samples/RenderDemo/Pages/AnimationSpeedPage.xaml
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/RenderDemo/Pages/AnimationSpeedPage.xaml.cs b/samples/RenderDemo/Pages/AnimationSpeedPage.xaml.cs
new file mode 100644
index 00000000000..00dce1d0558
--- /dev/null
+++ b/samples/RenderDemo/Pages/AnimationSpeedPage.xaml.cs
@@ -0,0 +1,23 @@
+using System.Reactive.Linq;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Markup.Xaml;
+using RenderDemo.ViewModels;
+
+namespace RenderDemo.Pages;
+
+public class AnimationSpeedPage : UserControl
+{
+ public AnimationSpeedPage()
+ {
+ InitializeComponent();
+ this.DataContext = new AnimationSpeedPageViewModel();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
diff --git a/samples/RenderDemo/ViewModels/AnimationSpeedPageViewModel.cs b/samples/RenderDemo/ViewModels/AnimationSpeedPageViewModel.cs
new file mode 100644
index 00000000000..3fd45639dab
--- /dev/null
+++ b/samples/RenderDemo/ViewModels/AnimationSpeedPageViewModel.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections;
+using Avalonia.Animation;
+using MiniMvvm;
+
+namespace RenderDemo.ViewModels;
+
+public class AnimationSpeedPageViewModel : ViewModelBase
+{
+ private double _speedRatio;
+ public double SpeedRatio
+ {
+ get => _speedRatio;
+ set => RaiseAndSetIfChanged(ref _speedRatio, value);
+ }
+
+ private static readonly PlaybackDirection[] s_playbackDirections = [
+ PlaybackDirection.Normal,
+ PlaybackDirection.Reverse,
+ PlaybackDirection.Alternate,
+ PlaybackDirection.AlternateReverse
+ ];
+ public PlaybackDirection[] PlaybackDirections
+ {
+ get => s_playbackDirections;
+ }
+
+ private PlaybackDirection GetOppositePlaybackDirection(PlaybackDirection value)
+ {
+ switch (value)
+ {
+ case PlaybackDirection.Normal:
+ return PlaybackDirection.Reverse;
+ case PlaybackDirection.Reverse:
+ return PlaybackDirection.Normal;
+ case PlaybackDirection.Alternate:
+ return PlaybackDirection.AlternateReverse;
+ case PlaybackDirection.AlternateReverse:
+ return PlaybackDirection.Alternate;
+ }
+ throw new ArgumentOutOfRangeException();
+ }
+
+ private PlaybackDirection _playbackDirection;
+ public PlaybackDirection PlaybackDirection
+ {
+ get => _playbackDirection;
+ set
+ {
+ _playbackDirection = value;
+ RaisePropertyChanged(nameof(PlaybackDirection));
+ _playbackDirectionOpposite = GetOppositePlaybackDirection(value);
+ RaisePropertyChanged(nameof(PlaybackDirectionOpposite));
+ }
+ }
+
+ private PlaybackDirection _playbackDirectionOpposite;
+ public PlaybackDirection PlaybackDirectionOpposite
+ {
+ get => _playbackDirectionOpposite;
+ set
+ {
+ _playbackDirectionOpposite = value;
+ RaisePropertyChanged(nameof(PlaybackDirectionOpposite));
+ _playbackDirection = GetOppositePlaybackDirection(value);
+ RaisePropertyChanged(nameof(PlaybackDirection));
+ }
+ }
+
+ private TimeSpan _delay;
+ public TimeSpan Delay
+ {
+ get => _delay;
+ set => RaiseAndSetIfChanged(ref _delay, value);
+ }
+
+ public double DelayInput
+ {
+ get => _delay.TotalSeconds;
+ set => Delay = TimeSpan.FromSeconds(value);
+ }
+
+ private TimeSpan _delayIters;
+ public TimeSpan DelayIters
+ {
+ get => _delayIters;
+ set => RaiseAndSetIfChanged(ref _delayIters, value);
+ }
+
+ public double DelayItersInput
+ {
+ get => _delayIters.TotalSeconds;
+ set => DelayIters = TimeSpan.FromSeconds(value);
+ }
+
+ public AnimationSpeedPageViewModel()
+ {
+ SpeedRatio = 1;
+ PlaybackDirection = PlaybackDirection.Normal;
+ DelayInput = 0;
+ DelayItersInput = 0;
+ }
+}
diff --git a/src/Avalonia.Base/Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs
index ec27978939b..e30d3d5ab56 100644
--- a/src/Avalonia.Base/Animation/AnimationInstance`1.cs
+++ b/src/Avalonia.Base/Animation/AnimationInstance`1.cs
@@ -12,68 +12,102 @@ namespace Avalonia.Animation
///
internal class AnimationInstance : SingleSubscriberObservableBase
{
- private T _lastInterpValue;
- private T _firstKFValue;
- private ulong? _iterationCount;
- private ulong _currentIteration;
- private bool _gotFirstKFValue;
- private bool _playbackReversed;
- private FillMode _fillMode;
- private PlaybackDirection _playbackDirection;
private readonly Animator _animator;
private readonly Animation _animation;
private readonly Animatable _targetControl;
- private T _neutralValue;
- private double _speedRatioConv;
- private TimeSpan _initialDelay;
- private TimeSpan _iterationDelay;
- private TimeSpan _duration;
- private Easings.Easing? _easeFunc;
private readonly Action? _onCompleteAction;
- private readonly Func _interpolator;
private IDisposable? _timerSub;
+ private EventHandler? _propertyChangedDelegate;
+
private readonly IClock _baseClock;
private IClock? _clock;
- private EventHandler? _propertyChangedDelegate;
+
+ private Easings.Easing? _easeFunc;
+ private readonly Func _interpolator;
+
+ private T _neutralValue;
+ private FillMode _fillMode;
+
+ private bool _isFirstFrame;
+ private bool _isInFirstInitialDelay;
+ private T _lastInterpValue;
+ private T _initialKFValue;
+ private long? _iterationCount;
+ private TimeSpan _initialDelay;
+ private TimeSpan _iterationDelay;
+ private TimeSpan _duration;
+
+ private TimeSpan _timePrev;
+ private long _animTimePrev;
+
+ private PlaybackDirection _playbackDirection;
+ private PlaybackDirection _playbackDirectionPrev;
+ private double _speedRatio;
+ private double _speedRatioPrev;
+ private bool _timeMovesBackwards;
+
+ private TimeSpan _timeOfLastChange;
+ private long _animTimeOfLastChange;
+
+ ///
+ /// Animation's smallest unit of time in terms of TimeSpan Ticks. This can be used
+ /// to increase possible runtime of animation before reaching over/under-flow.
+ ///
+ ///
+ /// Value to use here can be found with TimeSpan.FromMilliseconds(x).Ticks
+ ///
+ private const long PRECISION_IN_TICKS = 10_000;
public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action? OnComplete, Func Interpolator)
{
+ _lastInterpValue = default!;
_animator = animator;
_animation = animation;
_targetControl = control;
_onCompleteAction = OnComplete;
_interpolator = Interpolator;
_baseClock = baseClock;
- _lastInterpValue = default!;
- _firstKFValue = default!;
+ _initialKFValue = default!;
_neutralValue = default!;
+ _isFirstFrame = true;
+ _isInFirstInitialDelay = true;
+ _speedRatio = 1;
FetchProperties();
}
private void FetchProperties()
{
- if (_animation.SpeedRatio < 0d)
- throw new InvalidOperationException("SpeedRatio value should not be negative.");
-
- if (_animation.Duration < TimeSpan.Zero)
- throw new InvalidOperationException("Duration value cannot be negative.");
-
- if (_animation.Delay < TimeSpan.Zero)
- throw new InvalidOperationException("Delay value cannot be negative.");
-
_easeFunc = _animation.Easing;
- _speedRatioConv = 1d / _animation.SpeedRatio;
+ _speedRatioPrev = _speedRatio;
+ _speedRatio = _animation.SpeedRatio;
+ if (_speedRatio < 0d)
+ throw new InvalidOperationException("SpeedRatio value cannot be negative.");
_initialDelay = _animation.Delay;
+ if (_initialDelay < TimeSpan.Zero)
+ throw new InvalidOperationException("Delay value cannot be negative.");
+
_duration = _animation.Duration;
+ if (_duration < TimeSpan.Zero)
+ throw new InvalidOperationException("Duration value cannot be negative.");
+
_iterationDelay = _animation.DelayBetweenIterations;
+ if (_iterationDelay < TimeSpan.Zero)
+ throw new InvalidOperationException("DelayBetweenIterations value cannot be negative.");
if (_animation.IterationCount.RepeatType == IterationType.Many)
- _iterationCount = _animation.IterationCount.Value;
+ {
+ if (_animation.IterationCount.Value > long.MaxValue)
+ throw new InvalidOperationException("IterationCount value cannot be larger than long.MaxValue.");
+ _iterationCount = (long)_animation.IterationCount.Value;
+ }
else
+ {
_iterationCount = null;
+ }
+ _playbackDirectionPrev = _playbackDirection;
_playbackDirection = _animation.PlaybackDirection;
_fillMode = _animation.FillMode;
}
@@ -81,7 +115,8 @@ private void FetchProperties()
protected override void Unsubscribed()
{
// Animation may have been stopped before it has finished.
- ApplyFinalFill();
+ if (CanApplyFinalFill())
+ ApplyFinalFill(_lastInterpValue);
_targetControl.PropertyChanged -= _propertyChangedDelegate;
_timerSub?.Dispose();
@@ -109,121 +144,278 @@ public void Step(TimeSpan frameTick)
}
}
- private void ApplyFinalFill()
+ private bool CanApplyFinalFill()
+ {
+ return _fillMode is FillMode.Forward or FillMode.Both;
+ }
+
+ private void ApplyFinalFill(T interpolatedValue)
{
if (_animator.Property is null)
throw new InvalidOperationException("Animator has no property specified.");
- if (_fillMode is FillMode.Forward or FillMode.Both)
- _targetControl.SetValue(_animator.Property, _lastInterpValue);
+ _targetControl.SetValue(_animator.Property, interpolatedValue);
}
- private void DoComplete()
+ private void DoComplete(bool stopAsIs)
{
- ApplyFinalFill();
+ if (!stopAsIs)
+ {
+ // Update _lastInterpValue because PublishCompleted might perform final fill
+ // and we should ensure that filled value is snapped to last value that animation would
+ // reach if it was left running in current state.
+ var isIterationReversed = IsIterationReversed(IsAlternatingPlaybackDirection(), _iterationCount + 1);
+ _lastInterpValue = FindEdgeValue(IsAnimTimeGoingBackwards(), isIterationReversed);
+ }
_onCompleteAction?.Invoke();
PublishCompleted();
}
- private void DoDelay()
+ private void DoInitialDelay()
+ {
+ if (_isInFirstInitialDelay)
+ {
+ if (_fillMode is not (FillMode.Backward or FillMode.Both))
+ return;
+ }
+
+ PublishNext(_initialKFValue);
+ }
+
+ private void DoIterationDelay(bool isIterationReversed)
{
- if (_fillMode is not (FillMode.Backward or FillMode.Both)) return;
- PublishNext(_currentIteration == 0 ? _firstKFValue : _lastInterpValue);
+ _lastInterpValue = FindEdgeValue(false, isIterationReversed);
+ PublishNext(_lastInterpValue);
}
private void DoPlayStates()
{
if (_clock!.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop)
- DoComplete();
+ DoComplete(true);
- if (!_gotFirstKFValue)
+ if (_isFirstFrame)
{
- _firstKFValue = (T)_animator.First().Value!;
- _gotFirstKFValue = true;
+ // In first frame of animation we determine the expected direction of time.
+ _timeMovesBackwards =
+ _animation.PlaybackDirection == PlaybackDirection.Reverse ||
+ _animation.PlaybackDirection == PlaybackDirection.AlternateReverse;
+
+ if (_timeMovesBackwards)
+ {
+ if (_animator.Last().Value is T last)
+ _initialKFValue = last;
+ }
+ else
+ {
+ if (_animator.First().Value is T first)
+ _initialKFValue = first;
+ }
+
+ _isFirstFrame = false;
+ }
+ }
+
+ private static bool IsIterationReversed(bool isAlternatingPlaybackDirection, long? iterationIndex)
+ {
+ if (isAlternatingPlaybackDirection)
+ return (iterationIndex & 1) != 0;
+ else
+ return false;
+ }
+
+ private T FindEdgeValue(bool isAnimTimeGoingBackwards, bool isIterationReversed)
+ {
+ double finalEaseValue = isAnimTimeGoingBackwards ^ isIterationReversed ? 0.0 : 1.0;
+
+ var easedTime = _easeFunc!.Ease(finalEaseValue);
+ return _interpolator(easedTime, _neutralValue);
+ }
+
+ private bool IsAnimTimeGoingBackwards()
+ {
+ return
+ _playbackDirection == PlaybackDirection.Reverse ||
+ _playbackDirection == PlaybackDirection.AlternateReverse;
+ }
+
+ private bool IsAlternatingPlaybackDirection()
+ {
+ return
+ _playbackDirection == PlaybackDirection.Alternate ||
+ _playbackDirection == PlaybackDirection.AlternateReverse;
+ }
+
+ private bool ApplyInitialDelay(ref long animTime)
+ {
+ // Handle all possible cases of applying initial delay and clamping.
+ long delay = _initialDelay.Ticks / PRECISION_IN_TICKS;
+ if (_iterationCount.HasValue)
+ {
+ animTime -= delay;
+ if (animTime <= 0)
+ {
+ if (animTime < -delay)
+ animTime = -delay;
+ return true;
+ }
+ }
+ else
+ {
+ // Determine the interval of initial delay.
+ long low = -delay;
+ long high = 0;
+
+ // Handle all 3 location cases based on above interval.
+ if (animTime < low)
+ {
+ animTime += delay;
+ if (animTime > 0)
+ {
+ animTime = 0;
+ return true;
+ }
+ }
+ else if (animTime > high)
+ {
+ animTime -= delay;
+ if (animTime < 0)
+ {
+ animTime = 0;
+ return true;
+ }
+ }
+ else // if (animTime >= low && animTime <= high)
+ {
+ animTime = 0;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void ApplyLimitedClamp(ref long animTime)
+ {
+ if (_timeMovesBackwards)
+ {
+ if (animTime > 0)
+ animTime = 0;
+ }
+ else
+ {
+ if (animTime < 0)
+ animTime = 0;
}
}
private void InternalStep(TimeSpan time)
{
DoPlayStates();
-
FetchProperties();
- // Scale timebases according to speedratio.
- var indexTime = time.Ticks;
- var iterDuration = _duration.Ticks * _speedRatioConv;
- var iterDelay = _iterationDelay.Ticks * _speedRatioConv;
- var initDelay = _initialDelay.Ticks * _speedRatioConv;
+ if (_speedRatio != _speedRatioPrev || _playbackDirection != _playbackDirectionPrev)
+ {
+ // Remember the time when speed changed.
+ // All we can know is that it changed some time between current and prev frame.
+ // We assume that this happened exactly at prev frame.
+ _timeOfLastChange = _timePrev;
+ _animTimeOfLastChange = _animTimePrev;
+ }
+
+ bool isAnimTimeGoingBackwards = IsAnimTimeGoingBackwards();
+
+ // Combine SpeedRatio and PlaybackDirection into a single signed speed ratio.
+ double speedRatio = isAnimTimeGoingBackwards ? -_speedRatio : _speedRatio;
- // This conditional checks if the time given is the very start/zero
- // and when we have an active delay time.
- if (initDelay > 0 && indexTime <= initDelay)
+ // Calculate animation time. That's time that has passed inside
+ // the animation since its beginning.
+ var timeSinceLastChange = time - _timeOfLastChange;
+ var animTimeSinceLastChange = (long)(timeSinceLastChange.Ticks / PRECISION_IN_TICKS * speedRatio);
+ var animTime = _animTimeOfLastChange + animTimeSinceLastChange;
+
+ if (_iterationCount.HasValue)
{
- DoDelay();
- return;
+ // Make sure animation time is inside a valid interval.
+ ApplyLimitedClamp(ref animTime);
}
- // Calculate timebases.
- var iterationTime = iterDuration + iterDelay;
- var opsTime = indexTime - initDelay;
- var playbackTime = opsTime % iterationTime;
+ _timePrev = time;
+ _animTimePrev = animTime;
- _currentIteration = (ulong)(opsTime / iterationTime);
+ // Get animation time oriented in the direction of time. Animation running in
+ // same direction as it was on first frame will always increase this variable.
+ long animTimePositive = _timeMovesBackwards ? -animTime : animTime;
- // Stop animation when the current iteration is beyond the iteration count or
- // when the duration is set to zero while animating and snap to the last iterated value.
- if (_currentIteration + 1 > _iterationCount || _duration == TimeSpan.Zero)
+ if (_initialDelay > TimeSpan.Zero)
{
- var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0);
- _lastInterpValue = _interpolator(easedTime, _neutralValue);
- DoComplete();
+ bool isCurrentlyInsideDelay = ApplyInitialDelay(ref animTimePositive);
+ if (isCurrentlyInsideDelay)
+ {
+ DoInitialDelay();
+ return;
+ }
+
+ _isInFirstInitialDelay = false;
+ }
+
+ var iterDuration = _duration.Ticks / PRECISION_IN_TICKS;
+ var iterDelay = _iterationDelay.Ticks / PRECISION_IN_TICKS;
+ var iterDurationTotal = iterDuration + iterDelay;
+
+ if (iterDurationTotal <= 0)
+ {
+ DoComplete(false);
return;
}
- if (playbackTime <= iterDuration)
+ // Calculate current iteration info.
+ var iterIndex = animTimePositive / iterDurationTotal;
+ var iterTime = animTimePositive % iterDurationTotal;
+
+ bool playbackReversed = animTimePositive < 0;
+ if (playbackReversed)
{
- // Normalize time for interpolation.
- var normalizedTime = playbackTime / iterDuration;
+ // Animation time is behind the starting point of animation.
+
+ // First negative iteration has index -1, first positive iteration has index 0.
+ iterIndex--;
+
+ // Move iteration delay to the front of iteration, which is (when moving
+ // backwards through animation time) effectively at the end of iteration.
+ iterTime = -iterTime;
+ }
+ playbackReversed ^= _timeMovesBackwards ^ IsIterationReversed(IsAlternatingPlaybackDirection(), iterIndex);
- // Check if normalized time needs to be reversed according to PlaybackDirection
+ var itersUntilEnd = _iterationCount - iterIndex;
+
+ // End animation when limit is reached.
+ if (itersUntilEnd <= 0)
+ {
+ DoComplete(false);
+ return;
+ }
- switch (_playbackDirection)
+ if (iterTime > iterDuration && iterTime <= iterDurationTotal && iterDelay > 0)
+ {
+ // The last iteration's trailing delay should be skipped.
+ if (_iterationCount.HasValue && itersUntilEnd <= 1)
{
- case PlaybackDirection.Normal:
- _playbackReversed = false;
- break;
- case PlaybackDirection.Reverse:
- _playbackReversed = true;
- break;
- case PlaybackDirection.Alternate:
- _playbackReversed = _currentIteration % 2 != 0;
- break;
- case PlaybackDirection.AlternateReverse:
- _playbackReversed = _currentIteration % 2 == 0;
- break;
- default:
- throw new InvalidOperationException(
- $"Animation direction value is unknown: {_playbackDirection}");
+ DoComplete(false);
+ return;
}
- if (_playbackReversed)
- normalizedTime = 1 - normalizedTime;
+ DoIterationDelay(playbackReversed);
+ }
+ else if (iterTime <= iterDuration)
+ {
+ // Ease and interpolate.
+ var normalized = iterTime / (double)iterDuration;
+ if (playbackReversed)
+ normalized = 1 - normalized;
- // Ease and interpolate
- var easedTime = _easeFunc!.Ease(normalizedTime);
+ var easedTime = _easeFunc!.Ease(normalized);
_lastInterpValue = _interpolator(easedTime, _neutralValue);
PublishNext(_lastInterpValue);
}
- else if (playbackTime > iterDuration &&
- playbackTime <= iterationTime &&
- iterDelay > 0)
- {
- // The last iteration's trailing delay should be skipped.
- if (_currentIteration + 1 < _iterationCount)
- DoDelay();
- else
- DoComplete();
- }
}
private void UpdateNeutralValue()
diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs
index a76b5a9c3fb..cd00635e1fe 100644
--- a/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs
@@ -654,6 +654,262 @@ public void Zero_Duration_Should_Finish_Animation_With_Infinite_Iteration()
}
}
+ [Fact]
+ public void Changing_SpeedRatio_Keeps_Playback_Time_Constant()
+ {
+ using (Start())
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters = { new Setter(Visual.OpacityProperty, 0d), },
+ KeyTime = TimeSpan.FromSeconds(0)
+ };
+
+ var keyframe2 = new KeyFrame()
+ {
+ Setters = { new Setter(Visual.OpacityProperty, 1d), },
+ KeyTime = TimeSpan.FromSeconds(1)
+ };
+
+ var animation = new Avalonia.Animation.Animation()
+ {
+ Duration = TimeSpan.FromSeconds(1),
+ Children = { keyframe1, keyframe2 },
+ };
+
+ Border target;
+ var clock = new TestClock();
+ var root = new TestRoot
+ {
+ Clock = clock,
+ Styles = { new Style(x => x.OfType()) { Animations = { animation }, } },
+ Child = target = new Border { Background = Brushes.Red, }
+ };
+
+ root.Measure(Size.Infinity);
+ root.Arrange(new Rect(root.DesiredSize));
+
+ clock.Step(TimeSpan.FromSeconds(0));
+ clock.Step(TimeSpan.FromSeconds(0.25));
+
+ animation.SpeedRatio = 2;
+ Assert.Equal(0.25, target.Opacity);
+ clock.Step(TimeSpan.FromSeconds(0.25));
+ Assert.Equal(0.25, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(0.25 + 0.125));
+ Assert.Equal(0.5, target.Opacity);
+
+ animation.SpeedRatio = 0.5;
+ Assert.Equal(0.5, target.Opacity);
+ clock.Step(TimeSpan.FromSeconds(0.25 + 0.125));
+ Assert.Equal(0.5, target.Opacity);
+ clock.Step(TimeSpan.FromSeconds(0.25 + 0.125 + 0.5));
+ Assert.Equal(0.75, target.Opacity);
+
+ animation.SpeedRatio = 1;
+ Assert.Equal(0.75, target.Opacity);
+ clock.Step(TimeSpan.FromSeconds(0.25 + 0.125 + 0.5));
+ Assert.Equal(0.75, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(0.25 + 0.125 + 0.5 + 0.25));
+ Assert.Equal(1, target.Opacity);
+ }
+ }
+
+ [Fact]
+ public void Changing_PlaybackDirection_Keeps_Playback_Time_Constant()
+ {
+ using (Start())
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters = { new Setter(Visual.OpacityProperty, 0d), },
+ KeyTime = TimeSpan.FromSeconds(0)
+ };
+
+ var keyframe2 = new KeyFrame()
+ {
+ Setters = { new Setter(Visual.OpacityProperty, 1d), },
+ KeyTime = TimeSpan.FromSeconds(1)
+ };
+
+ var animation = new Avalonia.Animation.Animation()
+ {
+ Duration = TimeSpan.FromSeconds(1),
+ Children = { keyframe1, keyframe2 },
+ PlaybackDirection = PlaybackDirection.Normal
+ };
+
+ Border target;
+ var clock = new TestClock();
+ var root = new TestRoot
+ {
+ Clock = clock,
+ Styles = { new Style(x => x.OfType()) { Animations = { animation }, } },
+ Child = target = new Border { Background = Brushes.Red, }
+ };
+
+ root.Measure(Size.Infinity);
+ root.Arrange(new Rect(root.DesiredSize));
+
+ clock.Step(TimeSpan.FromSeconds(0));
+
+ clock.Step(TimeSpan.FromSeconds(0.25));
+ Assert.Equal(0.25, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(0.3));
+ Assert.Equal(0.3, target.Opacity);
+ animation.PlaybackDirection = PlaybackDirection.Reverse;
+ clock.Step(TimeSpan.FromSeconds(0.3));
+ Assert.Equal(0.3, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(0.35));
+ Assert.Equal(0.25, target.Opacity);
+ }
+ }
+
+ [Fact]
+ public void Reversing_Direction_Past_Initial_Point_Clamps_To_Initial_Point()
+ {
+ using (Start())
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters = { new Setter(Visual.OpacityProperty, 0d), },
+ KeyTime = TimeSpan.FromSeconds(0)
+ };
+
+ var keyframe2 = new KeyFrame()
+ {
+ Setters = { new Setter(Visual.OpacityProperty, 1d), },
+ KeyTime = TimeSpan.FromSeconds(1)
+ };
+
+ var animation = new Avalonia.Animation.Animation()
+ {
+ Duration = TimeSpan.FromSeconds(1),
+ Delay = TimeSpan.FromSeconds(0.5),
+ Children = { keyframe1, keyframe2 },
+ PlaybackDirection = PlaybackDirection.Normal
+ };
+
+ Border target;
+ var clock = new TestClock();
+ var root = new TestRoot
+ {
+ Clock = clock,
+ Styles = { new Style(x => x.OfType()) { Animations = { animation }, } },
+ Child = target = new Border { Background = Brushes.Red, }
+ };
+
+ root.Measure(Size.Infinity);
+ root.Arrange(new Rect(root.DesiredSize));
+
+ clock.Step(TimeSpan.FromSeconds(0));
+ Assert.Equal(1, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(0.5));
+ Assert.Equal(1, target.Opacity);
+ clock.Step(TimeSpan.FromSeconds(1));
+ Assert.Equal(0.5, target.Opacity);
+ animation.PlaybackDirection = PlaybackDirection.Reverse;
+ clock.Step(TimeSpan.FromSeconds(1));
+ Assert.Equal(0.5, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(1.2));
+ Assert.Equal(0.3, target.Opacity);
+ clock.Step(TimeSpan.FromSeconds(1.5));
+ Assert.Equal(0, target.Opacity);
+ clock.Step(TimeSpan.FromSeconds(2));
+ Assert.Equal(0, target.Opacity);
+ clock.Step(TimeSpan.FromSeconds(3));
+ Assert.Equal(0, target.Opacity);
+ animation.PlaybackDirection = PlaybackDirection.Normal;
+ clock.Step(TimeSpan.FromSeconds(3));
+ Assert.Equal(0, target.Opacity);
+ clock.Step(TimeSpan.FromSeconds(3.5));
+ Assert.Equal(0, target.Opacity);
+ clock.Step(TimeSpan.FromSeconds(4));
+ Assert.Equal(0.5, target.Opacity);
+ clock.Step(TimeSpan.FromSeconds(4.5));
+ Assert.Equal(1, target.Opacity);
+ }
+ }
+
+ [Fact]
+ public void DelayBetweenIterations_Behind_Initial_Point_Is_In_Front_Of_Iterations()
+ {
+ using (Start())
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters = { new Setter(Visual.OpacityProperty, 0d), },
+ KeyTime = TimeSpan.FromSeconds(0)
+ };
+
+ var keyframe2 = new KeyFrame()
+ {
+ Setters = { new Setter(Visual.OpacityProperty, 1d), },
+ KeyTime = TimeSpan.FromSeconds(1)
+ };
+
+ var animation = new Avalonia.Animation.Animation()
+ {
+ Duration = TimeSpan.FromSeconds(1),
+ DelayBetweenIterations = TimeSpan.FromSeconds(0.5),
+ Children = { keyframe1, keyframe2 },
+ IterationCount = IterationCount.Infinite,
+ PlaybackDirection = PlaybackDirection.Normal
+ };
+
+ Border target;
+ var clock = new TestClock();
+ var root = new TestRoot
+ {
+ Clock = clock,
+ Styles = { new Style(x => x.OfType()) { Animations = { animation }, } },
+ Child = target = new Border { Background = Brushes.Red, }
+ };
+
+ root.Measure(Size.Infinity);
+ root.Arrange(new Rect(root.DesiredSize));
+
+ clock.Step(TimeSpan.FromSeconds(0));
+ Assert.Equal(0, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(0.1));
+ Assert.Equal(0.1, target.Opacity);
+ animation.PlaybackDirection = PlaybackDirection.Reverse;
+ clock.Step(TimeSpan.FromSeconds(0.1));
+ Assert.Equal(0.1, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(0.2));
+ Assert.Equal(0, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(0.2 + 0.1));
+ Assert.Equal(0.9, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(0.2 + 0.9));
+ Assert.Equal(0.1, target.Opacity, 0.000001);
+
+ clock.Step(TimeSpan.FromSeconds(0.2 + 1));
+ Assert.Equal(0, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(0.2 + 1 + 0.25));
+ Assert.Equal(0, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(0.2 + 1 + 0.499));
+ Assert.Equal(0, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(0.2 + 1 + 0.5));
+ Assert.Equal(1, target.Opacity);
+
+ clock.Step(TimeSpan.FromSeconds(0.2 + 1 + 0.5 + 0.1));
+ Assert.Equal(0.9, target.Opacity);
+ }
+ }
+
private static IDisposable Start()
{
var clock = new MockGlobalClock();