diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs
index 5d1920de15e2..4ab71bb3e39a 100644
--- a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs
+++ b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs
@@ -1,4 +1,5 @@
-using Microsoft.Maui.Controls.Platform;
+using System;
+using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Graphics;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -333,11 +334,14 @@ void ApplyLayoutOrientation()
// Using MinWidth (not Width) preserves horizontal scrolling when content exceeds
// the viewport — DesiredSize = max(contentWidth, MinWidth), so the ScrollViewer
// still sees the full content extent for many-item scenarios.
+ bool itemsWidthChanged = false;
+ var measured = base.MeasureOverride(availableSize);
+
if (_isHorizontalLayout && _itemsRepeater is not null)
{
if (!emptyViewWillFill && !double.IsInfinity(availableSize.Width) && availableSize.Width > 0)
{
- _itemsRepeater.MinWidth = availableSize.Width;
+ itemsWidthChanged = ApplyItemsRepeaterMinWidth(availableSize);
}
else
{
@@ -353,14 +357,14 @@ void ApplyLayoutOrientation()
_itemsRepeater.ClearValue(MinWidthProperty);
}
- var measured = base.MeasureOverride(availableSize);
-
// When the EmptyView is visible, stretch it to fill the remaining viewport
// space (after Header/Footer/Items) so it occupies the entire available area.
// The EmptyView is laid out inside the StackPanel between the items repeater
// and the footer; setting MinHeight/MinWidth here pushes the Footer to the
// far edge of the viewport when empty.
- if (SizeEmptyViewToFillViewport(availableSize))
+ bool emptyViewSizeChanged = SizeEmptyViewToFillViewport(availableSize);
+
+ if (emptyViewSizeChanged || itemsWidthChanged)
{
// MinHeight/MinWidth changed — re-measure so the StackPanel picks it up.
measured = base.MeasureOverride(availableSize);
@@ -369,6 +373,37 @@ void ApplyLayoutOrientation()
return measured;
}
+ ///
+ /// Sets the _itemsRepeater MinWidth to the available viewport width minus the
+ /// Header/Footer's own widths, so items stretch to fill the remaining space.
+ /// Returns true if the MinWidth value changed.
+ ///
+ bool ApplyItemsRepeaterMinWidth(global::Windows.Foundation.Size availableSize)
+ {
+ if (_itemsRepeater is null)
+ {
+ return false;
+ }
+
+ var headerWidth = (_headerContentControl is not null && _headerContentControl.Visibility == WVisibility.Visible)
+ ? _headerContentControl.DesiredSize.Width
+ : 0;
+ var footerWidth = (_footerContentControl is not null && _footerContentControl.Visibility == WVisibility.Visible)
+ ? _footerContentControl.DesiredSize.Width
+ : 0;
+
+ var remainingWidth = availableSize.Width - headerWidth - footerWidth;
+ var newMinWidth = Math.Max(remainingWidth, 0);
+
+ if (_itemsRepeater.MinWidth == newMinWidth)
+ {
+ return false;
+ }
+
+ _itemsRepeater.MinWidth = newMinWidth;
+ return true;
+ }
+
bool SizeEmptyViewToFillViewport(global::Windows.Foundation.Size availableSize)
{
if (_emptyViewContentControl is null || _emptyViewVisibility != WVisibility.Visible)
diff --git a/src/Controls/tests/DeviceTests/Elements/Modal/ModalTests.cs b/src/Controls/tests/DeviceTests/Elements/Modal/ModalTests.cs
index c146beb97ba6..abac26a5bb5a 100644
--- a/src/Controls/tests/DeviceTests/Elements/Modal/ModalTests.cs
+++ b/src/Controls/tests/DeviceTests/Elements/Modal/ModalTests.cs
@@ -32,10 +32,7 @@ namespace Microsoft.Maui.DeviceTests
[Trait(RendererHandlerVariant.TraitName, RendererHandlerVariant.AndroidShellRenderer)] // See RendererHandlerVariant.cs
public partial class ModalTests : ControlsHandlerTestBase
{
- protected virtual void SetupBuilder() =>
- SetupBuilder(includeNavigationViewHandler: true);
-
- void SetupBuilder(bool includeNavigationViewHandler)
+ protected virtual void SetupBuilder(bool includeNavigationViewHandler = true)
{
EnsureHandlerCreated(builder =>
{
diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellHandlerSubclasses.Android.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellHandlerSubclasses.Android.cs
index 2c91281c9439..359d43b2fa9f 100644
--- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellHandlerSubclasses.Android.cs
+++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellHandlerSubclasses.Android.cs
@@ -99,7 +99,7 @@ MaterialToolbar GetShellHandlerToolbar(IElementHandler handler)
[Trait(RendererHandlerVariant.TraitName, RendererHandlerVariant.AndroidShellHandler)] // See RendererHandlerVariant.cs
public partial class ModalHandlerTests : ModalTests
{
- protected override void SetupBuilder()
+ protected override void SetupBuilder(bool includeNavigationViewHandler = true)
{
EnsureHandlerCreated(builder =>
{
diff --git a/src/Core/src/Animations/PlatformTicker.Windows.cs b/src/Core/src/Animations/PlatformTicker.Windows.cs
index 35dc7ac8e9d0..ca75114fffeb 100644
--- a/src/Core/src/Animations/PlatformTicker.Windows.cs
+++ b/src/Core/src/Animations/PlatformTicker.Windows.cs
@@ -1,13 +1,35 @@
-using Microsoft.UI.Xaml.Media;
+using System;
+using Microsoft.UI.Dispatching;
+using Microsoft.UI.Xaml.Media;
+using ViewManagement = Windows.UI.ViewManagement;
namespace Microsoft.Maui.Animations
{
///
- public class PlatformTicker : Ticker
+ public class PlatformTicker : Ticker, IDisposable
{
+ readonly ViewManagement.UISettings _uiSettings = new();
+ readonly DispatcherQueue? _dispatcherQueue;
+ bool _disposed;
+
+ ///
+ /// Creates a new Windows that respects the "Show animations in Windows" accessibility setting.
+ ///
+ public PlatformTicker()
+ {
+ _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
+ SystemEnabled = _uiSettings.AnimationsEnabled;
+ _uiSettings.AnimationsEnabledChanged += OnAnimationsEnabledChanged;
+ }
+
///
public override void Start()
{
+ if (_disposed)
+ {
+ return;
+ }
+
CompositionTarget.Rendering += RenderingFrameEventHandler;
}
@@ -17,6 +39,49 @@ public override void Stop()
CompositionTarget.Rendering -= RenderingFrameEventHandler;
}
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ if (disposing)
+ {
+ Stop();
+ _uiSettings.AnimationsEnabledChanged -= OnAnimationsEnabledChanged;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ void OnAnimationsEnabledChanged(ViewManagement.UISettings sender, ViewManagement.UISettingsAnimationsEnabledChangedEventArgs args)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (_dispatcherQueue is not null)
+ {
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ if (!_disposed)
+ {
+ SystemEnabled = _uiSettings.AnimationsEnabled;
+ }
+ });
+ }
+ }
+
void RenderingFrameEventHandler(object? sender, object? args)
{
Fire?.Invoke();
diff --git a/src/Core/src/Animations/PlatformTicker.iOS.cs b/src/Core/src/Animations/PlatformTicker.iOS.cs
index 8f6346d95ee0..758f12379de0 100644
--- a/src/Core/src/Animations/PlatformTicker.iOS.cs
+++ b/src/Core/src/Animations/PlatformTicker.iOS.cs
@@ -1,12 +1,28 @@
-using CoreAnimation;
+using System;
+using CoreAnimation;
using Foundation;
+using UIKit;
namespace Microsoft.Maui.Animations
{
///
- public class PlatformTicker : Ticker
+ public class PlatformTicker : Ticker, IDisposable
{
CADisplayLink? _link;
+ NSObject? _observer;
+ bool _disposed;
+
+ ///
+ /// Creates a new iOS/MacCatalyst that respects the Reduce Motion accessibility setting.
+ ///
+ public PlatformTicker()
+ {
+ SystemEnabled = !UIAccessibility.IsReduceMotionEnabled;
+
+ _observer = NSNotificationCenter.DefaultCenter.AddObserver(
+ UIApplication.ReduceMotionStatusDidChangeNotification,
+ OnReduceMotionStatusChanged);
+ }
///
public override bool IsRunning =>
@@ -15,8 +31,10 @@ public class PlatformTicker : Ticker
///
public override void Start()
{
- if (_link != null)
+ if (_disposed || _link is not null)
+ {
return;
+ }
_link = CADisplayLink.Create(() => Fire?.Invoke());
_link.AddToRunLoop(NSRunLoop.Current, NSRunLoopMode.Common);
@@ -32,5 +50,45 @@ public override void Stop()
_link?.Dispose();
_link = null;
}
+
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ if (disposing)
+ {
+ Stop();
+
+ if (_observer is not null)
+ {
+ NSNotificationCenter.DefaultCenter.RemoveObserver(_observer);
+ _observer.Dispose();
+ _observer = null;
+ }
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ void OnReduceMotionStatusChanged(NSNotification notification)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ SystemEnabled = !UIAccessibility.IsReduceMotionEnabled;
+ }
}
}
\ No newline at end of file
diff --git a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
index c380a68e3243..31a694f9d1dc 100644
--- a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
@@ -41,6 +41,8 @@ static Microsoft.Maui.Handlers.EntryHandler.MapBackground(Microsoft.Maui.Handler
static Microsoft.Maui.Handlers.RadioButtonHandler.MapBackground(Microsoft.Maui.Handlers.IRadioButtonHandler! handler, Microsoft.Maui.IRadioButton! radioButton) -> void
static Microsoft.Maui.Handlers.WindowHandler.MapStatusBarTheme(Microsoft.Maui.Handlers.IWindowHandler! handler, Microsoft.Maui.IWindow! window) -> void
static Microsoft.Maui.SafeAreaEdges.Container.get -> Microsoft.Maui.SafeAreaEdges
+Microsoft.Maui.Animations.PlatformTicker.Dispose() -> void
+virtual Microsoft.Maui.Animations.PlatformTicker.Dispose(bool disposing) -> void
static Microsoft.Maui.Handlers.TimePickerHandler.MapFlowDirection(Microsoft.Maui.Handlers.ITimePickerHandler! handler, Microsoft.Maui.ITimePicker! timePicker) -> void
static Microsoft.Maui.Handlers.StepperHandler.MapFlowDirection(Microsoft.Maui.Handlers.IStepperHandler! handler, Microsoft.Maui.IStepper! stepper) -> void
static Microsoft.Maui.Handlers.ButtonHandler.MapBackground(Microsoft.Maui.Handlers.IButtonHandler! handler, Microsoft.Maui.IButton! button) -> void
diff --git a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
index 29d64e8e3873..d4fec52c7eaa 100644
--- a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
@@ -40,6 +40,8 @@ static Microsoft.Maui.Handlers.EntryHandler.MapBackground(Microsoft.Maui.Handler
static Microsoft.Maui.Handlers.RadioButtonHandler.MapBackground(Microsoft.Maui.Handlers.IRadioButtonHandler! handler, Microsoft.Maui.IRadioButton! radioButton) -> void
static Microsoft.Maui.Handlers.WindowHandler.MapStatusBarTheme(Microsoft.Maui.Handlers.IWindowHandler! handler, Microsoft.Maui.IWindow! window) -> void
static Microsoft.Maui.SafeAreaEdges.Container.get -> Microsoft.Maui.SafeAreaEdges
+Microsoft.Maui.Animations.PlatformTicker.Dispose() -> void
+virtual Microsoft.Maui.Animations.PlatformTicker.Dispose(bool disposing) -> void
static Microsoft.Maui.Handlers.StepperHandler.MapFlowDirection(Microsoft.Maui.Handlers.IStepperHandler! handler, Microsoft.Maui.IStepper! stepper) -> void
static Microsoft.Maui.Handlers.ShapeViewHandler.MapFlowDirection(Microsoft.Maui.Handlers.IShapeViewHandler! handler, Microsoft.Maui.IShapeView! shapeView) -> void
static Microsoft.Maui.Handlers.SearchBarHandler.MapCursorPosition(Microsoft.Maui.Handlers.ISearchBarHandler! handler, Microsoft.Maui.ISearchBar! searchBar) -> void
diff --git a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
index d82e8e548dc4..e7878b660915 100644
--- a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
@@ -32,6 +32,8 @@ Microsoft.Maui.ITab.Title.get -> string!
Microsoft.Maui.TabBarPlacement
Microsoft.Maui.TabBarPlacement.Bottom = 1 -> Microsoft.Maui.TabBarPlacement
Microsoft.Maui.TabBarPlacement.Top = 0 -> Microsoft.Maui.TabBarPlacement
+Microsoft.Maui.Animations.PlatformTicker.Dispose() -> void
+virtual Microsoft.Maui.Animations.PlatformTicker.Dispose(bool disposing) -> void
static Microsoft.Maui.GridLength.implicit operator Microsoft.Maui.GridLength(string! value) -> Microsoft.Maui.GridLength
static Microsoft.Maui.SafeAreaEdges.Container.get -> Microsoft.Maui.SafeAreaEdges
override Microsoft.Maui.Platform.ContentPanel.MeasureOverride(Windows.Foundation.Size availableSize) -> Windows.Foundation.Size