Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update layout system to ensure native measure/arrange are called for all controls #1819

Merged
merged 2 commits into from
Jul 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
21 changes: 3 additions & 18 deletions src/Controls/src/Core/HandlerImpl/ContentPage.Impl.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -17,28 +14,16 @@ 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);
}

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;
}

Expand Down
9 changes: 1 addition & 8 deletions src/Controls/src/Core/HandlerImpl/NavigationPage.Impl.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -42,13 +42,6 @@ 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;
}

Expand Down
30 changes: 29 additions & 1 deletion src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -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;
}
}
}
4 changes: 3 additions & 1 deletion src/Controls/src/Core/HandlerImpl/VisualElement.Impl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
42 changes: 31 additions & 11 deletions src/Controls/src/Core/Layout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> : Layout, Microsoft.Maui.ILayout, IViewContainer<T> where T : View
public abstract partial class Layout<T> : Layout, Microsoft.Maui.ILayout, ILayoutManager, IViewContainer<T> where T : View
{
readonly ElementCollection<T> _children;

Expand All @@ -21,6 +22,8 @@ public abstract partial class Layout<T> : 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);
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Expand Down
26 changes: 1 addition & 25 deletions src/Controls/src/Core/Layout/Layout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Microsoft.Maui.Controls.Layout2
public abstract class Layout : View, Microsoft.Maui.ILayout, IList<IView>, IPaddingElement
{
ILayoutManager _layoutManager;
ILayoutManager LayoutManager => _layoutManager ??= CreateLayoutManager();
public ILayoutManager LayoutManager => _layoutManager ??= CreateLayoutManager();

// The actual backing store for the IViews in the ILayout
readonly List<IView> _children = new();
Expand Down Expand Up @@ -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();
Expand Down
5 changes: 0 additions & 5 deletions src/Controls/src/Core/Page.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
17 changes: 2 additions & 15 deletions src/Controls/src/Core/ScrollView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
}
}
}
2 changes: 0 additions & 2 deletions src/Controls/src/Core/VisualElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,6 @@ private set
Y = value.Y;
SetSize(value.Width, value.Height);
BatchCommit();

Handler?.UpdateValue(nameof(IFrameworkElement.Frame));
}
}

Expand Down
53 changes: 0 additions & 53 deletions src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IViewHandler>();
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<ILayoutHandler>();
stackLayout.Handler = slHandler;

var verticalStackLayout = new VerticalStackLayout() { IsPlatformEnabled = true };
var vslHandler = Substitute.For<ILayoutHandler>();
verticalStackLayout.Handler = vslHandler;

var button = new Button() { IsPlatformEnabled = true, HeightRequest = 100, WidthRequest = 100 };
var buttonHandler = Substitute.For<IViewHandler>();
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()
{
Expand Down
7 changes: 6 additions & 1 deletion src/Core/src/Core/ILayout.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using Microsoft.Maui.Layouts;

namespace Microsoft.Maui
{
Expand All @@ -17,5 +17,10 @@ public interface ILayout : IView, IContainer
/// The space between the outer edge of the ILayout's content area and its children.
/// </summary>
Thickness Padding { get; }

/// <summary>
/// The LayoutManager responsible for laying out the children of this ILayout
/// </summary>
ILayoutManager LayoutManager { get; }
}
}
Loading