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 ca7566c43543..063a426011fc 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/Adapters/ItemsViewAdapter.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/Adapters/ItemsViewAdapter.cs @@ -155,6 +155,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 2ff1b4116442..ffe46b104a4f 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/ItemContentView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/ItemContentView.cs @@ -85,6 +85,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 5e691e8eb63b..937902e8e722 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs @@ -680,6 +680,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 && (Math.Abs(w - oldw) > 1 || Math.Abs(h - oldh) > 1)) + { + 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) { if (disposing) 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 7379bdbc0c06..ed768b9e0ee1 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -22,5 +22,6 @@ override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.OnH override Microsoft.Maui.Controls.SwipeItemView.IsEnabledCore.get -> bool ~override Microsoft.Maui.Controls.RadioButton.OnPropertyChanged(string propertyName = null) -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void +override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnSizeChanged(int w, int h, int oldw, int oldh) -> void ~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void Microsoft.Maui.Controls.Label.~Label() -> void 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