diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs index 2a9709fb6045..87cfb828150a 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Text; +using CoreGraphics; using Foundation; using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; @@ -67,7 +68,11 @@ private protected override UIView OnCreatePlatformView() protected abstract ItemsViewController2 CreateController(TItemsView newElement, UICollectionViewLayout layout); - protected override UIView CreatePlatformView() => Controller?.View; + protected override UIView CreatePlatformView() + { + var controllerView = Controller?.View ?? throw new InvalidOperationException("ItemsViewController2's view should not be null at this point."); + return controllerView; + } public static void MapItemsSource(ItemsViewHandler2 handler, ItemsView itemsView) { @@ -175,35 +180,20 @@ protected bool IsIndexPathValid(NSIndexPath indexPath) return true; } - // public override Size GetDesiredSize(double widthConstraint, double heightConstraint) - // { - // var size = base.GetDesiredSize(widthConstraint, heightConstraint); - - // var potentialContentSize = Controller.GetSize(); - - - // System.Diagnostics.Debug.WriteLine($"potentialContentSize: {potentialContentSize}"); - // // If contentSize comes back null, it means none of the content has been realized yet; - // // we need to return the expansive size the collection view wants by default to get - // // it to start measuring its content - // if (potentialContentSize == null) - // { - // return size; - // } - - // var contentSize = potentialContentSize.Value; - - // // If contentSize does have a value, our target size is the smaller of it and the constraints + public override Size GetDesiredSize(double widthConstraint, double heightConstraint) + { + var contentSize = Controller.GetSize(); - // size.Width = contentSize.Width <= widthConstraint ? contentSize.Width : widthConstraint; - // size.Height = contentSize.Height <= heightConstraint ? contentSize.Height : heightConstraint; + // Our target size is the smaller of it and the constraints + var width = contentSize.Width <= widthConstraint ? contentSize.Width : widthConstraint; + var height = contentSize.Height <= heightConstraint ? contentSize.Height : heightConstraint; - // var virtualView = this.VirtualView as IView; + IView virtualView = VirtualView; - // size.Width = ViewHandlerExtensions.ResolveConstraints(size.Width, virtualView.Width, virtualView.MinimumWidth, virtualView.MaximumWidth); - // size.Height = ViewHandlerExtensions.ResolveConstraints(size.Height, virtualView.Height, virtualView.MinimumHeight, virtualView.MaximumHeight); + width = ViewHandlerExtensions.ResolveConstraints(width, virtualView.Width, virtualView.MinimumWidth, virtualView.MaximumWidth); + height = ViewHandlerExtensions.ResolveConstraints(height, virtualView.Height, virtualView.MinimumHeight, virtualView.MaximumHeight); - // return size; - // } + return new Size(width, height); + } } } diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs index 576c3c39174a..e9a86e6aac4c 100644 --- a/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs +++ b/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs @@ -39,6 +39,7 @@ public abstract class ItemsViewController2 : UICollectionViewControl UIView _emptyUIView; VisualElement _emptyViewFormsElement; List _cellReuseIds = new List(); + CGSize _previousContentSize; [UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in test: MemoryTests.HandlerDoesNotLeak")] protected UICollectionViewDelegateFlowLayout Delegator { get; set; } @@ -190,6 +191,7 @@ public override void ViewWillLayoutSubviews() { base.ViewWillLayoutSubviews(); LayoutEmptyView(); + InvalidateMeasureIfContentSizeChanged(); } void Items.MauiCollectionView.ICustomMauiCollectionViewDelegate.MovedToWindow(UIView view) @@ -319,6 +321,63 @@ protected virtual CGRect DetermineEmptyViewFrame() CollectionView.Frame.Width, CollectionView.Frame.Height); } + void InvalidateMeasureIfContentSizeChanged() + { + if (CollectionView?.CollectionViewLayout?.CollectionViewContentSize is not { } contentSize) + { + return; + } + + var previousContentSize = _previousContentSize; + _previousContentSize = contentSize; + + bool widthChanged = previousContentSize.Width != contentSize.Width; + bool heightChanged = previousContentSize.Height != contentSize.Height; + + if (_initialized && (widthChanged || heightChanged)) + { + if (CollectionView?.Bounds is not { } bounds) + { + return; + } + + var cvWidth = bounds.Width; + var cvHeight = bounds.Height; + bool invalidate = false; + + // If both the previous content size and the current content size are larger + // than the UICollectionView size, then we know that we're already maxed out and the + // CollectionView items are scrollable. There's no reason to force an invalidation + // of the CollectionView to expand/contract it. + + // If either size is smaller than that, we need to invalidate to ensure that the + // CollectionView is re-measured and set to the correct size. + + if (widthChanged && (contentSize.Width <= cvWidth || previousContentSize.Width <= cvWidth)) + { + invalidate = true; + } + else if (heightChanged && (contentSize.Height <= cvHeight || previousContentSize.Height <= cvHeight)) + { + invalidate = true; + } + + if (invalidate) + { + (ItemsView as IView)?.InvalidateMeasure(); + } + } + } + + internal Size GetSize() + { + if (_emptyViewDisplayed) + { + return _emptyUIView.Frame.Size.ToSize(); + } + + return CollectionView.CollectionViewLayout.CollectionViewContentSize.ToSize(); + } internal void UpdateView(object view, DataTemplate viewTemplate, ref UIView uiView, ref VisualElement formsElement) { diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 32a7ac55be27..b04257ee5c13 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -1,5 +1,6 @@ #nullable enable Microsoft.Maui.Controls.StyleableElement.Style.get -> Microsoft.Maui.Controls.Style? +override Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size ~abstract Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.CreateController(TItemsView newElement, UIKit.UICollectionViewLayout layout) -> Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2 ~abstract Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.SelectLayout() -> UIKit.UICollectionViewLayout *REMOVED*~Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.ShellScrollViewTracker(Microsoft.Maui.IPlatformViewHandler renderer) -> void @@ -63,7 +64,6 @@ Microsoft.Maui.Controls.StyleableElement.Style.get -> Microsoft.Maui.Controls.St *REMOVED*~Microsoft.Maui.Controls.NavigableElement.StyleClass.set -> void Microsoft.Maui.Controls.HybridWebView.SetInvokeJavaScriptTarget(T! target) -> void ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void - ~Microsoft.Maui.Controls.WebViewProcessTerminatedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewProcessTerminatedEventArgs ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.RequireServiceAttribute(System.Type[] serviceTypes) -> void ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 32a7ac55be27..3afad77d9139 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -1,5 +1,6 @@ #nullable enable Microsoft.Maui.Controls.StyleableElement.Style.get -> Microsoft.Maui.Controls.Style? +override Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size ~abstract Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.CreateController(TItemsView newElement, UIKit.UICollectionViewLayout layout) -> Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2 ~abstract Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.SelectLayout() -> UIKit.UICollectionViewLayout *REMOVED*~Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.ShellScrollViewTracker(Microsoft.Maui.IPlatformViewHandler renderer) -> void diff --git a/src/Controls/tests/DeviceTests/Elements/ContentView/ContentViewTests.cs b/src/Controls/tests/DeviceTests/Elements/ContentView/ContentViewTests.cs index 258939f5580b..6497f1dbd283 100644 --- a/src/Controls/tests/DeviceTests/Elements/ContentView/ContentViewTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/ContentView/ContentViewTests.cs @@ -28,17 +28,16 @@ void SetupBuilder() public async Task ControlTemplateUpdates() { SetupBuilder(); - var child = new Label { Text = "Content 1" }; - var contentView = new Microsoft.Maui.Controls.ContentView(); - var header = new Label { Text = "Header" }; - var footer = new Label { Text = "Footer" }; - var presenter = new ContentPresenter(); - var grid = new Grid(); - - var contentViewHandler = await CreateHandlerAsync(contentView); await InvokeOnMainThreadAsync(() => { + var child = new Label { Text = "Content 1" }; + var contentView = new Microsoft.Maui.Controls.ContentView(); + var header = new Label { Text = "Header" }; + var footer = new Label { Text = "Footer" }; + var presenter = new ContentPresenter(); + var grid = new Grid(); + var contentViewHandler = CreateHandler(contentView); contentView.Content = child; Assert.True(GetChildCount(contentViewHandler) == 1); Assert.True(GetContentChildCount(contentViewHandler) == 0); diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/NoScrollbarsTest.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/NoScrollbarsTest.png index c93d6a47c329..104206c08550 100644 Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/NoScrollbarsTest.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/NoScrollbarsTest.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue26629.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue26629.cs new file mode 100644 index 000000000000..cd5d6fdf8103 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue26629.cs @@ -0,0 +1,70 @@ +namespace Maui.Controls.Sample.Issues +{ + [Issue(IssueTracker.Github, 26629, "ScrollView resizes when content changes", PlatformAffected.All)] + public class Issue26629 : ContentPage + { + public Issue26629() + { + var grid = new Grid + { + Margin = 40, + RowSpacing = 16, + BackgroundColor = Colors.Beige, + RowDefinitions = new RowDefinitionCollection( + new RowDefinition(GridLength.Auto), + new RowDefinition(GridLength.Auto), + new RowDefinition(GridLength.Auto), + new RowDefinition(GridLength.Star)), + ColumnDefinitions = new ColumnDefinitionCollection(new ColumnDefinition(GridLength.Star)), + }; + var scrollView = new ScrollView { AutomationId = "TheScrollView" }; + var scrollViewVsl = new VerticalStackLayout(); + var button = new Button + { + Text = "Add Label", + AutomationId = "AddLabelButton", + }; + + var sizeLabel = new Label + { + AutomationId = "SizeLabel", + }; + sizeLabel.SetBinding(Label.TextProperty, new Binding(nameof(View.Height), source: scrollView)); + + var scrollOffsetLabel = new Label + { + AutomationId = "ScrollOffsetLabel", + }; + scrollOffsetLabel.SetBinding(Label.TextProperty, new Binding(nameof(ScrollView.ScrollY), source: scrollView)); + + var i = 0; + scrollView.BackgroundColor = Colors.LightBlue; + scrollView.Padding = 16; + scrollView.VerticalOptions = LayoutOptions.Start; + scrollViewVsl.Children.Add(CreateLabel("Label0")); + button.Clicked += (sender, args) => + { + scrollViewVsl.Children.Add(CreateLabel($"Label{++i}")); + }; + + scrollView.Content = scrollViewVsl; + grid.Add(button, 0, 0); + grid.Add(sizeLabel, 0, 1); + grid.Add(scrollOffsetLabel, 0, 2); + grid.Add(scrollView, 0, 3); + + Content = grid; + } + + static Label CreateLabel(string automationId) + { + return new Label + { + Text = "Huge", + FontSize = 100, + BackgroundColor = Colors.SlateBlue, + AutomationId = automationId, + }; + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issues17801.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issues17801.xaml index b7adb7b484e9..23f53a23f33e 100644 --- a/src/Controls/tests/TestCases.HostApp/Issues/Issues17801.xaml +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issues17801.xaml @@ -1,31 +1,35 @@  - - - - -