From 5b6d84478a61e3dcd9d2d8970efb3e3cec656293 Mon Sep 17 00:00:00 2001 From: SyedAbdulAzeem Date: Tue, 10 Mar 2026 16:55:26 +0530 Subject: [PATCH 1/3] Fix CollectionView contents not sizing correctly after orientation change --- .../Android/Adapters/ItemsViewAdapter.cs | 8 +++ .../Adapters/StructuredItemsViewAdapter.cs | 5 ++ .../Handlers/Items/Android/ItemContentView.cs | 11 ++++ .../Items/Android/MauiRecyclerView.cs | 29 ++++++++++ .../TestCases.HostApp/Issues/Issue19667.cs | 53 +++++++++++++++++++ .../Tests/Issues/Issue19667.cs | 49 +++++++++++++++++ 6 files changed, 155 insertions(+) create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue19667.cs create mode 100644 src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19667.cs diff --git a/src/Controls/src/Core/Handlers/Items/Android/Adapters/ItemsViewAdapter.cs b/src/Controls/src/Core/Handlers/Items/Android/Adapters/ItemsViewAdapter.cs index 43408e333924..ea9a7811d92b 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/Adapters/ItemsViewAdapter.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/Adapters/ItemsViewAdapter.cs @@ -152,6 +152,14 @@ protected virtual void BindTemplatedItemViewHolder(TemplatedItemViewHolder templ templatedItemViewHolder.Bind(context, ItemsView); } + /// + /// Clears any cached item size used by the MeasureFirstItem sizing strategy. + /// Called when the RecyclerView's size changes (e.g., after an orientation change). + /// + internal virtual void ClearMeasureCache() + { + } + void UpdateUsingItemTemplate() { _usingItemTemplate = ItemsView.ItemTemplate != null; diff --git a/src/Controls/src/Core/Handlers/Items/Android/Adapters/StructuredItemsViewAdapter.cs b/src/Controls/src/Core/Handlers/Items/Android/Adapters/StructuredItemsViewAdapter.cs index 42c8055d9a73..109c2681cff6 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/Adapters/StructuredItemsViewAdapter.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/Adapters/StructuredItemsViewAdapter.cs @@ -169,5 +169,10 @@ void SetStaticSize(Size size) { _size = size; } + + internal override void ClearMeasureCache() + { + _size = null; + } } } diff --git a/src/Controls/src/Core/Handlers/Items/Android/ItemContentView.cs b/src/Controls/src/Core/Handlers/Items/Android/ItemContentView.cs index c07bbfa766fd..6c92a62a597b 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/ItemContentView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/ItemContentView.cs @@ -83,6 +83,17 @@ internal void HandleItemSizingStrategy(Action reportMeasure, Size? size) _pixelSize = size; } + /// + /// Invalidates the cached size so the next measure pass will re-measure the content. + /// Called when the parent RecyclerView's size changes (e.g., after orientation change). + /// + internal void InvalidateCachedSize() + { + _pixelSize = null; + _previousPixelWidth = -1; + _previousPixelHeight = -1; + } + protected override void OnLayout(bool changed, int l, int t, int r, int b) { if (Content == null) diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs index 288727d264c2..291291977a2b 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs @@ -574,6 +574,35 @@ protected override void OnLayout(bool changed, int l, int t, int r, int b) _scrollHelper?.AdjustScroll(); } + protected override void OnSizeChanged(int w, int h, int oldw, int oldh) + { + base.OnSizeChanged(w, h, oldw, oldh); + + // When the RecyclerView's size changes (e.g., after an orientation change while + // the CollectionView was not visible), we need to invalidate all visible item views + // to ensure they are re-measured with the new dimensions. + if (oldw > 0 && oldh > 0 && (w != oldw || h != oldh)) + { + InvalidateItemMeasures(); + } + } + + void InvalidateItemMeasures() + { + // Clear the adapter's static size cache (used by MeasureFirstItem strategy) + ItemsViewAdapter?.ClearMeasureCache(); + + // Force all visible children to invalidate their cached sizes and re-measure + for (int i = 0; i < ChildCount; i++) + { + if (GetChildAt(i) is ItemContentView itemContentView) + { + itemContentView.InvalidateCachedSize(); + itemContentView.ForceLayout(); + } + } + } + protected override void Dispose(bool disposing) { base.Dispose(disposing); diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue19667.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue19667.cs new file mode 100644 index 000000000000..273941164db6 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue19667.cs @@ -0,0 +1,53 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 19667, "CollectionView contents not sizing correctly after orientation change", PlatformAffected.Android)] +public class Issue19667 : TestShell +{ + protected override void Init() + { + ContentPage page1 = new ContentPage + { + Content = new Label + { + Text = "Page 1", + AutomationId = "Page1Label", + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center + } + }; + + var items = Enumerable.Range(0, 15).ToList(); + + CollectionView collectionView = new CollectionView + { + AutomationId = "CollectionView19667", + ItemsSource = items, + ItemTemplate = new DataTemplate(() => + { + Label label = new Label + { + VerticalTextAlignment = TextAlignment.Center + }; + label.SetBinding(Label.TextProperty, new Binding(".", stringFormat: "Item {0}")); + label.SetBinding(AutomationIdProperty, new Binding(".", stringFormat: "CvItem{0}")); + + Grid grid = new Grid + { + BackgroundColor = Colors.Beige, + Padding = new Thickness(20), + Margin = new Thickness(5, 2) + }; + grid.Add(label); + return grid; + }) + }; + + ContentPage page2 = new ContentPage + { + Content = collectionView + }; + + AddFlyoutItem(page1, "Page1"); + AddFlyoutItem(page2, "CollectionViewPage"); + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19667.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19667.cs new file mode 100644 index 000000000000..c317ccbb3da8 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19667.cs @@ -0,0 +1,49 @@ +#if ANDROID || IOS // The test fails on Windows and MacCatalyst because the SetOrientation method, which is intended to change the device orientation, is only supported on mobile platforms iOS and Android. +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue19667 : _IssuesUITest +{ + public Issue19667(TestDevice device) : base(device) + { + } + + public override string Issue => "CollectionView contents not sizing correctly after orientation change"; + + [Test] + [Category(UITestCategories.CollectionView)] + public void CollectionViewItemsSizeCorrectlyAfterOrientationChange() + { + App.TapShellFlyoutIcon(); + App.Tap("CollectionViewPage"); + App.WaitForElement("CollectionView19667"); + App.WaitForElement("CvItem0"); + + var portraitWidth = App.WaitForElement("CvItem0").GetRect().Width; + + App.TapShellFlyoutIcon(); + App.Tap("Page1"); + App.WaitForElement("Page1Label"); + + App.SetOrientationLandscape(); + + App.TapShellFlyoutIcon(); + App.Tap("CollectionViewPage"); + App.WaitForElement("CollectionView19667"); + App.WaitForElement("CvItem0"); + + var landscapeWidth = App.WaitForElement("CvItem0").GetRect().Width; + Assert.That(landscapeWidth, Is.GreaterThan(portraitWidth), + "CollectionView items should resize to landscape width after orientation change."); + } + + [TearDown] + public void TearDown() + { + App.SetOrientationPortrait(); + } +} +#endif From 309de37ac18fe916b3468a98537a7b30e13ecced Mon Sep 17 00:00:00 2001 From: SyedAbdulAzeem Date: Wed, 11 Mar 2026 13:58:01 +0530 Subject: [PATCH 2/3] Use tolerance when comparing old and new size values in OnSizeChanged --- .../src/Core/Handlers/Items/Android/MauiRecyclerView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs index 291291977a2b..ca750548cea0 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs @@ -581,7 +581,7 @@ protected override void OnSizeChanged(int w, int h, int oldw, int oldh) // When the RecyclerView's size changes (e.g., after an orientation change while // the CollectionView was not visible), we need to invalidate all visible item views // to ensure they are re-measured with the new dimensions. - if (oldw > 0 && oldh > 0 && (w != oldw || h != oldh)) + if (oldw > 0 && oldh > 0 && (Math.Abs(w - oldw) > 1 || Math.Abs(h - oldh) > 1)) { InvalidateItemMeasures(); } From a57a2fd8d2d67871b09613b98b625aa6787fca65 Mon Sep 17 00:00:00 2001 From: SyedAbdulAzeem Date: Fri, 22 May 2026 12:11:30 +0530 Subject: [PATCH 3/3] Update PublicAPI.Unshipped.txt --- .../src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt index d7e793dd6c1c..d976770b8537 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -11,3 +11,4 @@ override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.OnH ~override Microsoft.Maui.Controls.Handlers.Items.RecyclerViewScrollListener.OnScrollStateChanged(AndroidX.RecyclerView.Widget.RecyclerView recyclerView, int newState) -> void ~override Microsoft.Maui.Controls.Handlers.Items.SelectableItemsViewAdapter.IsSelectionEnabled(Android.Views.ViewGroup parent, int viewType) -> bool override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void +override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnSizeChanged(int w, int h, int oldw, int oldh) -> void