Skip to content
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
28 changes: 0 additions & 28 deletions src/Controls/src/Core/Compatibility/Handlers/iOS/FrameRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,34 +217,6 @@ protected override void Dispose(bool disposing)
}
}

bool _pendingSuperViewSetNeedsLayout;

public override void SetNeedsLayout()
{
base.SetNeedsLayout();

if (Window is not null)
{
_pendingSuperViewSetNeedsLayout = false;
this.Superview?.SetNeedsLayout();
}
else
{
_pendingSuperViewSetNeedsLayout = true;
}
}

public override void MovedToWindow()
{
base.MovedToWindow();
if (_pendingSuperViewSetNeedsLayout)
{
this.Superview?.SetNeedsLayout();
}

_pendingSuperViewSetNeedsLayout = false;
}

[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
class FrameView : Microsoft.Maui.Platform.ContentView
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
using System;
using System.ComponentModel;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Graphics;
using UIKit;

namespace Microsoft.Maui.Controls.Handlers.Compatibility
{
public abstract partial class VisualElementRenderer<TElement> : UIView, IPlatformViewHandler, IElementHandler
public abstract partial class VisualElementRenderer<TElement> : UIView, IPlatformViewHandler, IElementHandler, IPlatformMeasureInvalidationController
where TElement : Element, IView
{
bool _invalidateParentWhenMovedToWindow;
object? IElementHandler.PlatformView => Subviews.Length > 0 ? Subviews[0] : this;

public virtual UIViewController? ViewController => null;
Expand Down Expand Up @@ -93,5 +93,22 @@ void OnSizeChanged(object? sender, EventArgs e)
{
UpdateNativeWidget();
}

void IPlatformMeasureInvalidationController.InvalidateAncestorsMeasuresWhenMovedToWindow()
{
_invalidateParentWhenMovedToWindow = true;
}

void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating) => SetNeedsLayout();

public override void MovedToWindow()
{
base.MovedToWindow();
if (_invalidateParentWhenMovedToWindow)
{
_invalidateParentWhenMovedToWindow = false;
this.InvalidateAncestorsMeasures();
}
}
}
}
24 changes: 23 additions & 1 deletion src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

namespace Microsoft.Maui.Controls.Handlers.Items;

internal class MauiCollectionView : UICollectionView, IUIViewLifeCycleEvents
internal class MauiCollectionView : UICollectionView, IUIViewLifeCycleEvents, IPlatformMeasureInvalidationController
{
bool _invalidateParentWhenMovedToWindow;

WeakReference<ICustomMauiCollectionViewDelegate>? _customDelegate;
public MauiCollectionView(CGRect frame, UICollectionViewLayout layout) : base(frame, layout)
{
Expand All @@ -18,8 +20,22 @@ public override void ScrollRectToVisible(CGRect rect, bool animated)
base.ScrollRectToVisible(rect, animated);
}

void IPlatformMeasureInvalidationController.InvalidateAncestorsMeasuresWhenMovedToWindow()
{
_invalidateParentWhenMovedToWindow = true;
}

void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
{
if (!isPropagating)
{
SetNeedsLayout();
}
}

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = IUIViewLifeCycleEvents.UnconditionalSuppressMessage)]
EventHandler? _movedToWindow;

event EventHandler? IUIViewLifeCycleEvents.MovedToWindow
{
add => _movedToWindow += value;
Expand All @@ -35,6 +51,12 @@ public override void MovedToWindow()
{
target.MovedToWindow(this);
}

if (_invalidateParentWhenMovedToWindow)
{
_invalidateParentWhenMovedToWindow = false;
this.InvalidateAncestorsMeasures();
}
}

