From 2dfd8515a74fac33842b9025f1d666988b6d4413 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 26 Mar 2026 16:25:25 +0100 Subject: [PATCH] Use the correct value for animations stopped during a visual tree detach (#20995) * Add failing test for FillMode on visual tree detach * Update animation fill value when detached from visual tree --- .../Animation/AnimationInstance`1.cs | 15 ++++++-- .../Animation/AnimationIterationTests.cs | 38 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs index 390a4a10b4..757e5f8987 100644 --- a/src/Avalonia.Base/Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Base/Animation/AnimationInstance`1.cs @@ -148,7 +148,11 @@ namespace Avalonia.Animation } // Stop and dispose the animation when detached from the visual tree. - _detachedHandler = (_, _) => DoComplete(); + _detachedHandler = (_, _) => + { + SetFinalValue(); + DoComplete(); + }; visual.DetachedFromVisualTree += _detachedHandler; } @@ -172,6 +176,12 @@ namespace Avalonia.Animation } } + private void SetFinalValue() + { + var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0); + _lastInterpValue = _interpolator(easedTime, _neutralValue); + } + private void ApplyFinalFill() { if (_animator.Property is null) @@ -237,8 +247,7 @@ namespace Avalonia.Animation // 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); + SetFinalValue(); DoComplete(); return; } diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs index 0ca5a3be6a..b32cb5a7d1 100644 --- a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs @@ -1264,6 +1264,44 @@ namespace Avalonia.Base.UnitTests.Animation Assert.Equal(200d, border.Width); } + [Fact] + public void FillMode_Applies_Final_Value_When_Visual_Detached_During_Animation() + { + var keyframe1 = new KeyFrame + { + Setters = { new Setter(Layoutable.WidthProperty, 100d) }, + Cue = new Cue(0d) + }; + var keyframe2 = new KeyFrame + { + Setters = { new Setter(Layoutable.WidthProperty, 300d) }, + Cue = new Cue(1d) + }; + + var animation = new Animation + { + Duration = TimeSpan.FromSeconds(5), + IterationCount = new IterationCount(1), + FillMode = FillMode.Forward, + Children = { keyframe1, keyframe2 } + }; + + var border = new Border { Height = 100d, Width = 50d }; + var root = new TestRoot(border); + var clock = new TestClock(); + var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken); + + clock.Step(TimeSpan.Zero); + Assert.Equal(100d, border.Width); + + // Detach from visual tree immediately + root.Child = null; + + // The final value should be applied + Assert.True(animationRun.IsCompleted); + Assert.Equal(300d, border.Width); + } + private sealed class FakeAnimator : InterpolatingAnimator { public double LastProgress { get; set; } = double.NaN;