Skip to content

Commit 179c8c4

Browse files
authored
Change iOS SetNeedsLayout propagation mechanism (#26629)
1 parent 85d53f6 commit 179c8c4

File tree

26 files changed

+390
-151
lines changed

26 files changed

+390
-151
lines changed

src/Controls/src/Core/Compatibility/Handlers/iOS/FrameRenderer.cs

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -217,34 +217,6 @@ protected override void Dispose(bool disposing)
217217
}
218218
}
219219

220-
bool _pendingSuperViewSetNeedsLayout;
221-
222-
public override void SetNeedsLayout()
223-
{
224-
base.SetNeedsLayout();
225-
226-
if (Window is not null)
227-
{
228-
_pendingSuperViewSetNeedsLayout = false;
229-
this.Superview?.SetNeedsLayout();
230-
}
231-
else
232-
{
233-
_pendingSuperViewSetNeedsLayout = true;
234-
}
235-
}
236-
237-
public override void MovedToWindow()
238-
{
239-
base.MovedToWindow();
240-
if (_pendingSuperViewSetNeedsLayout)
241-
{
242-
this.Superview?.SetNeedsLayout();
243-
}
244-
245-
_pendingSuperViewSetNeedsLayout = false;
246-
}
247-
248220
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
249221
class FrameView : Microsoft.Maui.Platform.ContentView
250222
{

src/Controls/src/Core/Compatibility/Handlers/iOS/VisualElementRenderer.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
using System;
22
using System.ComponentModel;
33
using Microsoft.Maui.Controls.Platform;
4-
using Microsoft.Maui.Graphics;
54
using UIKit;
65

76
namespace Microsoft.Maui.Controls.Handlers.Compatibility
87
{
9-
public abstract partial class VisualElementRenderer<TElement> : UIView, IPlatformViewHandler, IElementHandler
8+
public abstract partial class VisualElementRenderer<TElement> : UIView, IPlatformViewHandler, IElementHandler, IPlatformMeasureInvalidationController
109
where TElement : Element, IView
1110
{
11+
bool _invalidateParentWhenMovedToWindow;
1212
object? IElementHandler.PlatformView => Subviews.Length > 0 ? Subviews[0] : this;
1313

1414
public virtual UIViewController? ViewController => null;
@@ -93,5 +93,22 @@ void OnSizeChanged(object? sender, EventArgs e)
9393
{
9494
UpdateNativeWidget();
9595
}
96+
97+
void IPlatformMeasureInvalidationController.InvalidateAncestorsMeasuresWhenMovedToWindow()
98+
{
99+
_invalidateParentWhenMovedToWindow = true;
100+
}
101+
102+
void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating) => SetNeedsLayout();
103+
104+
public override void MovedToWindow()
105+
{
106+
base.MovedToWindow();
107+
if (_invalidateParentWhenMovedToWindow)
108+
{
109+
_invalidateParentWhenMovedToWindow = false;
110+
this.InvalidateAncestorsMeasures();
111+
}
112+
}
96113
}
97114
}

src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55

66
namespace Microsoft.Maui.Controls.Handlers.Items;
77

8-
internal class MauiCollectionView : UICollectionView, IUIViewLifeCycleEvents
8+
internal class MauiCollectionView : UICollectionView, IUIViewLifeCycleEvents, IPlatformMeasureInvalidationController
99
{
10+
bool _invalidateParentWhenMovedToWindow;
11+
1012
WeakReference<ICustomMauiCollectionViewDelegate>? _customDelegate;
1113
public MauiCollectionView(CGRect frame, UICollectionViewLayout layout) : base(frame, layout)
1214
{
@@ -18,8 +20,22 @@ public override void ScrollRectToVisible(CGRect rect, bool animated)
1820
base.ScrollRectToVisible(rect, animated);
1921
}
2022

23+
void IPlatformMeasureInvalidationController.InvalidateAncestorsMeasuresWhenMovedToWindow()
24+
{
25+
_invalidateParentWhenMovedToWindow = true;
26+
}
27+
28+
void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
29+
{
30+
if (!isPropagating)
31+
{
32+
SetNeedsLayout();
33+
}
34+
}
35+
2136
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = IUIViewLifeCycleEvents.UnconditionalSuppressMessage)]
2237
EventHandler? _movedToWindow;
38+
2339
event EventHandler? IUIViewLifeCycleEvents.MovedToWindow
2440
{
2541
add => _movedToWindow += value;
@@ -35,6 +51,12 @@ public override void MovedToWindow()
3551
{
3652
target.MovedToWindow(this);
3753
}
54+
55+
if (_invalidateParentWhenMovedToWindow)
56+
{
57+
_invalidateParentWhenMovedToWindow = false;
58+
this.InvalidateAncestorsMeasures();
59+
}
3860
}
3961

