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