diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs
index d67a9501da..30c1f78c36 100644
--- a/src/Avalonia.Base/Animation/Animatable.cs
+++ b/src/Avalonia.Base/Animation/Animatable.cs
@@ -17,8 +17,8 @@ namespace Avalonia.Animation
///
/// Defines the property.
///
- internal static readonly StyledProperty ClockProperty =
- AvaloniaProperty.Register(nameof(Clock), inherits: true);
+ internal static readonly StyledProperty ClockProperty =
+ AvaloniaProperty.Register(nameof(Clock), inherits: true);
///
/// Defines the property.
@@ -36,7 +36,7 @@ namespace Avalonia.Animation
///
/// Gets or sets the clock which controls the animations on the control.
///
- internal IClock Clock
+ internal IClock? Clock
{
get => GetValue(ClockProperty);
set => SetValue(ClockProperty, value);
diff --git a/src/Avalonia.Base/Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs
index f584cad951..7e139c5ae6 100644
--- a/src/Avalonia.Base/Animation/Animation.cs
+++ b/src/Avalonia.Base/Animation/Animation.cs
@@ -234,6 +234,8 @@ namespace Avalonia.Animation
}
}
+ animatorKeyFrames.Sort(static (x, y) => x.Cue.CueValue.CompareTo(y.Cue.CueValue));
+
var newAnimatorInstances = new List();
foreach (var handler in handlerList)
@@ -247,9 +249,22 @@ namespace Avalonia.Animation
{
var animator = newAnimatorInstances.First(a => a.GetType() == keyframe.AnimatorType &&
a.Property == keyframe.Property);
+
+ if (animator.Count == 0 && FillMode is FillMode.Backward or FillMode.Both)
+ keyframe.FillBefore = true;
+
animator.Add(keyframe);
}
+ if (FillMode is FillMode.Forward or FillMode.Both)
+ {
+ foreach (var newAnimatorInstance in newAnimatorInstances)
+ {
+ if (newAnimatorInstance.Count > 0)
+ newAnimatorInstance[newAnimatorInstance.Count - 1].FillAfter = true;
+ }
+ }
+
return (newAnimatorInstances, subscriptions);
}
diff --git a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs b/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
index 81f742de9a..bba512de54 100644
--- a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
+++ b/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
@@ -16,19 +16,6 @@ namespace Avalonia.Animation
public static readonly DirectProperty ValueProperty =
AvaloniaProperty.RegisterDirect(nameof(Value), k => k.Value, (k, v) => k.Value = v);
- public AnimatorKeyFrame()
- {
-
- }
-
- public AnimatorKeyFrame(Type? animatorType, Func? animatorFactory, Cue cue)
- {
- AnimatorType = animatorType;
- AnimatorFactory = animatorFactory;
- Cue = cue;
- KeySpline = null;
- }
-
public AnimatorKeyFrame(Type? animatorType, Func? animatorFactory, Cue cue, KeySpline? keySpline)
{
AnimatorType = animatorType;
@@ -37,11 +24,12 @@ namespace Avalonia.Animation
KeySpline = keySpline;
}
- internal bool isNeutral;
public Type? AnimatorType { get; }
public Func? AnimatorFactory { get; }
public Cue Cue { get; }
public KeySpline? KeySpline { get; }
+ public bool FillBefore { get; set; }
+ public bool FillAfter { get; set; }
public AvaloniaProperty? Property { get; private set; }
private object? _value;
diff --git a/src/Avalonia.Base/Animation/Animators/Animator`1.cs b/src/Avalonia.Base/Animation/Animators/Animator`1.cs
index f93067d642..8a4469d020 100644
--- a/src/Avalonia.Base/Animation/Animators/Animator`1.cs
+++ b/src/Avalonia.Base/Animation/Animators/Animator`1.cs
@@ -1,7 +1,5 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using Avalonia.Animation.Utils;
+using System.Diagnostics;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Reactive;
@@ -13,94 +11,72 @@ namespace Avalonia.Animation.Animators
///
internal abstract class Animator : AvaloniaList, IAnimator
{
- ///
- /// List of type-converted keyframes.
- ///
- private readonly List _convertedKeyframes = new List();
-
- private bool _isVerifiedAndConverted;
-
///
/// Gets or sets the target property for the keyframe.
///
public AvaloniaProperty? Property { get; set; }
- public Animator()
- {
- // Invalidate keyframes when changed.
- this.CollectionChanged += delegate { _isVerifiedAndConverted = false; };
- }
-
///
public virtual IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable match, Action? onComplete)
{
- if (!_isVerifiedAndConverted)
- VerifyConvertKeyFrames();
-
var subject = new DisposeAnimationInstanceSubject(this, animation, control, clock, onComplete);
return new CompositeDisposable(match.Subscribe(subject), subject);
}
protected T InterpolationHandler(double animationTime, T neutralValue)
{
- AnimatorKeyFrame firstKeyframe, lastKeyframe;
+ if (Count == 0)
+ return neutralValue;
+
+ var (beforeKeyFrame, afterKeyFrame) = FindKeyFrames(animationTime);
- int kvCount = _convertedKeyframes.Count;
- if (kvCount > 2)
+ double beforeTime, afterTime;
+ T beforeValue, afterValue;
+
+ if (beforeKeyFrame is null)
{
- if (animationTime <= 0.0)
- {
- firstKeyframe = _convertedKeyframes[0];
- lastKeyframe = _convertedKeyframes[1];
- }
- else if (animationTime >= 1.0)
- {
- firstKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 2];
- lastKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 1];
- }
- else
- {
- int index = FindClosestBeforeKeyFrame(animationTime);
- firstKeyframe = _convertedKeyframes[index];
- lastKeyframe = _convertedKeyframes[index + 1];
- }
+ beforeTime = 0.0;
+ beforeValue = afterKeyFrame is { FillBefore: true, Value: T fillValue } ? fillValue : neutralValue;
}
else
{
- firstKeyframe = _convertedKeyframes[0];
- lastKeyframe = _convertedKeyframes[1];
+ beforeTime = beforeKeyFrame.Cue.CueValue;
+ beforeValue = beforeKeyFrame.Value is T value ? value : neutralValue;
}
- double t0 = firstKeyframe.Cue.CueValue;
- double t1 = lastKeyframe.Cue.CueValue;
-
- double progress = (animationTime - t0) / (t1 - t0);
-
- T oldValue, newValue;
-
- if (!firstKeyframe.isNeutral && firstKeyframe.Value is T firstKeyframeValue)
- oldValue = firstKeyframeValue;
+ if (afterKeyFrame is null)
+ {
+ afterTime = 1.0;
+ afterValue = beforeKeyFrame is { FillAfter: true, Value: T fillValue } ? fillValue : neutralValue;
+ }
else
- oldValue = neutralValue;
+ {
+ afterTime = afterKeyFrame.Cue.CueValue;
+ afterValue = afterKeyFrame.Value is T value ? value : neutralValue;
+ }
- if (!lastKeyframe.isNeutral && lastKeyframe.Value is T lastKeyframeValue)
- newValue = lastKeyframeValue;
- else
- newValue = neutralValue;
+ var progress = (animationTime - beforeTime) / (afterTime - beforeTime);
- if (lastKeyframe.KeySpline != null)
- progress = lastKeyframe.KeySpline.GetSplineProgress(progress);
+ if (afterKeyFrame?.KeySpline is { } keySpline)
+ progress = keySpline.GetSplineProgress(progress);
- return Interpolate(progress, oldValue, newValue);
+ return Interpolate(progress, beforeValue, afterValue);
}
- private int FindClosestBeforeKeyFrame(double time)
+ private (AnimatorKeyFrame? Before, AnimatorKeyFrame? After) FindKeyFrames(double time)
{
- for (int i = 0; i < _convertedKeyframes.Count; i++)
- if (_convertedKeyframes[i].Cue.CueValue > time)
- return i - 1;
+ Debug.Assert(Count >= 1);
- throw new Exception("Index time is out of keyframe time range.");
+ for (var i = 0; i < Count; i++)
+ {
+ var keyFrame = this[i];
+ var keyFrameTime = keyFrame.Cue.CueValue;
+
+ if (time < keyFrameTime || keyFrameTime == 1.0)
+ return (i > 0 ? this[i - 1] : null, keyFrame);
+ }
+
+ return (this[Count - 1], null);
}
public virtual IDisposable BindAnimation(Animatable control, IObservable instance)
@@ -123,7 +99,7 @@ namespace Avalonia.Animation.Animators
clock ?? control.Clock ?? Clock.GlobalClock,
onComplete,
InterpolationHandler);
-
+
return BindAnimation(control, instance);
}
@@ -131,52 +107,5 @@ namespace Avalonia.Animation.Animators
/// Interpolates in-between two key values given the desired progress time.
///
public abstract T Interpolate(double progress, T oldValue, T newValue);
-
- private void VerifyConvertKeyFrames()
- {
- foreach (AnimatorKeyFrame keyframe in this)
- {
- _convertedKeyframes.Add(keyframe);
- }
-
- AddNeutralKeyFramesIfNeeded();
-
- _isVerifiedAndConverted = true;
- }
-
- private void AddNeutralKeyFramesIfNeeded()
- {
- bool hasStartKey, hasEndKey;
- hasStartKey = hasEndKey = false;
-
- // Check if there's start and end keyframes.
- foreach (var frame in _convertedKeyframes)
- {
- if (frame.Cue.CueValue == 0.0d)
- {
- hasStartKey = true;
- }
- else if (frame.Cue.CueValue == 1.0d)
- {
- hasEndKey = true;
- }
- }
-
- if (!hasStartKey || !hasEndKey)
- AddNeutralKeyFrames(hasStartKey, hasEndKey);
- }
-
- private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey)
- {
- if (!hasStartKey)
- {
- _convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, null, new Cue(0.0d)) { Value = default(T), isNeutral = true });
- }
-
- if (!hasEndKey)
- {
- _convertedKeyframes.Add(new AnimatorKeyFrame(null, null, new Cue(1.0d)) { Value = default(T), isNeutral = true });
- }
- }
}
}
diff --git a/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs
index 96617b1732..c81be67060 100644
--- a/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs
+++ b/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs
@@ -86,14 +86,18 @@ namespace Avalonia.Animation.Animators
{
gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), () => new GradientBrushAnimator(), keyframe.Cue, keyframe.KeySpline)
{
- Value = GradientBrushAnimator.ConvertSolidColorBrushToGradient(firstGradient, solidColorBrush)
+ Value = GradientBrushAnimator.ConvertSolidColorBrushToGradient(firstGradient, solidColorBrush),
+ FillBefore = keyframe.FillBefore,
+ FillAfter = keyframe.FillAfter
});
}
else if (keyframe.Value is IGradientBrush)
{
gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), () => new GradientBrushAnimator(), keyframe.Cue, keyframe.KeySpline)
{
- Value = keyframe.Value
+ Value = keyframe.Value,
+ FillBefore = keyframe.FillBefore,
+ FillAfter = keyframe.FillAfter
});
}
else
@@ -118,7 +122,9 @@ namespace Avalonia.Animation.Animators
{
solidColorBrushAnimator.Add(new AnimatorKeyFrame(typeof(ISolidColorBrushAnimator), () => new ISolidColorBrushAnimator(), keyframe.Cue, keyframe.KeySpline)
{
- Value = keyframe.Value
+ Value = keyframe.Value,
+ FillBefore = keyframe.FillBefore,
+ FillAfter = keyframe.FillAfter
});
}
else
@@ -149,7 +155,9 @@ namespace Avalonia.Animation.Animators
{
animator.Add(new AnimatorKeyFrame(animatorType, animatorFactory, keyframe.Cue, keyframe.KeySpline)
{
- Value = keyframe.Value
+ Value = keyframe.Value,
+ FillBefore = keyframe.FillBefore,
+ FillAfter = keyframe.FillAfter
});
}
diff --git a/src/Avalonia.Base/Media/Effects/EffectAnimator.cs b/src/Avalonia.Base/Media/Effects/EffectAnimator.cs
index cdf6aa6b63..e2c4cc096c 100644
--- a/src/Avalonia.Base/Media/Effects/EffectAnimator.cs
+++ b/src/Avalonia.Base/Media/Effects/EffectAnimator.cs
@@ -38,7 +38,9 @@ internal class EffectAnimator : Animator
createdAnimator.Add(new AnimatorKeyFrame(typeof(TAnimator), () => new TAnimator(), keyFrame.Cue,
keyFrame.KeySpline)
{
- Value = keyFrame.Value
+ Value = keyFrame.Value,
+ FillBefore = keyFrame.FillBefore,
+ FillAfter = keyFrame.FillAfter
});
}
else
diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs
index 7ceaddfa16..a76b5a9c3f 100644
--- a/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs
@@ -126,18 +126,16 @@ namespace Avalonia.Base.UnitTests.Animation
var rect = new Rectangle() { Width = 11, };
- var originalValue = rect.Width;
-
var clock = new TestClock();
animation.RunAsync(rect, clock);
clock.Step(TimeSpan.Zero);
- Assert.Equal(rect.Width, 1);
+ Assert.Equal(1, rect.Width);
clock.Step(TimeSpan.FromSeconds(2));
- Assert.Equal(rect.Width, 2);
+ Assert.Equal(2, rect.Width);
clock.Step(TimeSpan.FromSeconds(3));
//here we have invalid value so value should be expected and set to initial original value
- Assert.Equal(rect.Width, originalValue);
+ Assert.Equal(11, rect.Width);
}
[Fact]
diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
index 250bbb5e88..65253e7214 100644
--- a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
@@ -131,17 +131,23 @@ namespace Avalonia.Base.UnitTests.Animation
}
[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)
+ [InlineData(FillMode.Backward, 50.0, 0.0, 0.7, false)]
+ [InlineData(FillMode.Backward, 50.0, 0.0, 0.7, true )]
+ [InlineData(FillMode.Both, 50.0, 0.0, 0.7, false)]
+ [InlineData(FillMode.Both, 50.0, 0.0, 0.7, true )]
+ [InlineData(FillMode.Forward, 50.0, 0.0, 0.7, false)] // no delay but cue 0.0: the animation has started normally, explaining the 50.0 target without fill
+ [InlineData(FillMode.Forward, 100.0, 0.0, 0.7, true )]
+ [InlineData(FillMode.Backward, 50.0, 0.3, 0.7, false)]
+ [InlineData(FillMode.Backward, 50.0, 0.3, 0.7, true )]
+ [InlineData(FillMode.Both, 50.0, 0.3, 0.7, false)]
+ [InlineData(FillMode.Both, 50.0, 0.3, 0.7, true )]
+ [InlineData(FillMode.Forward, 100.0, 0.3, 0.7, false)]
+ [InlineData(FillMode.Forward, 100.0, 0.3, 0.7, true )]
+ public void Check_FillMode_Start_Value(FillMode fillMode, double target, double startCue, double endCue, bool delay)
{
var keyframe1 = new KeyFrame()
{
- Setters = { new Setter(Layoutable.WidthProperty, 0d), }, Cue = new Cue(startCue)
+ Setters = { new Setter(Layoutable.WidthProperty, 50d), }, Cue = new Cue(startCue)
};
var keyframe2 = new KeyFrame()
@@ -152,7 +158,7 @@ namespace Avalonia.Base.UnitTests.Animation
var animation = new Animation()
{
Duration = TimeSpan.FromSeconds(10d),
- Delay = TimeSpan.FromSeconds(5d),
+ Delay = delay ? TimeSpan.FromSeconds(5d) : TimeSpan.Zero,
FillMode = fillMode,
Children = { keyframe1, keyframe2 }
};
@@ -169,28 +175,34 @@ namespace Avalonia.Base.UnitTests.Animation
}
[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)
+ [InlineData(FillMode.Backward, 100.0, 0.3, 1.0, false)]
+ [InlineData(FillMode.Backward, 100.0, 0.3, 1.0, true )]
+ [InlineData(FillMode.Both, 300.0, 0.3, 1.0, false)]
+ [InlineData(FillMode.Both, 300.0, 0.3, 1.0, true )]
+ [InlineData(FillMode.Forward, 300.0, 0.3, 1.0, false)]
+ [InlineData(FillMode.Forward, 300.0, 0.3, 1.0, true )]
+ [InlineData(FillMode.Backward, 100.0, 0.3, 0.7, false)]
+ [InlineData(FillMode.Backward, 100.0, 0.3, 0.7, true )]
+ [InlineData(FillMode.Both, 300.0, 0.3, 0.7, false)]
+ [InlineData(FillMode.Both, 300.0, 0.3, 0.7, true )]
+ [InlineData(FillMode.Forward, 300.0, 0.3, 0.7, false)]
+ [InlineData(FillMode.Forward, 300.0, 0.3, 0.7, true )]
+ public void Check_FillMode_End_Value(FillMode fillMode, double target, double startCue, double endCue, bool delay)
{
var keyframe1 = new KeyFrame()
{
- Setters = { new Setter(Layoutable.WidthProperty, 0d), }, Cue = new Cue(0.7d)
+ Setters = { new Setter(Layoutable.WidthProperty, 0d), }, Cue = new Cue(startCue)
};
var keyframe2 = new KeyFrame()
{
- Setters = { new Setter(Layoutable.WidthProperty, 300d), }, Cue = new Cue(1d)
+ Setters = { new Setter(Layoutable.WidthProperty, 300d), }, Cue = new Cue(endCue)
};
var animation = new Animation()
{
Duration = TimeSpan.FromSeconds(10d),
- Delay = TimeSpan.FromSeconds(5d),
+ Delay = delay ? TimeSpan.FromSeconds(5d) : TimeSpan.Zero,
FillMode = fillMode,
Children = { keyframe1, keyframe2 }
};
@@ -502,6 +514,96 @@ namespace Avalonia.Base.UnitTests.Animation
animationRun.Wait();
}
+ [Theory]
+ [InlineData(0, 1, 2)]
+ [InlineData(0, 2, 1)]
+ [InlineData(1, 0, 2)]
+ [InlineData(1, 2, 0)]
+ [InlineData(2, 0, 1)]
+ [InlineData(2, 1, 0)]
+ public void KeyFrames_Order_Does_Not_Matter(int index0, int index1, int index2)
+ {
+ static KeyFrame CreateKeyFrame(double width, double cue)
+ => new()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, width) },
+ Cue = new Cue(cue)
+ };
+
+ var keyFrames = new[]
+ {
+ CreateKeyFrame(100.0, 0.0),
+ CreateKeyFrame(200.0, 0.5),
+ CreateKeyFrame(300.0, 1.0)
+ };
+
+ var animation = new Animation
+ {
+ Duration = TimeSpan.FromSeconds(1.0),
+ IterationCount = new IterationCount(1),
+ Easing = new LinearEasing(),
+ FillMode = FillMode.Forward
+ };
+
+ animation.Children.Add(keyFrames[index0]);
+ animation.Children.Add(keyFrames[index1]);
+ animation.Children.Add(keyFrames[index2]);
+
+ var border = new Border
+ {
+ Height = 100.0,
+ Width = 50.0
+ };
+
+ var clock = new TestClock();
+ animation.RunAsync(border, clock);
+
+ clock.Step(TimeSpan.Zero);
+ Assert.Equal(100.0, border.Width);
+
+ clock.Step(TimeSpan.FromSeconds(0.5));
+ Assert.Equal(200.0, border.Width);
+
+ clock.Step(TimeSpan.FromSeconds(1.0));
+ Assert.Equal(300.0, border.Width);
+ }
+
+ [Theory]
+ [InlineData(0.0)]
+ [InlineData(0.5)]
+ [InlineData(1.0)]
+ public void Single_KeyFrame_Works(double cue)
+ {
+ var animation = new Animation
+ {
+ Duration = TimeSpan.FromSeconds(1.0),
+ IterationCount = new IterationCount(1),
+ Easing = new LinearEasing(),
+ FillMode = FillMode.Forward,
+ Children =
+ {
+ new KeyFrame
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 100.0) },
+ Cue = new Cue(cue)
+ }
+ }
+ };
+
+ var border = new Border
+ {
+ Height = 100.0,
+ Width = 50.0
+ };
+
+ var clock = new TestClock();
+ animation.RunAsync(border, clock);
+
+ clock.Step(TimeSpan.Zero);
+ clock.Step(TimeSpan.FromSeconds(cue));
+ Assert.Equal(100.0, border.Width);
+ }
+
private sealed class FakeAnimator : InterpolatingAnimator
{
public double LastProgress { get; set; } = double.NaN;
diff --git a/tests/Avalonia.Base.UnitTests/Animation/SpringTests.cs b/tests/Avalonia.Base.UnitTests/Animation/SpringTests.cs
index ed2c00e63c..cb1a3487be 100644
--- a/tests/Avalonia.Base.UnitTests/Animation/SpringTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Animation/SpringTests.cs
@@ -80,9 +80,9 @@ public class SpringTests
animation.RunAsync(rect, clock);
clock.Step(TimeSpan.Zero);
- Assert.Equal(rotateTransform.Angle, -2.5);
+ Assert.Equal(-2.5, rotateTransform.Angle);
clock.Step(TimeSpan.FromSeconds(5));
- Assert.Equal(rotateTransform.Angle, 5.522828945000075);
+ Assert.Equal(5.522828945000075, rotateTransform.Angle);
var tolerance = 0.01;
clock.Step(TimeSpan.Parse("00:00:10.0153932"));