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