From 4217b0c03bede22f32c8ffe4be66e38e87d68106 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Tue, 27 Jul 2021 01:35:22 -0600 Subject: [PATCH] Update layout system to ensure native measure/arrange are called for all controls, even from Page subclasses Fixes #1776 Fixes #1778 --- .../src/Android/Renderers/PageContainer.cs | 3 +- .../src/Core/HandlerImpl/ContentPage.Impl.cs | 21 ++------ .../Core/HandlerImpl/NavigationPage.Impl.cs | 17 +----- .../src/Core/HandlerImpl/ScrollView.Impl.cs | 30 ++++++++++- .../Core/HandlerImpl/VisualElement.Impl.cs | 4 +- src/Controls/src/Core/Layout.cs | 42 +++++++++++---- src/Controls/src/Core/Layout/Layout.cs | 26 +-------- src/Controls/src/Core/Page.cs | 5 -- src/Controls/src/Core/ScrollView.cs | 17 +----- src/Controls/src/Core/VisualElement.cs | 2 - .../Layouts/LayoutCompatTests.cs | 53 ------------------- src/Core/src/Core/ILayout.cs | 7 ++- .../Handlers/Layout/LayoutHandler.Android.cs | 8 +-- .../Handlers/Layout/LayoutHandler.Windows.cs | 9 ++-- src/Core/src/Handlers/Layout/LayoutHandler.cs | 3 +- .../src/Handlers/Layout/LayoutHandler.iOS.cs | 8 +-- .../src/Handlers/Page/PageHandler.Windows.cs | 1 - .../Handlers/View/ViewHandlerOfT.Windows.cs | 6 +-- src/Core/src/Layouts/FlexLayoutManager.cs | 4 +- src/Core/src/Layouts/GridLayoutManager.cs | 7 +-- .../Layouts/HorizontalStackLayoutManager.cs | 20 ++++--- src/Core/src/Layouts/ILayoutManager.cs | 2 +- src/Core/src/Layouts/LayoutManager.cs | 2 +- .../src/Layouts/VerticalStackLayoutManager.cs | 7 +-- src/Core/src/Platform/iOS/LayoutView.cs | 4 +- .../DeviceTests/Stubs/LayoutManagerStub.cs | 18 +++++++ .../tests/DeviceTests/Stubs/LayoutStub.cs | 5 ++ 27 files changed, 149 insertions(+), 182 deletions(-) create mode 100644 src/Core/tests/DeviceTests/Stubs/LayoutManagerStub.cs diff --git a/src/Compatibility/Core/src/Android/Renderers/PageContainer.cs b/src/Compatibility/Core/src/Android/Renderers/PageContainer.cs index a446d0cd2347..7c3845a7e140 100644 --- a/src/Compatibility/Core/src/Android/Renderers/PageContainer.cs +++ b/src/Compatibility/Core/src/Android/Renderers/PageContainer.cs @@ -45,7 +45,8 @@ protected override void OnLayout(bool changed, int l, int t, int r, int b) { (l, t, r, b) = Context.ToPixels(ipc.ContainerArea); } - + + pageViewGroup.Measure(r - l, b - t); pageViewGroup.Layout(l, t, r, b); } } diff --git a/src/Controls/src/Core/HandlerImpl/ContentPage.Impl.cs b/src/Controls/src/Core/HandlerImpl/ContentPage.Impl.cs index 8a067dba5479..f3790fe0ac5d 100644 --- a/src/Controls/src/Core/HandlerImpl/ContentPage.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/ContentPage.Impl.cs @@ -1,8 +1,5 @@ -using Microsoft.Maui.Controls.Internals; -using Microsoft.Maui.Graphics; -using Microsoft.Maui.Hosting; +using Microsoft.Maui.Graphics; using Microsoft.Maui.HotReload; -using Microsoft.Maui.Layouts; namespace Microsoft.Maui.Controls { @@ -17,7 +14,7 @@ protected override Size MeasureOverride(double widthConstraint, double heightCon { if (Content is IFrameworkElement frameworkElement) { - _ = frameworkElement.Handler?.GetDesiredSize(widthConstraint, heightConstraint); + _ = frameworkElement.Measure(widthConstraint, heightConstraint); } return new Size(widthConstraint, heightConstraint); @@ -25,20 +22,8 @@ protected override Size MeasureOverride(double widthConstraint, double heightCon protected override Size ArrangeOverride(Rectangle bounds) { - // Update the Bounds (Frame) for this page + // Update the other stuff on this page (basically the content) Layout(bounds); - - if (Content is IFrameworkElement element and VisualElement visualElement) - { - // The size checks here are a guard against legacy layouts which try to lay things out before the - // native side is ready. We just ignore those invalid values. - if (bounds.Size.Width >= 0 && bounds.Size.Height >= 0) - { - visualElement.Frame = element.ComputeFrame(bounds); - element.Handler?.NativeArrange(visualElement.Frame); - } - } - return Frame.Size; } diff --git a/src/Controls/src/Core/HandlerImpl/NavigationPage.Impl.cs b/src/Controls/src/Core/HandlerImpl/NavigationPage.Impl.cs index 37d0b2054100..7d915bc74403 100644 --- a/src/Controls/src/Core/HandlerImpl/NavigationPage.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/NavigationPage.Impl.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.Maui.Controls.Internals; -using Microsoft.Maui.Graphics; -using Microsoft.Maui.Layouts; +using Microsoft.Maui.Graphics; namespace Microsoft.Maui.Controls { @@ -25,18 +20,10 @@ protected override Size ArrangeOverride(Rectangle bounds) { // Update the Bounds (Frame) for this page Layout(bounds); - - if (Content is IFrameworkElement element and VisualElement visualElement) - { - visualElement.Frame = element.ComputeFrame(bounds); - element.Handler?.NativeArrange(visualElement.Frame); - } - return Frame.Size; } - IFrameworkElement Content => - this.CurrentPage; + IFrameworkElement Content => CurrentPage; } } diff --git a/src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs b/src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs index cdd93479d2f7..9008e1d7d038 100644 --- a/src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs @@ -1,4 +1,7 @@ -namespace Microsoft.Maui.Controls +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Layouts; + +namespace Microsoft.Maui.Controls { public partial class ScrollView : IScrollView { @@ -35,5 +38,30 @@ void IScrollView.RequestScrollTo(double horizontalOffset, double verticalOffset, } void IScrollView.ScrollFinished() => SendScrollFinished(); + + protected override Size MeasureOverride(double widthConstraint, double heightConstraint) + { + // We call OnSizeRequest so that the content gets measured appropriately + // and then use the standard GetDesiredSize from the handler so the ScrollView's + // backing control gets measured. + + // TODO ezhart 2021-07-14 Verify that we've got the naming correct on this after we resolve the OnSizeRequest obsolete stuff +#pragma warning disable CS0618 // Type or member is obsolete + var request = OnSizeRequest(widthConstraint, heightConstraint); +#pragma warning restore CS0618 // Type or member is obsolete + + DesiredSize = this.ComputeDesiredSize(widthConstraint, heightConstraint); + return DesiredSize; + } + + protected override Size ArrangeOverride(Rectangle bounds) + { + Layout(this.ComputeFrame(bounds)); + + // Force a native arrange call; otherwise, the native bookkeeping won't be done and things won't lay out correctly + Handler?.NativeArrange(Frame); + + return Frame.Size; + } } } diff --git a/src/Controls/src/Core/HandlerImpl/VisualElement.Impl.cs b/src/Controls/src/Core/HandlerImpl/VisualElement.Impl.cs index 5366d843ea45..a773a5a56646 100644 --- a/src/Controls/src/Core/HandlerImpl/VisualElement.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/VisualElement.Impl.cs @@ -60,10 +60,12 @@ Size IFrameworkElement.Arrange(Rectangle bounds) return ArrangeOverride(bounds); } - // ArrangeOverride provides a way to allow subclasses (e.g., Layout) to override Arrange even though + // ArrangeOverride provides a way to allow subclasses (e.g., ScrollView) to override Arrange even though // the interface has to be explicitly implemented to avoid conflict with the old Arrange method protected virtual Size ArrangeOverride(Rectangle bounds) { + Frame = this.ComputeFrame(bounds); + Handler?.NativeArrange(Frame); return Frame.Size; } diff --git a/src/Controls/src/Core/Layout.cs b/src/Controls/src/Core/Layout.cs index 351ea2085742..5970239247c2 100644 --- a/src/Controls/src/Core/Layout.cs +++ b/src/Controls/src/Core/Layout.cs @@ -7,11 +7,12 @@ using System.Linq; using Microsoft.Maui.Controls.Internals; using Microsoft.Maui.Graphics; +using Microsoft.Maui.Layouts; namespace Microsoft.Maui.Controls { [ContentProperty(nameof(Children))] - public abstract partial class Layout : Layout, Microsoft.Maui.ILayout, IViewContainer where T : View + public abstract partial class Layout : Layout, Microsoft.Maui.ILayout, ILayoutManager, IViewContainer where T : View { readonly ElementCollection _children; @@ -21,6 +22,8 @@ public abstract partial class Layout : Layout, Microsoft.Maui.ILayout, IViewC public ILayoutHandler LayoutHandler => Handler as ILayoutHandler; + ILayoutManager Maui.ILayout.LayoutManager => this; + protected override void OnChildAdded(Element child) { base.OnChildAdded(child); @@ -44,6 +47,17 @@ protected virtual void OnAdded(T view) protected virtual void OnRemoved(T view) { } + + Size ILayoutManager.Measure(double widthConstraint, double heightConstraint) + { + return OnMeasure(widthConstraint, heightConstraint); + } + + Size ILayoutManager.ArrangeChildren(Rectangle childBounds) + { + LayoutChildren(childBounds.X, childBounds.Y, childBounds.Width, childBounds.Height); + return childBounds.Size; + } } public abstract class Layout : View, ILayout, ILayoutController, IPaddingElement, IFrameworkElement @@ -122,6 +136,13 @@ public static void LayoutChildIntoBoundingRegion(VisualElement child, Rectangle if (child.Parent is IFlowDirectionController parent && (isRightToLeft = parent.ApplyEffectiveFlowDirectionToChildContainer && parent.EffectiveFlowDirection.IsRightToLeft())) region = new Rectangle(parent.Width - region.Right, region.Y, region.Width, region.Height); + if (child is IFrameworkElement fe && fe.Handler != null) + { + // The new arrange methods will take care of all the alignment and margins and such + fe.Arrange(region); + return; + } + if (!(child is View view)) { child.Layout(region); @@ -268,6 +289,13 @@ internal static void LayoutChildIntoBoundingRegion(View child, Rectangle region, if (child.Parent is IFlowDirectionController parent && (isRightToLeft = parent.ApplyEffectiveFlowDirectionToChildContainer && parent.EffectiveFlowDirection.IsRightToLeft())) region = new Rectangle(parent.Width - region.Right, region.Y, region.Width, region.Height); + if (child is IFrameworkElement fe && fe.Handler != null) + { + // The new arrange methods will take care of all the alignment and margins and such + fe.Arrange(region); + return; + } + if (region.Size != childSizeRequest.Request) { bool canUseAlreadyDoneRequest = region.Width >= childSizeRequest.Request.Width && region.Height >= childSizeRequest.Request.Height; @@ -503,23 +531,15 @@ protected override void InvalidateMeasureOverride() protected override Size ArrangeOverride(Rectangle bounds) { - var size = base.ArrangeOverride(bounds); + base.ArrangeOverride(bounds); // The SholdLayoutChildren check will catch impossible sizes (negative widths/heights), not-yet-loaded controls, // and other weirdness that comes from the legacy layouts trying to run layout before the native side is ready. if (!ShouldLayoutChildren()) - return size; + return bounds.Size; UpdateChildrenLayout(); - foreach (var child in Children) - { - if (child is IFrameworkElement frameworkElement) - { - frameworkElement.Handler?.NativeArrange(frameworkElement.Frame); - } - } - return Frame.Size; } } diff --git a/src/Controls/src/Core/Layout/Layout.cs b/src/Controls/src/Core/Layout/Layout.cs index f75741cb6227..2e9535a92de4 100644 --- a/src/Controls/src/Core/Layout/Layout.cs +++ b/src/Controls/src/Core/Layout/Layout.cs @@ -11,7 +11,7 @@ namespace Microsoft.Maui.Controls.Layout2 public abstract class Layout : View, Microsoft.Maui.ILayout, IList, IPaddingElement { ILayoutManager _layoutManager; - ILayoutManager LayoutManager => _layoutManager ??= CreateLayoutManager(); + public ILayoutManager LayoutManager => _layoutManager ??= CreateLayoutManager(); // The actual backing store for the IViews in the ILayout readonly List _children = new(); @@ -47,30 +47,6 @@ public override SizeRequest GetSizeRequest(double widthConstraint, double height return new SizeRequest(size); } - protected override Size MeasureOverride(double widthConstraint, double heightConstraint) - { - var margin = (this as IView)?.Margin ?? Thickness.Zero; - // Adjust the constraints to account for the margins - widthConstraint -= margin.HorizontalThickness; - heightConstraint -= margin.VerticalThickness; - var sizeWithoutMargins = LayoutManager.Measure(widthConstraint, heightConstraint); - DesiredSize = new Size(sizeWithoutMargins.Width + Margin.HorizontalThickness, - sizeWithoutMargins.Height + Margin.VerticalThickness); - return DesiredSize; - } - - protected override Size ArrangeOverride(Rectangle bounds) - { - base.ArrangeOverride(bounds); - Frame = bounds; - LayoutManager.ArrangeChildren(Frame); - foreach (var child in Children) - { - child.Handler?.NativeArrange(child.Frame); - } - return Frame.Size; - } - protected override void InvalidateMeasureOverride() { base.InvalidateMeasureOverride(); diff --git a/src/Controls/src/Core/Page.cs b/src/Controls/src/Core/Page.cs index b0c0dd196200..dbed056c15f2 100644 --- a/src/Controls/src/Core/Page.cs +++ b/src/Controls/src/Core/Page.cs @@ -259,11 +259,6 @@ protected virtual void LayoutChildren(double x, double y, double width, double h if (child == null) continue; - if (child is Layout2.Layout) - { - continue; - } - var page = child as Page; if (page != null && page.IgnoresContainerArea) Maui.Controls.Layout.LayoutChildIntoBoundingRegion(child, originalArea); diff --git a/src/Controls/src/Core/ScrollView.cs b/src/Controls/src/Core/ScrollView.cs index aa60f78c5e7e..cea28258d031 100644 --- a/src/Controls/src/Core/ScrollView.cs +++ b/src/Controls/src/Core/ScrollView.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.Maui.Controls.Internals; using Microsoft.Maui.Graphics; +using Microsoft.Maui.Layouts; namespace Microsoft.Maui.Controls { @@ -280,7 +281,7 @@ protected override SizeRequest OnSizeRequest(double widthConstraint, double heig if (Content is IFrameworkElement fe && fe.Handler != null) { - contentRequest = fe.Handler.GetDesiredSize(widthConstraint, heightConstraint); + contentRequest = fe.Measure(widthConstraint, heightConstraint); } else { @@ -366,19 +367,5 @@ void OnScrollToRequested(ScrollToRequestedEventArgs e) Handler?.Invoke(nameof(IScrollView.RequestScrollTo), e.ToRequest()); } - - protected override Size MeasureOverride(double widthConstraint, double heightConstraint) - { - // We call OnSizeRequest so that the content gets measured appropriately - // and then use the standard GetDesiredSize from the handler so the ScrollView's - // backing control gets measured. - - // TODO ezhart 2021-07-14 Verify that we've got the naming correct on this after we resolve the OnSizeRequest obsolete stuff -#pragma warning disable CS0618 // Type or member is obsolete - _ = OnSizeRequest(widthConstraint, heightConstraint); -#pragma warning restore CS0618 // Type or member is obsolete - - return Handler.GetDesiredSize(widthConstraint, heightConstraint); - } } } diff --git a/src/Controls/src/Core/VisualElement.cs b/src/Controls/src/Core/VisualElement.cs index da2905842742..532ae641bb2a 100644 --- a/src/Controls/src/Core/VisualElement.cs +++ b/src/Controls/src/Core/VisualElement.cs @@ -413,8 +413,6 @@ private set Y = value.Y; SetSize(value.Width, value.Height); BatchCommit(); - - Handler?.UpdateValue(nameof(IFrameworkElement.Frame)); } } diff --git a/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs b/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs index 35887b2fb4a9..dda3737227a4 100644 --- a/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs +++ b/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs @@ -31,59 +31,6 @@ public void BasicContentPage() Assert.AreEqual(expectedSize, button.Bounds.Size); } - [Test] - public void VerticalStackLayoutInsideStackLayout() - { - var stackLayout = new StackLayout() { IsPlatformEnabled = true }; - var verticalStackLayout = new VerticalStackLayout() { IsPlatformEnabled = true }; - var button = new Button() { IsPlatformEnabled = true, HeightRequest = 100, WidthRequest = 100 }; - var expectedSize = new Size(100, 100); - - var buttonHandler = Substitute.For(); - buttonHandler.GetDesiredSize(default, default).ReturnsForAnyArgs(expectedSize); - button.Handler = buttonHandler; - - stackLayout.Children.Add(verticalStackLayout); - verticalStackLayout.Add(button); - - var rect = new Rectangle(0, 0, 100, 100); - Layout.LayoutChildIntoBoundingRegion(stackLayout, rect); - - // Normally this would get called from the native platform - (verticalStackLayout as IFrameworkElement).Arrange(rect); - - Assert.AreEqual(expectedSize, button.Bounds.Size); - } - - [Test] - public void StackLayoutInsideVerticalStackLayout() - { - var expectedSize = new Size(100, 100); - var expectedRect = new Rectangle(Point.Zero, expectedSize); - - var stackLayout = new StackLayout() { IsPlatformEnabled = true }; - var slHandler = Substitute.For(); - stackLayout.Handler = slHandler; - - var verticalStackLayout = new VerticalStackLayout() { IsPlatformEnabled = true }; - var vslHandler = Substitute.For(); - verticalStackLayout.Handler = vslHandler; - - var button = new Button() { IsPlatformEnabled = true, HeightRequest = 100, WidthRequest = 100 }; - var buttonHandler = Substitute.For(); - buttonHandler.GetDesiredSize(default, default).ReturnsForAnyArgs(expectedSize); - button.Handler = buttonHandler; - - verticalStackLayout.Add(stackLayout); - stackLayout.Children.Add(button); - - (verticalStackLayout as IFrameworkElement).Measure(expectedRect.Width, expectedRect.Height); - (verticalStackLayout as IFrameworkElement).Arrange(expectedRect); - - slHandler.Received().NativeArrange(expectedRect); - Assert.AreEqual(expectedSize, stackLayout.Bounds.Size); - } - [Test] public void GridInsideStackLayout() { diff --git a/src/Core/src/Core/ILayout.cs b/src/Core/src/Core/ILayout.cs index e39f68b7173b..32d2525d889d 100644 --- a/src/Core/src/Core/ILayout.cs +++ b/src/Core/src/Core/ILayout.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using Microsoft.Maui.Layouts; namespace Microsoft.Maui { @@ -17,5 +17,10 @@ public interface ILayout : IView, IContainer /// The space between the outer edge of the ILayout's content area and its children. /// Thickness Padding { get; } + + /// + /// The LayoutManager responsible for laying out the children of this ILayout + /// + ILayoutManager LayoutManager { get; } } } \ No newline at end of file diff --git a/src/Core/src/Handlers/Layout/LayoutHandler.Android.cs b/src/Core/src/Handlers/Layout/LayoutHandler.Android.cs index c4617a65a5ef..cdd170ff7cfe 100644 --- a/src/Core/src/Handlers/Layout/LayoutHandler.Android.cs +++ b/src/Core/src/Handlers/Layout/LayoutHandler.Android.cs @@ -14,8 +14,8 @@ protected override LayoutViewGroup CreateNativeView() var viewGroup = new LayoutViewGroup(Context!) { - CrossPlatformMeasure = VirtualView.Measure, - CrossPlatformArrange = VirtualView.Arrange + CrossPlatformMeasure = VirtualView.LayoutManager.Measure, + CrossPlatformArrange = VirtualView.LayoutManager.ArrangeChildren }; return viewGroup; @@ -29,8 +29,8 @@ public override void SetVirtualView(IView view) _ = VirtualView ?? throw new InvalidOperationException($"{nameof(VirtualView)} should have been set by base class."); _ = MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} should have been set by base class."); - NativeView.CrossPlatformMeasure = VirtualView.Measure; - NativeView.CrossPlatformArrange = VirtualView.Arrange; + NativeView.CrossPlatformMeasure = VirtualView.LayoutManager.Measure; + NativeView.CrossPlatformArrange = VirtualView.LayoutManager.ArrangeChildren; NativeView.RemoveAllViews(); foreach (var child in VirtualView) diff --git a/src/Core/src/Handlers/Layout/LayoutHandler.Windows.cs b/src/Core/src/Handlers/Layout/LayoutHandler.Windows.cs index 6b9972c026cb..b85299ae5938 100644 --- a/src/Core/src/Handlers/Layout/LayoutHandler.Windows.cs +++ b/src/Core/src/Handlers/Layout/LayoutHandler.Windows.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Maui.Graphics; using Microsoft.UI.Xaml; namespace Microsoft.Maui.Handlers @@ -22,8 +23,8 @@ public override void SetVirtualView(IView view) _ = VirtualView ?? throw new InvalidOperationException($"{nameof(VirtualView)} should have been set by base class."); _ = MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} should have been set by base class."); - NativeView.CrossPlatformMeasure = VirtualView.Measure; - NativeView.CrossPlatformArrange = VirtualView.Arrange; + NativeView.CrossPlatformMeasure = VirtualView.LayoutManager.Measure; + NativeView.CrossPlatformArrange = VirtualView.LayoutManager.ArrangeChildren; NativeView.Children.Clear(); foreach (var child in VirtualView) @@ -52,8 +53,8 @@ protected override LayoutPanel CreateNativeView() var view = new LayoutPanel { - CrossPlatformMeasure = VirtualView.Measure, - CrossPlatformArrange = VirtualView.Arrange, + CrossPlatformMeasure = VirtualView.LayoutManager.Measure, + CrossPlatformArrange = VirtualView.LayoutManager.ArrangeChildren, }; return view; diff --git a/src/Core/src/Handlers/Layout/LayoutHandler.cs b/src/Core/src/Handlers/Layout/LayoutHandler.cs index 6618dc3abdee..a4f753951d74 100644 --- a/src/Core/src/Handlers/Layout/LayoutHandler.cs +++ b/src/Core/src/Handlers/Layout/LayoutHandler.cs @@ -2,12 +2,13 @@ using System; using System.Collections.Generic; using System.Text; +using Microsoft.Maui.Graphics; namespace Microsoft.Maui.Handlers { public partial class LayoutHandler : ILayoutHandler { - public static PropertyMapper LayoutMapper = new PropertyMapper(ViewHandler.ViewMapper) + public static PropertyMapper LayoutMapper = new(ViewMapper) { }; diff --git a/src/Core/src/Handlers/Layout/LayoutHandler.iOS.cs b/src/Core/src/Handlers/Layout/LayoutHandler.iOS.cs index 40afb7a6cbd2..9c6047cda5aa 100644 --- a/src/Core/src/Handlers/Layout/LayoutHandler.iOS.cs +++ b/src/Core/src/Handlers/Layout/LayoutHandler.iOS.cs @@ -14,8 +14,8 @@ protected override LayoutView CreateNativeView() var view = new LayoutView { - CrossPlatformMeasure = VirtualView.Measure, - CrossPlatformArrange = VirtualView.Arrange, + CrossPlatformMeasure = VirtualView.LayoutManager.Measure, + CrossPlatformArrange = VirtualView.LayoutManager.ArrangeChildren, }; return view; @@ -30,8 +30,8 @@ public override void SetVirtualView(IView view) _ = MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} should have been set by base class."); NativeView.View = view; - NativeView.CrossPlatformMeasure = VirtualView.Measure; - NativeView.CrossPlatformArrange = VirtualView.Arrange; + NativeView.CrossPlatformMeasure = VirtualView.LayoutManager.Measure; + NativeView.CrossPlatformArrange = VirtualView.LayoutManager.ArrangeChildren; // Remove any previous children var oldChildren = NativeView.Subviews; diff --git a/src/Core/src/Handlers/Page/PageHandler.Windows.cs b/src/Core/src/Handlers/Page/PageHandler.Windows.cs index e40b39ec7cdd..733f17051d20 100644 --- a/src/Core/src/Handlers/Page/PageHandler.Windows.cs +++ b/src/Core/src/Handlers/Page/PageHandler.Windows.cs @@ -6,7 +6,6 @@ namespace Microsoft.Maui.Handlers { public partial class PageHandler : ViewHandler { - public override void SetVirtualView(IView view) { base.SetVirtualView(view); diff --git a/src/Core/src/Handlers/View/ViewHandlerOfT.Windows.cs b/src/Core/src/Handlers/View/ViewHandlerOfT.Windows.cs index 98804a452f3d..d4b083a8d154 100644 --- a/src/Core/src/Handlers/View/ViewHandlerOfT.Windows.cs +++ b/src/Core/src/Handlers/View/ViewHandlerOfT.Windows.cs @@ -35,11 +35,11 @@ public override void NativeArrange(Rectangle rect) public override Size GetDesiredSize(double widthConstraint, double heightConstraint) { - if (WrappedNativeView == null) - return Size.Zero; - var nativeView = WrappedNativeView; + if (nativeView == null) + return Size.Zero; + if (widthConstraint < 0 || heightConstraint < 0) return Size.Zero; diff --git a/src/Core/src/Layouts/FlexLayoutManager.cs b/src/Core/src/Layouts/FlexLayoutManager.cs index 25402d97fc2c..31ec866d045a 100644 --- a/src/Core/src/Layouts/FlexLayoutManager.cs +++ b/src/Core/src/Layouts/FlexLayoutManager.cs @@ -13,7 +13,7 @@ public FlexLayoutManager(IFlexLayout flexLayout) FlexLayout = flexLayout; } - public void ArrangeChildren(Rectangle childBounds) + public Size ArrangeChildren(Rectangle childBounds) { FlexLayout.Layout(childBounds.Width, childBounds.Height); @@ -28,6 +28,8 @@ public void ArrangeChildren(Rectangle childBounds) frame = frame.Offset(childBounds.X, childBounds.Y); child.Arrange(frame); } + + return childBounds.Size; } public Size Measure(double widthConstraint, double heightConstraint) diff --git a/src/Core/src/Layouts/GridLayoutManager.cs b/src/Core/src/Layouts/GridLayoutManager.cs index b43dc23341a1..e66a6f16c82b 100644 --- a/src/Core/src/Layouts/GridLayoutManager.cs +++ b/src/Core/src/Layouts/GridLayoutManager.cs @@ -22,7 +22,7 @@ public override Size Measure(double widthConstraint, double heightConstraint) return new Size(_gridStructure.GridWidth(), _gridStructure.GridHeight()); } - public override void ArrangeChildren(Rectangle childBounds) + public override Size ArrangeChildren(Rectangle childBounds) { var structure = _gridStructure ?? new GridStructure(Grid, childBounds.Width, childBounds.Height); @@ -35,9 +35,10 @@ public override void ArrangeChildren(Rectangle childBounds) var cell = structure.GetCellBoundsFor(view); - view.Frame = view.ComputeFrame(cell); - view.Arrange(view.Frame); + view.Arrange(cell); } + + return new Size(structure.GridWidth(), structure.GridHeight()); } class GridStructure diff --git a/src/Core/src/Layouts/HorizontalStackLayoutManager.cs b/src/Core/src/Layouts/HorizontalStackLayoutManager.cs index 71ced368b21d..2a2fd4b79954 100644 --- a/src/Core/src/Layouts/HorizontalStackLayoutManager.cs +++ b/src/Core/src/Layouts/HorizontalStackLayoutManager.cs @@ -41,24 +41,27 @@ public override Size Measure(double widthConstraint, double heightConstraint) return new Size(finalWidth, finalHeight); } - public override void ArrangeChildren(Rectangle bounds) + public override Size ArrangeChildren(Rectangle bounds) { var padding = Stack.Padding; var height = bounds.Height - padding.VerticalThickness; + double stackWidth = 0; if (Stack.FlowDirection == FlowDirection.LeftToRight) { - ArrangeLeftToRight(height, padding.Left, padding.Top, Stack.Spacing, Stack); + stackWidth = ArrangeLeftToRight(height, padding.Left, padding.Top, Stack.Spacing, Stack); } else { // We _could_ simply reverse the list of child views when arranging from right to left, // but this way we avoid extra list and enumerator allocations - ArrangeRightToLeft(height, padding.Left, padding.Top, Stack.Spacing, Stack); + stackWidth = ArrangeRightToLeft(height, padding.Left, padding.Top, Stack.Spacing, Stack); } + + return new Size(height, stackWidth); } - static void ArrangeLeftToRight(double height, double left, double top, double spacing, IList children) + static double ArrangeLeftToRight(double height, double left, double top, double spacing, IList children) { double xPosition = left; @@ -73,9 +76,11 @@ static void ArrangeLeftToRight(double height, double left, double top, double sp xPosition += ArrangeChild(child, height, top, spacing, xPosition); } + + return xPosition; } - static void ArrangeRightToLeft(double height, double left, double top, double spacing, IList children) + static double ArrangeRightToLeft(double height, double left, double top, double spacing, IList children) { double xPostition = left; @@ -90,13 +95,14 @@ static void ArrangeRightToLeft(double height, double left, double top, double sp xPostition += ArrangeChild(child, height, top, spacing, xPostition); } + + return xPostition; } static double ArrangeChild(IView child, double height, double top, double spacing, double x) { var destination = new Rectangle(x, top, child.DesiredSize.Width, height); - child.Frame = child.ComputeFrame(destination); - child.Arrange(child.Frame); + child.Arrange(destination); return destination.Width + spacing; } } diff --git a/src/Core/src/Layouts/ILayoutManager.cs b/src/Core/src/Layouts/ILayoutManager.cs index 51bc8dd1a179..830dc783f924 100644 --- a/src/Core/src/Layouts/ILayoutManager.cs +++ b/src/Core/src/Layouts/ILayoutManager.cs @@ -6,6 +6,6 @@ namespace Microsoft.Maui.Layouts public interface ILayoutManager { Size Measure(double widthConstraint, double heightConstraint); - void ArrangeChildren(Rectangle childBounds); + Size ArrangeChildren(Rectangle childBounds); } } diff --git a/src/Core/src/Layouts/LayoutManager.cs b/src/Core/src/Layouts/LayoutManager.cs index 890939946f39..72860276ede0 100644 --- a/src/Core/src/Layouts/LayoutManager.cs +++ b/src/Core/src/Layouts/LayoutManager.cs @@ -13,7 +13,7 @@ public LayoutManager(ILayout layout) public ILayout Layout { get; } public abstract Size Measure(double widthConstraint, double heightConstraint); - public abstract void ArrangeChildren(Rectangle childBounds); + public abstract Size ArrangeChildren(Rectangle childBounds); public static double ResolveConstraints(double externalConstraint, double explicitLength, double measuredLength) { diff --git a/src/Core/src/Layouts/VerticalStackLayoutManager.cs b/src/Core/src/Layouts/VerticalStackLayoutManager.cs index 21e2e7094c59..3fb5c85d2142 100644 --- a/src/Core/src/Layouts/VerticalStackLayoutManager.cs +++ b/src/Core/src/Layouts/VerticalStackLayoutManager.cs @@ -41,7 +41,7 @@ public override Size Measure(double widthConstraint, double heightConstraint) return new Size(finalWidth, finalHeight); } - public override void ArrangeChildren(Rectangle bounds) + public override Size ArrangeChildren(Rectangle bounds) { var padding = Stack.Padding; @@ -59,10 +59,11 @@ public override void ArrangeChildren(Rectangle bounds) } var destination = new Rectangle(left, stackHeight, width, child.DesiredSize.Height); - child.Frame = child.ComputeFrame(destination); - child.Arrange(child.Frame); + child.Arrange(destination); stackHeight += destination.Height + Stack.Spacing; } + + return new Size(width, stackHeight); } } } diff --git a/src/Core/src/Platform/iOS/LayoutView.cs b/src/Core/src/Platform/iOS/LayoutView.cs index 4319f7f306ca..fba406ab65d0 100644 --- a/src/Core/src/Platform/iOS/LayoutView.cs +++ b/src/Core/src/Platform/iOS/LayoutView.cs @@ -41,7 +41,6 @@ public override void LayoutSubviews() bounds.Width -= safe.Left + safe.Right; } - CrossPlatformMeasure?.Invoke(bounds.Width, bounds.Height); CrossPlatformArrange?.Invoke(bounds); } @@ -72,9 +71,11 @@ public override void LayoutSubviews() var width = Frame.Width; var height = Frame.Height; + CrossPlatformMeasure?.Invoke(width, height); CrossPlatformArrange?.Invoke(Frame.ToRectangle()); } + internal Func? CrossPlatformMeasure { get; set; } internal Func? CrossPlatformArrange { get; set; } } @@ -93,6 +94,7 @@ protected override UIView CreateNativeView(IElement view) return new PageView { CrossPlatformArrange = ((IPage)view).Arrange, + CrossPlatformMeasure = ((IPage)view).Measure }; } diff --git a/src/Core/tests/DeviceTests/Stubs/LayoutManagerStub.cs b/src/Core/tests/DeviceTests/Stubs/LayoutManagerStub.cs new file mode 100644 index 000000000000..96f54bec7404 --- /dev/null +++ b/src/Core/tests/DeviceTests/Stubs/LayoutManagerStub.cs @@ -0,0 +1,18 @@ +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Layouts; + +namespace Microsoft.Maui.DeviceTests.Stubs +{ + public class LayoutManagerStub : ILayoutManager + { + public Size ArrangeChildren(Rectangle childBounds) + { + return childBounds.Size; + } + + public Size Measure(double widthConstraint, double heightConstraint) + { + return new Size(widthConstraint, heightConstraint); + } + } +} diff --git a/src/Core/tests/DeviceTests/Stubs/LayoutStub.cs b/src/Core/tests/DeviceTests/Stubs/LayoutStub.cs index 2e5ded712014..ffbb491beccb 100644 --- a/src/Core/tests/DeviceTests/Stubs/LayoutStub.cs +++ b/src/Core/tests/DeviceTests/Stubs/LayoutStub.cs @@ -1,11 +1,14 @@ using System; using System.Collections; using System.Collections.Generic; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Layouts; namespace Microsoft.Maui.DeviceTests.Stubs { public class LayoutStub : StubBase, ILayout { + ILayoutManager _layoutManager; IList _children = new List(); public ILayoutHandler LayoutHandler => Handler as ILayoutHandler; @@ -64,6 +67,8 @@ IEnumerator IEnumerable.GetEnumerator() public int Count => _children.Count; public bool IsReadOnly => _children.IsReadOnly; + public ILayoutManager LayoutManager => _layoutManager ??= new LayoutManagerStub(); + public IView this[int index] { get => _children[index]; set => _children[index] = value; } } }