diff --git a/src/Avalonia.Base/Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs index ec27978939..3be646d66c 100644 --- a/src/Avalonia.Base/Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Base/Animation/AnimationInstance`1.cs @@ -137,7 +137,8 @@ namespace Avalonia.Animation if (!_gotFirstKFValue) { - _firstKFValue = (T)_animator.First().Value!; + var firstKeyFrame = _animator.First(); + _firstKFValue = firstKeyFrame.Value is T value ? value : _neutralValue; _gotFirstKFValue = true; } } diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs index 28beb4f4e8..81f1b758a7 100644 --- a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs @@ -7,7 +7,9 @@ using Xunit; using Avalonia.Animation.Easings; using System.Threading; using System.Reactive.Linq; +using Avalonia.Data; using Avalonia.Layout; +using Avalonia.UnitTests; namespace Avalonia.Base.UnitTests.Animation { @@ -604,6 +606,110 @@ namespace Avalonia.Base.UnitTests.Animation Assert.Equal(100.0, border.Width); } + [Fact] + public void Animation_Completes_Gracefully_When_First_KeyFrame_Value_Is_Null() + { + var clock = new MockGlobalClock(); + var services = new TestServices(globalClock: clock); + + using (UnitTestApplication.Start(services)) + { + var nullBinding = new Binding("NonExistentProperty"); + + var animation = new Animation + { + Duration = TimeSpan.FromSeconds(1), + FillMode = FillMode.Both, + Children = + { + new KeyFrame + { + KeyTime = TimeSpan.FromSeconds(0), + Setters = { new Setter(Layoutable.WidthProperty, nullBinding) } + }, + new KeyFrame + { + KeyTime = TimeSpan.FromSeconds(1), + Setters = { new Setter(Layoutable.WidthProperty, 200d) } + } + } + }; + + var border = new Border { Width = 100d, Height = 100d }; + + var root = new TestRoot(border); + root.LayoutManager.ExecuteInitialLayoutPass(); + + var animationTask = animation.RunAsync(border, clock); + + // Pulse the clock - this should not throw even though + // the first keyframe's value is null (falls back to neutral value) + var exception = Record.Exception(() => clock.Pulse(TimeSpan.Zero)); + Assert.Null(exception); + + // The animation should continue running (using neutral value as fallback) + clock.Pulse(TimeSpan.FromSeconds(0.5)); + Assert.False(animationTask.IsCompleted); + + // Animation completes after its full duration + clock.Pulse(TimeSpan.FromSeconds(1)); + Assert.True(animationTask.IsCompleted); + } + } + + [Fact] + public void Animation_With_Unresolved_Binding_Does_Not_Throw_NullReferenceException() + { + // Additional test to verify the null reference fix for animator first keyframe value + + var clock = new MockGlobalClock(); + var services = new TestServices(globalClock: clock); + + using (UnitTestApplication.Start(services)) + { + // Binding to a property that doesn't exist - will evaluate to null + var binding = new Binding("MissingProperty"); + + var animation = new Animation + { + Duration = TimeSpan.FromSeconds(1), + IterationCount = new IterationCount(1), + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = { new Setter(Layoutable.WidthProperty, binding) } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = { new Setter(Layoutable.WidthProperty, 300d) } + } + } + }; + + var control = new Border { Width = 50d }; + var root = new TestRoot(control); + root.LayoutManager.ExecuteInitialLayoutPass(); + + // Start animation - the first keyframe value will be null due to unresolved binding + var task = animation.RunAsync(control, clock); + + // The fix ensures this doesn't throw NullReferenceException + // Animation falls back to neutral value and continues + clock.Pulse(TimeSpan.Zero); + clock.Pulse(TimeSpan.FromSeconds(0.1)); + + // Animation should still be running (uses neutral value as fallback) + Assert.False(task.IsCompleted); + + // Animation completes after its full duration + clock.Pulse(TimeSpan.FromSeconds(1)); + Assert.True(task.IsCompleted); + } + } + private sealed class FakeAnimator : InterpolatingAnimator { public double LastProgress { get; set; } = double.NaN;