diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media_Animation/Given_DoubleAnimationUsingKeyFrames.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media_Animation/Given_DoubleAnimationUsingKeyFrames.cs new file mode 100644 index 0000000000000000000000000000000000000000..40c1af0c38aee53487aa66c5b1282c50cf730633 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media_Animation/Given_DoubleAnimationUsingKeyFrames.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Uno.UI.RuntimeTests.Helpers; +using Windows.UI; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Shapes; +using static Private.Infrastructure.TestServices; + +namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media_Animation; + +[TestClass] +[RunsOnUIThread] +public class Given_DoubleAnimationUsingKeyFrames +{ + [TestMethod] + public async Task When_Quickly_Transitions() + { + // We are mimicking Fluent-style ToggleSwitch's ToggleStates here. + var sut = new TestPages.QuickMultiTransitionsPage(); + WindowHelper.WindowContent = sut; + await WindowHelper.WaitForLoaded(sut); + await Task.Delay(1000); + + // Quickly play through multiple visual-states to simulate what happens when tapping(*1) on a ToggleSwitch. + /* *1: The full tapping experience is: [InitialState: Off]->Dragging->Off->On or [InitialState: A]->B->A->C + * where A=Off, B=Dragging, C=Off + * SwitchKnobOn.Opacity ToggleStates\AOff->COn 1 @[ControlFasterAnimationDuration] f=Linear + * SwitchKnobOn.Opacity ToggleStates\BDragging->AOff 0 @[ControlFasterAnimationDuration] f=Linear + * SwitchKnobOn.Opacity ToggleStates\BDragging->COn 1 @[ControlFasterAnimationDuration] f=Linear + * SwitchKnobOn.Opacity ToggleStates\COn->AOff 0 @[ControlFasterAnimationDuration] f=Linear + * SwitchKnobOn.Opacity ToggleStates\COn->BDragging 1 @0 f=Linear + * SwitchKnobOn.Opacity ToggleStates\COn 1 @[ControlFasterAnimationDuration] f=Linear + */ + VisualStateManager.GoToState(sut, TestPages.QuickMultiTransitionsPage.TestStateNames.PhaseB, useTransitions: true); + VisualStateManager.GoToState(sut, TestPages.QuickMultiTransitionsPage.TestStateNames.PhaseA, useTransitions: true); + VisualStateManager.GoToState(sut, TestPages.QuickMultiTransitionsPage.TestStateNames.PhaseC, useTransitions: true); + await WindowHelper.WaitForIdle(); + await Task.Delay(1000); + + // Given that it involves a race condition, we use a matrix of 4x4 to boost the failure rate. + // If everything went smoothly, the end result should be an opacity of 1 for all the borders. + var total = sut.RootGrid.Children.OfType().Count(); + var passed = sut.RootGrid.Children.OfType() + .Count(x => x.Opacity == 1.0); + Assert.AreEqual(total, passed, $"Only {passed} of {total} Border.Opacity is at expected value of 1.0"); + } +} diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media_Animation/TestPages/QuickMultiTransitionsPage.xaml b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media_Animation/TestPages/QuickMultiTransitionsPage.xaml new file mode 100644 index 0000000000000000000000000000000000000000..60933c4c60c0e1379f7f5a185135206cbb2e5ec9 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media_Animation/TestPages/QuickMultiTransitionsPage.xaml @@ -0,0 +1,502 @@ + + + + 00:00:00.083 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media_Animation/TestPages/QuickMultiTransitionsPage.xaml.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media_Animation/TestPages/QuickMultiTransitionsPage.xaml.cs new file mode 100644 index 0000000000000000000000000000000000000000..94c516526ad05dfcb1020760058992ba28cc7797 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media_Animation/TestPages/QuickMultiTransitionsPage.xaml.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media_Animation.TestPages; + +public sealed partial class QuickMultiTransitionsPage : Page +{ + public static class TestStateNames + { + public const string PhaseA = nameof(PhaseA); + public const string PhaseB = nameof(PhaseB); + public const string PhaseC = nameof(PhaseC); + } + + public QuickMultiTransitionsPage() + { + this.InitializeComponent(); + this.Loaded += (s, e) => VisualStateManager.GoToState(this, TestStateNames.PhaseA, useTransitions: true); + } +} diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/ColorAnimationUsingKeyFrames.cs b/src/Uno.UI/UI/Xaml/Media/Animation/ColorAnimationUsingKeyFrames.cs index e07821d00368dde8c7b148719fdd356cff17d609..2ac4179f6775af5d5687a11db7e17b216fd36f5d 100644 --- a/src/Uno.UI/UI/Xaml/Media/Animation/ColorAnimationUsingKeyFrames.cs +++ b/src/Uno.UI/UI/Xaml/Media/Animation/ColorAnimationUsingKeyFrames.cs @@ -15,6 +15,8 @@ namespace Windows.UI.Xaml.Media.Animation partial class ColorAnimationUsingKeyFrames : Timeline, ITimeline { private readonly Stopwatch _activeDuration = new Stopwatch(); + private bool _wasBeginScheduled; + private bool _wasRequestedToStop; private int _replayCount = 1; private ColorOffset? _startingValue; private ColorOffset _finalValue; @@ -79,7 +81,6 @@ namespace Windows.UI.Xaml.Media.Animation return base.GetCalculatedDuration(); } - bool _wasBeginScheduled; void ITimeline.Begin() { if (!_wasBeginScheduled) @@ -87,7 +88,9 @@ namespace Windows.UI.Xaml.Media.Animation // We dispatch the begin so that we can use bindings on ColorKeyFrame.Value from RelativeParent. // This works because the template bindings are executed just after the constructor. // WARNING: This does not allow us to bind ColorKeyFrame.Value with ViewModel properties. + _wasBeginScheduled = true; + _wasRequestedToStop = false; #if !NET461 #if __ANDROID__ @@ -97,14 +100,17 @@ namespace Windows.UI.Xaml.Media.Animation #endif #endif { - if (KeyFrames.Count < 1) + _wasBeginScheduled = false; + + if (KeyFrames.Count < 1 || // nothing to do + _wasRequestedToStop // was requested to stop, between Begin() and dispatched here + ) { - return; // nothing to do + return; } PropertyInfo?.CloneShareableObjectsInPath(); - _wasBeginScheduled = false; _activeDuration.Restart(); _replayCount = 1; @@ -207,7 +213,9 @@ namespace Windows.UI.Xaml.Media.Animation _currentAnimator.Cancel();//Stop the animator if it is running _startingValue = null; } + State = TimelineState.Stopped; + _wasRequestedToStop = true; } void ITimeline.Stop() @@ -215,7 +223,9 @@ namespace Windows.UI.Xaml.Media.Animation _currentAnimator?.Cancel(); // stop could be called before the initialization _startingValue = null; ClearValue(); + State = TimelineState.Stopped; + _wasRequestedToStop = true; } /// diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimationUsingKeyFrames.cs b/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimationUsingKeyFrames.cs index 36f072f2210443a7559c1f956f6c611b1bfc46cd..87d8a382f99dbbfdf5112941942e4af3c5a23758 100644 --- a/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimationUsingKeyFrames.cs +++ b/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimationUsingKeyFrames.cs @@ -15,6 +15,8 @@ namespace Windows.UI.Xaml.Media.Animation public partial class DoubleAnimationUsingKeyFrames : Timeline, ITimeline { private readonly Stopwatch _activeDuration = new Stopwatch(); + private bool _wasBeginScheduled; + private bool _wasRequestedToStop; private int _replayCount = 1; private double? _startingValue; private double _finalValue; @@ -55,7 +57,6 @@ namespace Windows.UI.Xaml.Media.Animation return base.GetCalculatedDuration(); } - bool _wasBeginScheduled; void ITimeline.Begin() { if (!_wasBeginScheduled) @@ -63,7 +64,10 @@ namespace Windows.UI.Xaml.Media.Animation // We dispatch the begin so that we can use bindings on DoubleKeyFrame.Value from RelativeParent. // This works because the template bindings are executed just after the constructor. // WARNING: This does not allow us to bind DoubleKeyFrame.Value with ViewModel properties. + _wasBeginScheduled = true; + _wasRequestedToStop = false; + #if !NET461 #if __ANDROID__ _ = Dispatcher.RunAnimation(() => @@ -72,11 +76,14 @@ namespace Windows.UI.Xaml.Media.Animation #endif #endif { - if (KeyFrames.Count < 1) + _wasBeginScheduled = false; + + if (KeyFrames.Count < 1 || // nothing to do + _wasRequestedToStop // was requested to stop, between Begin() and dispatched here + ) { - return; // nothing to do + return; } - _wasBeginScheduled = false; _activeDuration.Restart(); _replayCount = 1; @@ -175,7 +182,9 @@ namespace Windows.UI.Xaml.Media.Animation _currentAnimator.Cancel();//Stop the animator if it is running _startingValue = null; } + State = TimelineState.Stopped; + _wasRequestedToStop = true; } void ITimeline.Stop() @@ -183,7 +192,9 @@ namespace Windows.UI.Xaml.Media.Animation _currentAnimator?.Cancel(); // stop could be called before the initialization _startingValue = null; ClearValue(); + State = TimelineState.Stopped; + _wasRequestedToStop = true; } /// diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/ObjectAnimationUsingKeyFrames.cs b/src/Uno.UI/UI/Xaml/Media/Animation/ObjectAnimationUsingKeyFrames.cs index c8876182fc50e628a8f8a0475f55e50ff32ae112..3271ab943e37a914f7b4c98006dec9b93018be71 100644 --- a/src/Uno.UI/UI/Xaml/Media/Animation/ObjectAnimationUsingKeyFrames.cs +++ b/src/Uno.UI/UI/Xaml/Media/Animation/ObjectAnimationUsingKeyFrames.cs @@ -114,8 +114,8 @@ namespace Windows.UI.Xaml.Media.Animation ); } - // We explicitly call the Stop of the _frameScheduler befire teh Reste dispose it, - // so the EndReason will stopped instead of Aborted + // We explicitly call the Stop of the _frameScheduler before the Reset dispose it, + // so the EndReason will be Stopped instead of Aborted. _frameScheduler?.Stop(); Reset(); diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Storyboard.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Storyboard.cs index fbf25c74912a8d1737d59db6680050a096f29e4a..a10a78a7e040ba9649b319dd7ae826f54197c422 100644 --- a/src/Uno.UI/UI/Xaml/Media/Animation/Storyboard.cs +++ b/src/Uno.UI/UI/Xaml/Media/Animation/Storyboard.cs @@ -114,6 +114,7 @@ namespace Windows.UI.Xaml.Media.Animation private void Play() { + _runningChildren = 0; if (Children != null && Children.Count > 0) { for (int i = 0; i < Children.Count; i++)