diff --git a/src/Core/src/Animations/PlatformTicker.Windows.cs b/src/Core/src/Animations/PlatformTicker.Windows.cs index 35dc7ac8e9d0..92625ca13f66 100644 --- a/src/Core/src/Animations/PlatformTicker.Windows.cs +++ b/src/Core/src/Animations/PlatformTicker.Windows.cs @@ -5,15 +5,32 @@ namespace Microsoft.Maui.Animations /// public class PlatformTicker : Ticker { + bool _isRunning; + + /// + public override bool IsRunning => _isRunning; + /// public override void Start() { + if (_isRunning) + { + return; + } + + _isRunning = true; CompositionTarget.Rendering += RenderingFrameEventHandler; } /// public override void Stop() { + if (!_isRunning) + { + return; + } + + _isRunning = false; CompositionTarget.Rendering -= RenderingFrameEventHandler; } diff --git a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 4151228a7928..6fa37cf09b9f 100644 --- a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ #nullable enable +override Microsoft.Maui.Animations.PlatformTicker.IsRunning.get -> bool override Microsoft.Maui.Platform.LayoutPanel.OnCreateAutomationPeer() -> Microsoft.UI.Xaml.Automation.Peers.AutomationPeer! override Microsoft.Maui.Platform.MauiPasswordTextBox.OnCreateAutomationPeer() -> Microsoft.UI.Xaml.Automation.Peers.AutomationPeer! override Microsoft.Maui.Platform.ContentPanel.MeasureOverride(Windows.Foundation.Size availableSize) -> Windows.Foundation.Size diff --git a/src/Core/tests/DeviceTests/Animations/PlatformTickerTests.Windows.cs b/src/Core/tests/DeviceTests/Animations/PlatformTickerTests.Windows.cs new file mode 100644 index 000000000000..b4dbd9bf3dce --- /dev/null +++ b/src/Core/tests/DeviceTests/Animations/PlatformTickerTests.Windows.cs @@ -0,0 +1,73 @@ +using System.Threading.Tasks; +using Microsoft.Maui.Animations; +using Xunit; + +namespace Microsoft.Maui.DeviceTests +{ + [Category(TestCategory.Animation)] + public class PlatformTickerTests : TestBase + { + [Fact] + public async Task IsRunning_ReflectsStartAndStop() + { + await InvokeOnMainThreadAsync(() => + { + var ticker = new PlatformTicker(); + + Assert.False(ticker.IsRunning, "Should be false before Start"); + + ticker.Start(); + Assert.True(ticker.IsRunning, "Should be true after Start"); + + ticker.Stop(); + Assert.False(ticker.IsRunning, "Should be false after Stop"); + }); + } + + [Fact] + public async Task Start_IsIdempotent_NoDuplicateSubscriptions() + { + await InvokeOnMainThreadAsync(async () => + { + var ticker = new PlatformTicker(); + int fireCount = 0; + ticker.Fire = () => fireCount++; + + ticker.Start(); + ticker.Start(); // second call should be a no-op + ticker.Start(); // third call should be a no-op + + // Yield a few composition frames + await Task.Delay(100); + + ticker.Stop(); + var firesAfterStop = fireCount; + + await Task.Delay(50); + + // No additional fires after Stop + Assert.Equal(firesAfterStop, fireCount); + // ~6 expected at 60Hz over 100ms; 3x subscriptions would yield ~18+ + Assert.InRange(firesAfterStop, 1, 20); + }); + } + + [Fact] + public async Task AnimationManager_StartsTickerOnce_AcrossMultipleAdds() + { + await InvokeOnMainThreadAsync(() => + { + var ticker = new PlatformTicker(); + var manager = new AnimationManager(ticker); + + for (int i = 0; i < 5; i++) + manager.Add(new Animation { Duration = 1.0 }); + + Assert.True(ticker.IsRunning); + + ticker.Stop(); + Assert.False(ticker.IsRunning); + }); + } + } +} diff --git a/src/Core/tests/DeviceTests/TestCategory.cs b/src/Core/tests/DeviceTests/TestCategory.cs index 3c0f2ebdadcc..3b736e46e89c 100644 --- a/src/Core/tests/DeviceTests/TestCategory.cs +++ b/src/Core/tests/DeviceTests/TestCategory.cs @@ -51,5 +51,6 @@ public static class TestCategory public const string View = "View"; public const string WebView = "WebView"; public const string Window = "Window"; + public const string Animation = "Animation"; } } \ No newline at end of file