internal void SetCustomDelegate(ICustomMauiCollectionViewDelegate customDelegate)
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/ImageButton/ImageButton.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Size ICrossPlatformLayout.CrossPlatformMeasure(double widthConstraint, double he
if (platformButton.ImageView.Image is not null)
{
return platformButton.ImageView
.SizeThatFitsImage(constraintSize, Padding.IsNaN ? null : Padding).ToSize();
.SizeThatFitsImage(constraintSize, Padding.IsNaN ? default : Padding).ToSize();
}

return platformButton.SizeThatFits(constraintSize).ToSize();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,5 +340,7 @@ virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>.UpdateVisibility() -> void
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewDelegator2<TItemsView, TViewController>.GetVisibleItemsIndex() -> (bool VisibleItems, int First, int Center, int Last)
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.UpdateLayout() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
~Microsoft.Maui.Controls.Internals.TypedBindingBase.UpdateSourceEventName.set -> void
~Microsoft.Maui.Controls.Internals.TypedBindingBase.UpdateSourceEventName.set -> void
*REMOVED*override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.SetNeedsLayout() -> void
*REMOVED*override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.VisualElementRenderer<TElement>.MovedToWindow() -> void
Original file line number Diff line number Diff line change
Expand Up @@ -341,4 +341,6 @@ virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>.UpdateVisibility() -> void
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewDelegator2<TItemsView, TViewController>.GetVisibleItemsIndex() -> (bool VisibleItems, int First, int Center, int Last)
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.UpdateLayout() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
*REMOVED*override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.SetNeedsLayout() -> void
*REMOVED*override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.VisualElementRenderer<TElement>.MovedToWindow() -> void
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@ await CreateHandlerAndAddToWindow<CarouselViewHandler>(carouselView, async (hand
Assert.False(data.IsCollectionChangedEventEmpty);
});

carouselView.Handler?.DisconnectHandler();
await InvokeOnMainThreadAsync(() =>
{
carouselView.Handler?.DisconnectHandler();
});

Assert.True(data.IsCollectionChangedEventEmpty);
}
Expand Down
39 changes: 39 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue24996.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:issues="clr-namespace:Maui.Controls.Sample.Issues"
x:Class="Maui.Controls.Sample.Issues.Issue24996"
Title="Issue24996"
x:Name="Self">
<AbsoluteLayout BackgroundColor="DarkSlateBlue">
<AbsoluteLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="OnTapped" />
</AbsoluteLayout.GestureRecognizers>
<Grid RowDefinitions="*" ColumnDefinitions="*">
<issues:ObservedLayout24996 x:Name="Lvl1" BackgroundColor="BlueViolet">
<issues:ObservedLayout24996 x:Name="Lvl2" BackgroundColor="AliceBlue">
<issues:ObservedLayout24996 x:Name="Lvl3"
BackgroundColor="Aqua"
HeightRequest="200"
WidthRequest="200">
<Border BackgroundColor="GreenYellow" HeightRequest="100" WidthRequest="100" />
</issues:ObservedLayout24996>
</issues:ObservedLayout24996>
</issues:ObservedLayout24996>
</Grid>
<VerticalStackLayout AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0,1,-1,-1"
BackgroundColor="#222222">
<Label x:Name="Coords"
HeightRequest="40"
WidthRequest="{Binding Width, Source={x:Reference Self}}"
AutomationId="Coords"
TextColor="White"/>
<Label x:Name="Stats"
HeightRequest="40"
WidthRequest="{Binding Width, Source={x:Reference Self}}"
AutomationId="Stats"
TextColor="White"/>
</VerticalStackLayout>
</AbsoluteLayout>
</ContentPage>
73 changes: 73 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue24996.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 24996, "Changing Translation of an element causes Maui in iOS to constantly run Measure & ArrangeChildren", PlatformAffected.All)]
public partial class Issue24996 : ContentPage
{
Point[] _translations = [
new(40, 80),
new(1000, 20),
new(20, 1000),
new(1000, 1000),
];

int _index = -1;

public Issue24996()
{
InitializeComponent();
UpdateText();

SizeChanged += delegate {
if (Width > 0) {
// For some reason, constraining this layout to a fixed size causes a `SetNeedsLayout` to be called
// when translating the Lvl2 view outside the bottom boundary.
// This causes a layout pass to be called on the Root, Lvl1, Lvl2, and Lvl3.
Lvl1.WidthRequest = Width;
Lvl1.HeightRequest = Height;
}
};
}

protected override async void OnAppearing()
{
base.OnAppearing();
await Task.Delay(250);
Lvl1.MeasurePasses = Lvl1.ArrangePasses = 0;
Lvl2.MeasurePasses = Lvl2.ArrangePasses = 0;
Lvl3.MeasurePasses = Lvl3.ArrangePasses = 0;
UpdateText();
}

public async void OnTapped(object sender, EventArgs e)
{
var testPoint = _translations[++_index % _translations.Length];
Coords.Text = $"X: {testPoint.X}, Y: {testPoint.Y}";
Lvl2.TranslationX = testPoint.X;
Lvl2.TranslationY = testPoint.Y;
await Task.Delay(100);
UpdateText();
}

void UpdateText()
{
Stats.Text = $"Lvl1[{Lvl1.MeasurePasses}/{Lvl1.ArrangePasses}] - Lvl2[{Lvl2.MeasurePasses}/{Lvl2.ArrangePasses}] - Lvl3[{Lvl3.MeasurePasses}/{Lvl3.ArrangePasses}]";
}
}