4062
internal void SetCustomDelegate(ICustomMauiCollectionViewDelegate customDelegate)

src/Controls/src/Core/ImageButton/ImageButton.iOS.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Size ICrossPlatformLayout.CrossPlatformMeasure(double widthConstraint, double he
2525
if (platformButton.ImageView.Image is not null)
2626
{
2727
return platformButton.ImageView
28-
.SizeThatFitsImage(constraintSize, Padding.IsNaN ? null : Padding).ToSize();
28+
.SizeThatFitsImage(constraintSize, Padding.IsNaN ? default : Padding).ToSize();
2929
}
3030

3131
return platformButton.SizeThatFits(constraintSize).ToSize();

src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,5 +340,7 @@ virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>
340340
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>.UpdateVisibility() -> void
341341
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewDelegator2<TItemsView, TViewController>.GetVisibleItemsIndex() -> (bool VisibleItems, int First, int Center, int Last)
342342
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.UpdateLayout() -> void
343-
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
344-
~Microsoft.Maui.Controls.Internals.TypedBindingBase.UpdateSourceEventName.set -> void
343+
~Microsoft.Maui.Controls.Internals.TypedBindingBase.UpdateSourceEventName.set -> void
344+
*REMOVED*override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.SetNeedsLayout() -> void
345+
*REMOVED*override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
346+
override Microsoft.Maui.Controls.Handlers.Compatibility.VisualElementRenderer<TElement>.MovedToWindow() -> void

src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,4 +341,6 @@ virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>
341341
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>.UpdateVisibility() -> void
342342
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewDelegator2<TItemsView, TViewController>.GetVisibleItemsIndex() -> (bool VisibleItems, int First, int Center, int Last)
343343
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.UpdateLayout() -> void
344-
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
344+
*REMOVED*override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.SetNeedsLayout() -> void
345+
*REMOVED*override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
346+
override Microsoft.Maui.Controls.Handlers.Compatibility.VisualElementRenderer<TElement>.MovedToWindow() -> void

src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,10 @@ await CreateHandlerAndAddToWindow<CarouselViewHandler>(carouselView, async (hand
147147
Assert.False(data.IsCollectionChangedEventEmpty);
148148
});
149149

150-
carouselView.Handler?.DisconnectHandler();
150+
await InvokeOnMainThreadAsync(() =>
151+
{
152+
carouselView.Handler?.DisconnectHandler();
153+
});
151154

152155
Assert.True(data.IsCollectionChangedEventEmpty);
153156
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
xmlns:issues="clr-namespace:Maui.Controls.Sample.Issues"
5+
x:Class="Maui.Controls.Sample.Issues.Issue24996"
6+
Title="Issue24996"
7+
x:Name="Self">
8+
<AbsoluteLayout BackgroundColor="DarkSlateBlue">
9+
<AbsoluteLayout.GestureRecognizers>
10+
<TapGestureRecognizer Tapped="OnTapped" />
11+
</AbsoluteLayout.GestureRecognizers>
12+
<Grid RowDefinitions="*" ColumnDefinitions="*">
13+
<issues:ObservedLayout24996 x:Name="Lvl1" BackgroundColor="BlueViolet">
14+
<issues:ObservedLayout24996 x:Name="Lvl2" BackgroundColor="AliceBlue">
15+
<issues:ObservedLayout24996 x:Name="Lvl3"
16+
BackgroundColor="Aqua"
17+
HeightRequest="200"
18+
WidthRequest="200">
19+
<Border BackgroundColor="GreenYellow" HeightRequest="100" WidthRequest="100" />
20+
</issues:ObservedLayout24996>
21+
</issues:ObservedLayout24996>
22+
</issues:ObservedLayout24996>
23+
</Grid>
24+
<VerticalStackLayout AbsoluteLayout.LayoutFlags="PositionProportional"
25+
AbsoluteLayout.LayoutBounds="0,1,-1,-1"
26+
BackgroundColor="#222222">
27+
<Label x:Name="Coords"
28+
HeightRequest="40"
29+
WidthRequest="{Binding Width, Source={x:Reference Self}}"
30+
AutomationId="Coords"
31+
TextColor="White"/>
32+
<Label x:Name="Stats"
33+
HeightRequest="40"
34+
WidthRequest="{Binding Width, Source={x:Reference Self}}"
35+
AutomationId="Stats"
36+
TextColor="White"/>
37+
</VerticalStackLayout>
38+
</AbsoluteLayout>
39+
</ContentPage>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
namespace Maui.Controls.Sample.Issues;
2+
3+
[Issue(IssueTracker.Github, 24996, "Changing Translation of an element causes Maui in iOS to constantly run Measure & ArrangeChildren", PlatformAffected.All)]
4+
public partial class Issue24996 : ContentPage
5+
{
6+
Point[] _translations = [
7+
new(40, 80),
8+
new(1000, 20),
9+
new(20, 1000),
10+
new(1000, 1000),
11+
];
12+
13+
int _index = -1;
14+
15+
public Issue24996()
16+
{
17+
InitializeComponent();
18+
UpdateText();
19+
20+
SizeChanged += delegate {
21+
if (Width > 0) {
22+
// For some reason, constraining this layout to a fixed size causes a `SetNeedsLayout` to be called
23+
// when translating the Lvl2 view outside the bottom boundary.
24+
// This causes a layout pass to be called on the Root, Lvl1, Lvl2, and Lvl3.
25+
Lvl1.WidthRequest = Width;
26+
Lvl1.HeightRequest = Height;
27+
}
28+
};
29+
}
30+
31+
protected override async void OnAppearing()
32+
{
33+
base.OnAppearing();
34+
await Task.Delay(250);
35+
Lvl1.MeasurePasses = Lvl1.ArrangePasses = 0;
36+
Lvl2.MeasurePasses = Lvl2.ArrangePasses = 0;
37+
Lvl3.MeasurePasses = Lvl3.ArrangePasses = 0;
38+
UpdateText();
39+
}
40+
41+
public async void OnTapped(object sender, EventArgs e)
42+
{
43+
var testPoint = _translations[++_index % _translations.Length];
44+
Coords.Text = $"X: {testPoint.X}, Y: {testPoint.Y}";
45+
Lvl2.TranslationX = testPoint.X;
46+
Lvl2.TranslationY = testPoint.Y;
47+
await Task.Delay(100);
48+
UpdateText();
49+
}
50+
51+
void UpdateText()
52+
{
53+
Stats.Text = $"Lvl1[{Lvl1.MeasurePasses}/{Lvl1.ArrangePasses}] - Lvl2[{Lvl2.MeasurePasses}/{Lvl2.ArrangePasses}] - Lvl3[{Lvl3.MeasurePasses}/{Lvl3.ArrangePasses}]";
54+
}
55+
}
56+
57+
public class ObservedLayout24996 : AbsoluteLayout
58+
{
59+
public int MeasurePasses { get; set; }
60+
public int ArrangePasses { get; set; }
61+
62+
protected override Size MeasureOverride(double widthConstraint, double heightConstraint)
63+
{
64+
MeasurePasses++;
65+
return base.MeasureOverride(widthConstraint, heightConstraint);
66+
}
67+
68+
protected override Size ArrangeOverride(Rect bounds)
69+
{
70+
ArrangePasses++;
71+
return base.ArrangeOverride(bounds);
72+
}
73+
}
-136 Bytes
Loading

0 commit comments

Comments
 (0)