diff --git a/src/Controls/src/Core/Handlers/Items/Android/ItemContentView.cs b/src/Controls/src/Core/Handlers/Items/Android/ItemContentView.cs index 71f0ff2136fc..1d12f0d0c50b 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/ItemContentView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/ItemContentView.cs @@ -90,6 +90,16 @@ protected override void OnLayout(bool changed, int l, int t, int r, int b) { handler.LayoutVirtualView(l, t, r, b); } + + // Ensure the View's Frame is updated with the final layout bounds + // This is especially important during scrolling scenarios in CollectionView for hierarchical item structures + var currentFrame = View.Frame; + var newFrame = new Graphics.Rect(this.FromPixels(l), this.FromPixels(t), + this.FromPixels(r - l), this.FromPixels(b - t)); + if (currentFrame != newFrame) + { + View.Frame = newFrame; + } } protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) @@ -190,6 +200,16 @@ _pixelSize is not null && ? double.PositiveInfinity : this.FromPixels(pixelHeight); + // Update the View's Frame BEFORE measuring child elements so that Width and Height + // properties are available to nested child elements during their measure calculations in CollectionView scenarios. + // This addresses the hierarchical structure issue where CollectionView doesn't properly measure all nested elements. + var currentFrame = View.Frame; + var newFrame = new Graphics.Rect(currentFrame.X, currentFrame.Y, width, height); + if (currentFrame != newFrame) + { + View.Frame = newFrame; + } + var measure = View.Measure(width, height); if (pixelWidth == 0) diff --git a/src/Core/tests/DeviceTests/Handlers/ContentView/ContentViewTests.cs b/src/Core/tests/DeviceTests/Handlers/ContentView/ContentViewTests.cs index 7393c830f2d0..a289cfbfd10b 100644 --- a/src/Core/tests/DeviceTests/Handlers/ContentView/ContentViewTests.cs +++ b/src/Core/tests/DeviceTests/Handlers/ContentView/ContentViewTests.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Graphics; using Xunit; namespace Microsoft.Maui.DeviceTests.Handlers.ContentView @@ -50,5 +51,147 @@ public async Task RespectsMinimumValues() Assert.Equal(cv.MinimumWidth, measure.Width, 0); Assert.Equal(cv.MinimumHeight, measure.Height, 0); } + + [Fact] + public async Task ContentViewWidthAvailableToChildrenDuringLayout() + { + var contentView = new ContentViewStub(); + var childView = new TestChildView(); + + contentView.Content = childView; + contentView.WidthRequest = 200; + contentView.HeightRequest = 100; + + var contentViewHandler = await CreateHandlerAsync(contentView); + + // Simulate a layout pass similar to what happens in CollectionView + var result = await InvokeOnMainThreadAsync(() => + { + contentView.Measure(200, 100); + contentView.Arrange(new Graphics.Rect(0, 0, 200, 100)); + return new { ContentViewWidth = contentView.Width, ChildRecordedWidth = childView.RecordedParentWidth }; + }); + + // The child should have access to the parent's width during layout + Assert.True(result.ContentViewWidth > 0, "ContentView Width should be greater than 0"); + Assert.True(result.ChildRecordedWidth > 0, "Child should have recorded a positive parent width during layout"); + Assert.Equal(200, result.ContentViewWidth, 0); + Assert.Equal(200, result.ChildRecordedWidth, 0); + } + + [Fact] + public async Task ContentViewWidthAvailableToChildrenDuringScrolling() + { + var contentView = new ContentViewStub(); + var childView = new TestChildView(); + + contentView.Content = childView; + contentView.WidthRequest = 200; + contentView.HeightRequest = 100; + + var contentViewHandler = await CreateHandlerAsync(contentView); + + // Simulate multiple layout passes that occur during scrolling in CollectionView + var result = await InvokeOnMainThreadAsync(() => + { + // Initial layout + contentView.Measure(200, 100); + contentView.Arrange(new Graphics.Rect(0, 0, 200, 100)); + + // Simulate scrolling - multiple measure/arrange cycles with different positions + // This simulates what happens when CollectionView recycles and repositions items during scrolling + for (int i = 0; i < 3; i++) + { + // Reset the child's recorded width to test each cycle + childView.RecordedParentWidth = -1; + + // Simulate different positions during scrolling + var yOffset = i * 10; + contentView.Measure(200, 100); + contentView.Arrange(new Graphics.Rect(0, yOffset, 200, 100 + yOffset)); + + // Child should have access to parent width even during position changes + if (childView.RecordedParentWidth <= 0) + { + return new { Success = false, ContentViewWidth = contentView.Width, ChildRecordedWidth = childView.RecordedParentWidth, FailedAtIteration = i }; + } + } + + return new { Success = true, ContentViewWidth = contentView.Width, ChildRecordedWidth = childView.RecordedParentWidth, FailedAtIteration = -1 }; + }); + + // The child should have access to the parent's width during all scrolling scenarios + Assert.True(result.Success, $"Child failed to access parent width during scrolling at iteration {result.FailedAtIteration}"); + Assert.True(result.ContentViewWidth > 0, "ContentView Width should be greater than 0"); + Assert.True(result.ChildRecordedWidth > 0, "Child should have recorded a positive parent width during scrolling"); + Assert.Equal(200, result.ContentViewWidth, 0); + Assert.Equal(200, result.ChildRecordedWidth, 0); + } + } + + public class TestChildView : IView + { + public double RecordedParentWidth { get; private set; } = -1; + + public Size Arrange(Rect bounds) + { + // Record the parent's width when this child is arranged + if (this.Parent is IView parent) + { + RecordedParentWidth = parent.Width; + } + Frame = bounds; + return bounds.Size; + } + + public Size Measure(double widthConstraint, double heightConstraint) + { + // Record the parent's width when this child is measured + if (this.Parent is IView parent) + { + RecordedParentWidth = parent.Width; + } + DesiredSize = new Size(System.Math.Min(50, widthConstraint), System.Math.Min(50, heightConstraint)); + return DesiredSize; + } + + // Minimal implementation of IView interface + public IElement? Parent { get; set; } + public IElementHandler? Handler { get; set; } + public Rect Frame { get; set; } + public Size DesiredSize { get; set; } + public double Width => Frame.Width; + public double Height => Frame.Height; + public Thickness Margin => Thickness.Zero; + public string AutomationId => ""; + public FlowDirection FlowDirection => FlowDirection.LeftToRight; + public LayoutAlignment HorizontalLayoutAlignment => LayoutAlignment.Fill; + public LayoutAlignment VerticalLayoutAlignment => LayoutAlignment.Fill; + public Semantics? Semantics => null; + public IShape? Clip => null; + public IShadow? Shadow => null; + public bool IsEnabled => true; + public bool IsFocused { get; set; } + public Visibility Visibility => Visibility.Visible; + public double Opacity => 1.0; + public Paint? Background => null; + public double MinimumWidth => 0; + public double MaximumWidth => double.PositiveInfinity; + public double MinimumHeight => 0; + public double MaximumHeight => double.PositiveInfinity; + public int ZIndex => 0; + public void InvalidateMeasure() { } + public void InvalidateArrange() { } + IViewHandler? IView.Handler { get; set; } + public double TranslationX => 0; + public double TranslationY => 0; + public double Scale => 1; + public double ScaleX => 1; + public double ScaleY => 1; + public double Rotation => 0; + public double RotationX => 0; + public double RotationY => 0; + public double AnchorX => 0.5; + public double AnchorY => 0.5; } } \ No newline at end of file