public class ObservedLayout24996 : AbsoluteLayout
{
public int MeasurePasses { get; set; }
public int ArrangePasses { get; set; }

protected override Size MeasureOverride(double widthConstraint, double heightConstraint)
{
MeasurePasses++;
return base.MeasureOverride(widthConstraint, heightConstraint);
}

protected override Size ArrangeOverride(Rect bounds)
{
ArrangePasses++;
return base.ArrangeOverride(bounds);
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using NUnit.Framework;
using NUnit.Framework.Legacy;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class Issue24996 : _IssuesUITest
{
public Issue24996(TestDevice testDevice) : base(testDevice)
{
}

public override string Issue => "Changing Translation of an element causes Maui in iOS to constantly run Measure & ArrangeChildren";

[Test]
[Category(UITestCategories.Layout)]
public async Task ChangingTranslationShouldNotCauseLayoutPassOnAncestors()
{
var element = App.WaitForElement("Stats");
// Tries to translate the element in different positions, on-screen and off-screen.
for (int i = 0; i < 4; i++)
{
element.Tap();
await Task.Delay(150);
ClassicAssert.True(element.GetText()!.StartsWith("Lvl1[0/0]"));
}
}
}
}
5 changes: 5 additions & 0 deletions src/Core/src/Handlers/Button/ButtonHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ public override void SetImageSource(UIImage? platformImage)
platformImage = platformImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);

button.SetImage(platformImage, UIControlState.Normal);

// UIButton.SetImage(image, forState:) does not immediately assign the image to UIButton.ImageView.Image.
// Instead, the image is set internally and only applied to ImageView when the button is rendered.
// To ensure SizeThatFits is correct, and avoid race conditions, we have to force a layout.
button.LayoutIfNeeded();
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/Core/src/Handlers/ImageButton/ImageButtonHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ public override void SetImageSource(UIImage? platformImage)
button.SetImage(platformImage, UIControlState.Normal);
button.HorizontalAlignment = UIControlContentHorizontalAlignment.Fill;
button.VerticalAlignment = UIControlContentVerticalAlignment.Fill;

// UIButton.SetImage(image, forState:) does not immediately assign the image to UIButton.ImageView.Image.
// Instead, the image is set internally and only applied to ImageView when the button is rendered.
// To ensure SizeThatFits is correct, and avoid race conditions, we have to force a layout.
button.LayoutIfNeeded();
}
}

Expand Down
11 changes: 11 additions & 0 deletions src/Core/src/Handlers/Layout/LayoutHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public override void SetVirtualView(IView view)
{
PlatformView.AddSubview(child.ToPlatform(MauiContext));
}

PlatformView.InvalidateAncestorsMeasures();
}

public void Add(IView child)
Expand All @@ -54,6 +56,8 @@ public void Add(IView child)
{
childPlatformView.UpdateFlowDirection(child);
}

PlatformView.InvalidateAncestorsMeasures();
}

public void Remove(IView child)
Expand All @@ -65,11 +69,14 @@ public void Remove(IView child)
{
childView.RemoveFromSuperview();
}

PlatformView.InvalidateAncestorsMeasures();
}

public void Clear()
{
PlatformView.ClearSubviews();
PlatformView.InvalidateAncestorsMeasures();
}

public void Insert(int index, IView child)
Expand All @@ -86,6 +93,8 @@ public void Insert(int index, IView child)
{
childPlatformView.UpdateFlowDirection(child);
}

PlatformView.InvalidateAncestorsMeasures();
}

public void Update(int index, IView child)
Expand All @@ -99,6 +108,7 @@ public void Update(int index, IView child)
var targetIndex = VirtualView.GetLayoutHandlerIndex(child);
PlatformView.InsertSubview(child.ToPlatform(MauiContext), targetIndex);
PlatformView.SetNeedsLayout();
PlatformView.InvalidateAncestorsMeasures();
}

public void UpdateZIndex(IView child)
Expand Down Expand Up @@ -137,6 +147,7 @@ void EnsureZIndexOrder(IView child)
{
PlatformView.Subviews.RemoveAt(currentIndex);
PlatformView.InsertSubview(nativeChildView, targetIndex);
PlatformView.InvalidateAncestorsMeasures();
}
}

Expand Down
Loading
Loading