Skip to content

Commit

Permalink
Flag valid measure status in iOS backing views to avoid repeat measur…
Browse files Browse the repository at this point in the history
…ing (#12933)

Co-authored-by: E.Z. Hart <hartez@gmail.com>
  • Loading branch information
github-actions[bot] and hartez authored Jan 30, 2023
1 parent e5ec60b commit 9674a90
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
using Microsoft.Maui.Handlers;
using System.Threading.Tasks;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers;
using Xunit;
using Microsoft.Maui.Platform;
using ContentView = Microsoft.Maui.Controls.ContentView;

namespace Microsoft.Maui.DeviceTests
{
Expand All @@ -13,5 +19,137 @@ static int GetContentChildCount(ContentViewHandler contentViewHandler)
{
return contentViewHandler.PlatformView.Subviews[0].Subviews.Length;
}

[Fact, Category(TestCategory.Layout)]
public async Task ContentViewHasExpectedSize()
{
var contentView = new ContentView();
var label = new Label { Text = "", HeightRequest = 100, WidthRequest = 100 };
contentView.Content = label;

var size = await InvokeOnMainThreadAsync(() =>
{
var labelHandler = CreateHandler<LabelHandler>(label);
var handler = CreateHandler<ContentViewHandler>(contentView);

var targetSize = handler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
handler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return handler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(100, size.Width);
Assert.Equal(100, size.Height);
}

[Fact, Category(TestCategory.Layout)]
public async Task ContentViewRespondsWhenViewAdded()
{
var contentView = new ContentView();
var label = new Label { Text = "", HeightRequest = 100, WidthRequest = 100 };

var size = await InvokeOnMainThreadAsync(() =>
{
var labelHandler = CreateHandler<LabelHandler>(label);
var handler = CreateHandler<ContentViewHandler>(contentView);

var targetSize = handler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
handler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return handler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(0, size.Width);
Assert.Equal(0, size.Height);

var updatedSize = await InvokeOnMainThreadAsync(() =>
{
var handler = CreateHandler<ContentViewHandler>(contentView);

contentView.Content = label;

var targetSize = handler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
handler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return handler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(100, updatedSize.Width);
Assert.Equal(100, updatedSize.Height);
}

[Fact, Category(TestCategory.Layout)]
public async Task ContentViewRespondsWhenViewRemoved()
{
var contentView = new ContentView();
var label = new Label { Text = "", HeightRequest = 100, WidthRequest = 100 };
contentView.Content = label;

var size = await InvokeOnMainThreadAsync(() =>
{
var labelHandler = CreateHandler<LabelHandler>(label);
var handler = CreateHandler<ContentViewHandler>(contentView);

var targetSize = handler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
handler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return handler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(100, size.Width);
Assert.Equal(100, size.Height);

var updatedSize = await InvokeOnMainThreadAsync(() =>
{
var handler = CreateHandler<ContentViewHandler>(contentView);

contentView.Content = null;

var targetSize = handler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
handler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return handler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(0, updatedSize.Width);
Assert.Equal(0, updatedSize.Height);
}

[Fact, Category(TestCategory.Layout)]
public async Task ContentViewRespondsWhenViewUpdated()
{
var contentView = new ContentView();
var label = new Label { Text = "", HeightRequest = 100, WidthRequest = 100 };
contentView.Content = label;

var size = await InvokeOnMainThreadAsync(() =>
{
var labelHandler = CreateHandler<LabelHandler>(label);
var handler = CreateHandler<ContentViewHandler>(contentView);

var targetSize = handler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
handler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return handler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(100, size.Width);
Assert.Equal(100, size.Height);

var updatedSize = await InvokeOnMainThreadAsync(() =>
{
var handler = CreateHandler<ContentViewHandler>(contentView);

label.HeightRequest = 300;

var targetSize = handler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
handler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return handler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(100, updatedSize.Width);
Assert.Equal(300, updatedSize.Height);
}
}
}
154 changes: 154 additions & 0 deletions src/Controls/tests/DeviceTests/Elements/Layout/LayoutTests.iOS.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using Xunit;

namespace Microsoft.Maui.DeviceTests
{
public partial class LayoutTests
{
[Fact, Category(TestCategory.Layout)]
public async Task LayoutViewHasExpectedSize()
{
var layout = new VerticalStackLayout();
var label = new Label { Text = "", HeightRequest = 100, WidthRequest = 100 };
layout.Add(label);

var size = await InvokeOnMainThreadAsync(() =>
{
var labelHandler = CreateHandler<LabelHandler>(label);
var layoutHandler = CreateHandler<LayoutHandler>(layout);

var targetSize = layoutHandler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
layoutHandler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return layoutHandler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(100, size.Width);
Assert.Equal(100, size.Height);
}

[Fact, Category(TestCategory.Layout)]
public async Task LayoutViewRespondsWhenViewAdded()
{
var layout = new VerticalStackLayout();
var label = new Label { Text = "", HeightRequest = 100, WidthRequest = 100 };
var secondLabel = new Label { Text = "", HeightRequest = 100, WidthRequest = 100 };
layout.Add(label);

var size = await InvokeOnMainThreadAsync(() =>
{
var labelHandler = CreateHandler<LabelHandler>(label);
var layoutHandler = CreateHandler<LayoutHandler>(layout);

var targetSize = layoutHandler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
layoutHandler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return layoutHandler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(100, size.Width);
Assert.Equal(100, size.Height);

var updatedSize = await InvokeOnMainThreadAsync(() =>
{
var labelHandler2 = CreateHandler<LabelHandler>(secondLabel);
var layoutHandler = CreateHandler<LayoutHandler>(layout);

layout.Add(secondLabel);

var targetSize = layoutHandler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
layoutHandler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return layoutHandler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(100, updatedSize.Width);
Assert.Equal(200, updatedSize.Height);
}

[Fact, Category(TestCategory.Layout)]
public async Task LayoutViewRespondsWhenViewRemoved()
{
var layout = new VerticalStackLayout();
var label = new Label { Text = "", HeightRequest = 100, WidthRequest = 100 };
var secondLabel = new Label { Text = "", HeightRequest = 100, WidthRequest = 100 };
layout.Add(label);
layout.Add(secondLabel);

var size = await InvokeOnMainThreadAsync(() =>
{
var labelHandler = CreateHandler<LabelHandler>(label);
var labelHandler2 = CreateHandler<LabelHandler>(secondLabel);
var layoutHandler = CreateHandler<LayoutHandler>(layout);

var targetSize = layoutHandler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
layoutHandler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return layoutHandler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(100, size.Width);
Assert.Equal(200, size.Height);

var updatedSize = await InvokeOnMainThreadAsync(() =>
{
var layoutHandler = CreateHandler<LayoutHandler>(layout);

layout.Remove(secondLabel);

var targetSize = layoutHandler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
layoutHandler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return layoutHandler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(100, updatedSize.Width);
Assert.Equal(100, updatedSize.Height);
}

[Fact, Category(TestCategory.Layout)]
public async Task LayoutViewRespondsWhenViewUpdated()
{
var layout = new VerticalStackLayout();
var label = new Label { Text = "", HeightRequest = 100, WidthRequest = 100 };
layout.Add(label);

var size = await InvokeOnMainThreadAsync(() =>
{
var labelHandler = CreateHandler<LabelHandler>(label);
var layoutHandler = CreateHandler<LayoutHandler>(layout);

var targetSize = layoutHandler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
layoutHandler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return layoutHandler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(100, size.Width);
Assert.Equal(100, size.Height);

var updatedSize = await InvokeOnMainThreadAsync(() =>
{
var layoutHandler = CreateHandler<LayoutHandler>(layout);

label.HeightRequest = 300;

var targetSize = layoutHandler.VirtualView.Measure(double.PositiveInfinity, double.PositiveInfinity);
layoutHandler.VirtualView.Arrange(new Rect(Point.Zero, targetSize));

return layoutHandler.PlatformView.Bounds.Size.ToSize();
});

Assert.Equal(100, updatedSize.Width);
Assert.Equal(300, updatedSize.Height);
}
}
}
10 changes: 9 additions & 1 deletion src/Core/src/Platform/iOS/ContentView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class ContentView : MauiView
WeakReference<IBorderStroke>? _clip;
CAShapeLayer? _childMaskLayer;
internal event EventHandler? LayoutSubviewsChanged;
bool _measureValid;

public override CGSize SizeThatFits(CGSize size)
{
Expand All @@ -23,6 +24,7 @@ public override CGSize SizeThatFits(CGSize size)
var height = size.Height;

var crossPlatformSize = CrossPlatformMeasure(width, height);
_measureValid = true;

return crossPlatformSize.ToCGSize();
}
Expand All @@ -33,7 +35,12 @@ public override void LayoutSubviews()

var bounds = AdjustForSafeArea(Bounds).ToRectangle();

CrossPlatformMeasure?.Invoke(bounds.Width, bounds.Height);
if (!_measureValid)
{
CrossPlatformMeasure?.Invoke(bounds.Width, bounds.Height);
_measureValid = true;
}

CrossPlatformArrange?.Invoke(bounds);

if (ChildMaskLayer != null)
Expand All @@ -46,6 +53,7 @@ public override void LayoutSubviews()

public override void SetNeedsLayout()
{
_measureValid = false;
base.SetNeedsLayout();
Superview?.SetNeedsLayout();
}
Expand Down
13 changes: 12 additions & 1 deletion src/Core/src/Platform/iOS/LayoutView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.Maui.Platform
public class LayoutView : MauiView
{
bool _userInteractionEnabled;
bool _measureValid;

// TODO: Possibly reconcile this code with ViewHandlerExtensions.MeasureVirtualView
// If you make changes here please review if those changes should also
Expand All @@ -23,6 +24,7 @@ public override CGSize SizeThatFits(CGSize size)
var height = size.Height;

var crossPlatformSize = CrossPlatformMeasure(width, height);
_measureValid = true;

return crossPlatformSize.ToCGSize();
}
Expand All @@ -35,24 +37,33 @@ public override void LayoutSubviews()
base.LayoutSubviews();

var bounds = AdjustForSafeArea(Bounds).ToRectangle();
CrossPlatformMeasure?.Invoke(bounds.Width, bounds.Height);

if (!_measureValid)
{
CrossPlatformMeasure?.Invoke(bounds.Width, bounds.Height);
_measureValid = true;
}

CrossPlatformArrange?.Invoke(bounds);
}

public override void SetNeedsLayout()
{
_measureValid = false;
base.SetNeedsLayout();
Superview?.SetNeedsLayout();
}

public override void SubviewAdded(UIView uiview)
{
_measureValid = false;
base.SubviewAdded(uiview);
Superview?.SetNeedsLayout();
}

public override void WillRemoveSubview(UIView uiview)
{
_measureValid = false;
base.WillRemoveSubview(uiview);
Superview?.SetNeedsLayout();
}
Expand Down

0 comments on commit 9674a90

Please sign in to comment.