diff --git a/src/Avalonia.Base/Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs index 92d2c2c8b5..9d4575b9ad 100644 --- a/src/Avalonia.Base/Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Base/Animation/AnimationInstance`1.cs @@ -58,6 +58,9 @@ namespace Avalonia.Animation 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; @@ -151,72 +154,74 @@ namespace Avalonia.Animation var iterDelay = _iterationDelay.Ticks * _speedRatioConv; var initDelay = _initialDelay.Ticks * _speedRatioConv; - if (indexTime > 0 & indexTime <= initDelay) + // 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) { DoDelay(); + return; } - else + + // Calculate timebases. + var iterationTime = iterDuration + iterDelay; + var opsTime = indexTime - initDelay; + var playbackTime = opsTime % iterationTime; + + _currentIteration = (ulong)(opsTime / iterationTime); + + // 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) { - // Calculate timebases. - var iterationTime = iterDuration + iterDelay; - var opsTime = indexTime - initDelay; - var playbackTime = opsTime % iterationTime; + var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0); + _lastInterpValue = _interpolator(easedTime, _neutralValue); + DoComplete(); + } - _currentIteration = (ulong)(opsTime / iterationTime); + if (playbackTime <= iterDuration) + { + // Normalize time for interpolation. + var normalizedTime = playbackTime / iterDuration; - // 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) - { - var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0); - _lastInterpValue = _interpolator(easedTime, _neutralValue); - DoComplete(); - } + // Check if normalized time needs to be reversed according to PlaybackDirection - if (playbackTime <= iterDuration) - { - // Normalize time for interpolation. - var normalizedTime = playbackTime / iterDuration; - - // Check if normalized time needs to be reversed according to PlaybackDirection - - switch (_playbackDirection) - { - 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}"); - } - - if (_playbackReversed) - normalizedTime = 1 - normalizedTime; - - // Ease and interpolate - var easedTime = _easeFunc!.Ease(normalizedTime); - _lastInterpValue = _interpolator(easedTime, _neutralValue); - - PublishNext(_lastInterpValue); - } - else if (playbackTime > iterDuration & - playbackTime <= iterationTime & - iterDelay > 0) + switch (_playbackDirection) { - // The last iteration's trailing delay should be skipped. - if (_currentIteration + 1 < _iterationCount) - DoDelay(); - else - DoComplete(); + 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}"); } + + if (_playbackReversed) + normalizedTime = 1 - normalizedTime; + + // Ease and interpolate + var easedTime = _easeFunc!.Ease(normalizedTime); + _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(); } } diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs index dde59365a1..ca76c19de8 100644 --- a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs @@ -71,11 +71,14 @@ namespace Avalonia.Base.UnitTests.Animation var clock = new TestClock(); var animationRun = animation.RunAsync(border, clock); + border.Measure(Size.Infinity); + border.Arrange(new Rect(border.DesiredSize)); + clock.Step(TimeSpan.Zero); // Initial Delay. - clock.Step(TimeSpan.FromSeconds(1)); - Assert.Equal(border.Width, 0d); + clock.Step(TimeSpan.FromSeconds(0)); + Assert.Equal(100d, border.Width); clock.Step(TimeSpan.FromSeconds(6)); @@ -126,7 +129,84 @@ namespace Avalonia.Base.UnitTests.Animation clock.Step(TimeSpan.FromSeconds(0.100d)); Assert.Equal(border.Width, 300d); } + + [Theory] + [InlineData(FillMode.Backward, 0, 0d, 0.7d)] + [InlineData(FillMode.Both, 0, 0d, 0.7d)] + [InlineData(FillMode.Forward, 100, 0d, 0.7d)] + [InlineData(FillMode.Backward, 0, 0.3d, 0.7d)] + [InlineData(FillMode.Both, 0, 0.3d, 0.7d)] + [InlineData(FillMode.Forward, 100, 0.3d, 0.7d)] + public void Check_FillMode_Start_Value(FillMode fillMode, double target, double startCue, double endCue) + { + var keyframe1 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 0d), }, Cue = new Cue(startCue) + }; + + var keyframe2 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 300d), }, Cue = new Cue(endCue) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(10d), + Delay = TimeSpan.FromSeconds(5d), + FillMode = fillMode, + Children = { keyframe1, keyframe2 } + }; + + var border = new Border() { Height = 100d, Width = 100d, }; + + var clock = new TestClock(); + + animation.RunAsync(border, clock); + + clock.Step(TimeSpan.Zero); + + Assert.Equal(target, border.Width); + } + + [Theory] + [InlineData(FillMode.Backward, 100, 0.3d, 1d)] + [InlineData(FillMode.Both, 300, 0.3d, 1d)] + [InlineData(FillMode.Forward, 300, 0.3d, 1d)] + [InlineData(FillMode.Backward, 100, 0.3d, 0.7d)] + [InlineData(FillMode.Both, 300, 0.3d, 0.7d)] + [InlineData(FillMode.Forward, 300, 0.3d, 0.7d)] + public void Check_FillMode_End_Value(FillMode fillMode, double target, double startCue, double endCue) + { + var keyframe1 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 0d), }, Cue = new Cue(0.7d) + }; + + var keyframe2 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 300d), }, Cue = new Cue(1d) + }; + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(10d), + Delay = TimeSpan.FromSeconds(5d), + FillMode = fillMode, + Children = { keyframe1, keyframe2 } + }; + + var border = new Border() { Height = 100d, Width = 100d, }; + + var clock = new TestClock(); + + animation.RunAsync(border, clock); + + clock.Step(TimeSpan.FromSeconds(0)); + clock.Step(TimeSpan.FromSeconds(20)); + + Assert.Equal(target, border.Width); + } + [Fact] public void Dispose_Subscription_Should_Stop_Animation() {