diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue12131.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue12131.cs new file mode 100644 index 000000000000..333272103b1b --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue12131.cs @@ -0,0 +1,51 @@ +namespace Maui.Controls.Sample.Issues +{ + [Issue(IssueTracker.Github, 12131, "RefreshView - CollectionView sizing not working correctly inside VerticalStackLayout", PlatformAffected.Android)] + public class Issue12131 : ContentPage + { + public Issue12131() + { + var items = Enumerable.Range(1, 20).Select(i => $"Item {i}").ToList(); + + var collectionView = new CollectionView + { + AutomationId = "CollectionView12131", + ItemTemplate = new DataTemplate(() => + { + var label = new Label(); + label.SetBinding(Label.TextProperty, "."); + return label; + }), + ItemsSource = items + }; + + var refreshView = new RefreshView + { + AutomationId = "RefreshView12131", + Content = collectionView + }; + + var sizeLabel = new Label + { + AutomationId = "SizeLabel12131", + Text = "Waiting..." + }; + + refreshView.SizeChanged += (s, e) => + { + sizeLabel.Text = $"Height={refreshView.Height:F0}"; + }; + + Content = new VerticalStackLayout + { + Padding = 10, + Children = + { + new Label { Text = "CollectionView wrapped within RefreshView." }, + sizeLabel, + refreshView + } + }; + } + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12131.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12131.cs new file mode 100644 index 000000000000..1ae0b8042583 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12131.cs @@ -0,0 +1,32 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues +{ + public class Issue12131 : _IssuesUITest + { + public Issue12131(TestDevice testDevice) : base(testDevice) { } + + public override string Issue => "RefreshView - CollectionView sizing not working correctly inside VerticalStackLayout"; + + [Test] + [Category(UITestCategories.RefreshView)] + public void RefreshViewHasNonZeroHeightInVerticalStackLayout() + { + App.WaitForElement("RefreshView12131"); + var rect = App.FindElement("RefreshView12131").GetRect(); + Assert.That(rect.Height, Is.GreaterThan(0), "RefreshView should have a non-zero height when placed inside a VerticalStackLayout"); + } + + [Test] + [Category(UITestCategories.RefreshView)] + public void CollectionViewDoesNotExceedAvailableWidth() + { + App.WaitForElement("CollectionView12131"); + var refreshRect = App.FindElement("RefreshView12131").GetRect(); + var collectionRect = App.FindElement("CollectionView12131").GetRect(); + Assert.That(collectionRect.Width, Is.LessThanOrEqualTo(refreshRect.Width + 1), "CollectionView width should not exceed RefreshView width"); + } + } +} diff --git a/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs b/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs index 3f9734bb143a..1d2553dde335 100644 --- a/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs +++ b/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs @@ -39,6 +39,9 @@ public ICrossPlatformLayout? CrossPlatformLayout public override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Always call base.OnMeasure — unlike ContentViewGroup/LayoutViewGroup, + // SwipeRefreshLayout.onMeasure internally measures its spinner indicator + // (mCircleView). Skipping this leaves the spinner at 0x0, making it invisible. base.OnMeasure(widthMeasureSpec, heightMeasureSpec); if (CrossPlatformLayout is null) @@ -49,11 +52,33 @@ public override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) var deviceIndependentWidth = widthMeasureSpec.ToDouble(_context); var deviceIndependentHeight = heightMeasureSpec.ToDouble(_context); - CrossPlatformLayout?.CrossPlatformMeasure(deviceIndependentWidth, deviceIndependentHeight); + var widthMode = MeasureSpec.GetMode(widthMeasureSpec); + var heightMode = MeasureSpec.GetMode(heightMeasureSpec); + + var measure = CrossPlatformLayout.CrossPlatformMeasure(deviceIndependentWidth, deviceIndependentHeight); + + // Unlike ContentViewGroup/LayoutViewGroup, SwipeRefreshLayout internally positions + // its spinner at getMeasuredWidth()/2. We must use the full spec size for both + // Exactly and AtMost modes (matching View.getDefaultSize behavior) so the spinner + // is centered correctly. Only for Unspecified do we use the cross-platform measure. + var width = widthMode == MeasureSpecMode.Unspecified ? measure.Width : deviceIndependentWidth; + var height = heightMode == MeasureSpecMode.Unspecified ? measure.Height : deviceIndependentHeight; + + var platformWidth = _context.ToPixels(width); + var platformHeight = _context.ToPixels(height); + + // Minimum values win over everything + platformWidth = Math.Max(MinimumWidth, platformWidth); + platformHeight = Math.Max(MinimumHeight, platformHeight); + + SetMeasuredDimension((int)platformWidth, (int)platformHeight); } protected override void OnLayout(bool changed, int left, int top, int right, int bottom) { + // Always call base.OnLayout — SwipeRefreshLayout.onLayout positions the + // spinner indicator (mCircleView) centered horizontally. Without this, + // the spinner won't appear or will be mispositioned. base.OnLayout(changed, left, top, right, bottom); if (CrossPlatformLayout is null) {