Skip to content

Stop iOS SetNeedsLayout propagation by looking at VisualElement computed Constraint #28479

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

Merged
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 @@ -99,7 +99,11 @@ void IPlatformMeasureInvalidationController.InvalidateAncestorsMeasuresWhenMoved
_invalidateParentWhenMovedToWindow = true;
}

void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating) => SetNeedsLayout();
bool IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
{
SetNeedsLayout();
return true;
}

public override void MovedToWindow()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ void IPlatformMeasureInvalidationController.InvalidateAncestorsMeasuresWhenMoved
_invalidateParentWhenMovedToWindow = true;
}

void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
bool IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
{
if (isPropagating)
{
NeedsCellLayout = true;
}

SetNeedsLayout();
return !isPropagating;
}

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = IUIViewLifeCycleEvents.UnconditionalSuppressMessage)]
Expand Down
5 changes: 4 additions & 1 deletion src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,14 +306,17 @@ public override bool Selected

protected abstract (bool, Size) NeedsContentSizeUpdate(Size currentSize);

void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
bool IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
{
// If the cell is not bound (or getting unbounded), we don't want to measure it
// and cause a useless and harming InvalidateLayout on the collection view layout
if (!_measureInvalidated && _bound)
{
_measureInvalidated = true;
return true;
}

return false;
}

protected void OnContentSizeChanged()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,14 +306,17 @@ void IPlatformMeasureInvalidationController.InvalidateAncestorsMeasuresWhenMoved
// This is a no-op
}

void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
bool IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
{
// If the cell is not bound (or getting unbounded), we don't want to measure it
// and cause a useless and harming InvalidateLayout on the collection view layout
if (!_measureInvalidated && _bound)
{
_measureInvalidated = true;
return true;
}

return false;
}
}
}
4 changes: 3 additions & 1 deletion src/Controls/src/Core/Page/Page.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace Microsoft.Maui.Controls
/// <remarks><see cref = "Page" /> is primarily a base class for more useful derived types. Objects that are derived from the <see cref="Page"/> class are most prominently used as the top level UI element in .NET MAUI applications. In addition to their role as the main pages of applications, <see cref="Page"/> objects and their descendants can be used with navigation classes, such as <see cref="NavigationPage"/> or <see cref="FlyoutPage"/>, among others, to provide rich user experiences that conform to the expected behaviors on each platform.
/// </remarks>
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
public partial class Page : VisualElement, ILayout, IPageController, IElementConfiguration<Page>, IPaddingElement, ISafeAreaView, ISafeAreaView2, IView, ITitledElement, IToolbarElement
public partial class Page : VisualElement, ILayout, IPageController, IElementConfiguration<Page>, IPaddingElement, ISafeAreaView, ISafeAreaView2, IView, ITitledElement, IToolbarElement, IConstrainedView
#if IOS
,IiOSPageSpecifics
#endif
Expand Down Expand Up @@ -212,6 +212,8 @@ public bool IgnoresContainerArea
/// <inheritdoc/>
bool ISafeAreaView.IgnoreSafeArea => !On<PlatformConfiguration.iOS>().UsingSafeArea();

bool IConstrainedView.HasFixedConstraints => true;

#if IOS
/// <inheritdoc/>
bool IiOSPageSpecifics.IsHomeIndicatorAutoHidden
Expand Down
4 changes: 3 additions & 1 deletion src/Controls/src/Core/VisualElement/VisualElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace Microsoft.Maui.Controls
/// </remarks>

[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
public partial class VisualElement : NavigableElement, IAnimatable, IVisualElementController, IResourcesProvider, IStyleElement, IFlowDirectionController, IPropertyPropagationController, IVisualController, IWindowController, IView, IControlsVisualElement
public partial class VisualElement : NavigableElement, IAnimatable, IVisualElementController, IResourcesProvider, IStyleElement, IFlowDirectionController, IPropertyPropagationController, IVisualController, IWindowController, IView, IControlsVisualElement, IConstrainedView
{
/// <summary>Bindable property for <see cref="NavigableElement.Navigation"/>.</summary>
public new static readonly BindableProperty NavigationProperty = NavigableElement.NavigationProperty;
Expand Down Expand Up @@ -978,6 +978,8 @@ internal LayoutConstraint ComputedConstraint

internal LayoutConstraint Constraint => ComputedConstraint | SelfConstraint;

bool IConstrainedView.HasFixedConstraints => Constraint == LayoutConstraint.Fixed;

/// <summary>
/// Gets a value that indicates that layout for this element is disabled.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Core/src/Core/IConstrainedView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Microsoft.Maui;

internal interface IConstrainedView
{
bool HasFixedConstraints { get; }
}
8 changes: 0 additions & 8 deletions src/Core/src/Handlers/Page/PageHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@ protected override ContentView CreatePlatformView()
throw new InvalidOperationException($"PageViewController.View must be a {nameof(ContentView)}");
}

public override void SetVirtualView(IView view)
{
base.SetVirtualView(view);

// Ensure we tag the ContentView as a Page so that InvalidateAncestorsMeasures can stop propagation here
PlatformView.IsPage = true;
}

public static void MapBackground(IPageHandler handler, IContentView page)
{
if (handler is IPlatformViewHandler platformViewHandler && platformViewHandler.ViewController is not null)
Expand Down
2 changes: 0 additions & 2 deletions src/Core/src/Platform/iOS/ContentView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ public class ContentView : MauiView
// verify we're using the correct subview for masking (and any other purposes)
internal const nint ContentTag = 0x63D2A0;

internal bool IsPage { get; set; }

public ContentView()
{
if (OperatingSystem.IsIOSVersionAtLeast(13) || OperatingSystem.IsMacCatalystVersionAtLeast(13, 1))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ namespace Microsoft.Maui.Platform;
internal interface IPlatformMeasureInvalidationController
{
void InvalidateAncestorsMeasuresWhenMovedToWindow();
void InvalidateMeasure(bool isPropagating = false);
bool InvalidateMeasure(bool isPropagating = false);
}
4 changes: 3 additions & 1 deletion src/Core/src/Platform/iOS/MauiScrollView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,12 @@ void IPlatformMeasureInvalidationController.InvalidateAncestorsMeasuresWhenMoved
_invalidateParentWhenMovedToWindow = true;
}

void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
bool IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
{
SetNeedsLayout();
InvalidateConstraintsCache();

return !isPropagating;
}

bool IsMeasureValid(double widthConstraint, double heightConstraint)
Expand Down
31 changes: 28 additions & 3 deletions src/Core/src/Platform/iOS/MauiView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public IView? View
set => _reference = value == null ? null : new(value);
}

bool HasFixedConstraints => CrossPlatformLayout is IConstrainedView { HasFixedConstraints: true };

bool RespondsToSafeArea()
{
if (_respondsToSafeArea.HasValue)
Expand Down Expand Up @@ -55,6 +57,11 @@ protected bool IsMeasureValid(double widthConstraint, double heightConstraint)
return heightConstraint == _lastMeasureHeight && widthConstraint == _lastMeasureWidth;
}

bool HasBeenMeasured()
{
return !double.IsNaN(_lastMeasureWidth) && !double.IsNaN(_lastMeasureHeight);
}

protected void InvalidateConstraintsCache()
{
_lastMeasureWidth = double.NaN;
Expand Down Expand Up @@ -128,10 +135,11 @@ public override void LayoutSubviews()
// If the SuperView is a cross-platform layout backed view (i.e. MauiView, MauiScrollView, LayoutView, ..),
// then measurement has already happened via SizeThatFits and doesn't need to be repeated in LayoutSubviews.
// This is especially important to avoid overriding potentially infinite measurement constraints
// imposed by the parent (i.e. scroll view) with the current bounds.
// imposed by the parent (i.e. scroll view) with the current bounds, except when our bounds are fixed by constraints.
// But we _do_ need LayoutSubviews to make a measurement pass if the parent is something else (for example,
// the window); there's no guarantee that SizeThatFits has been called in that case.
if (!IsMeasureValid(widthConstraint, heightConstraint) && !this.IsFinalMeasureHandledBySuperView())
if (!IsMeasureValid(widthConstraint, heightConstraint) && !this.IsFinalMeasureHandledBySuperView() ||
!HasBeenMeasured() && HasFixedConstraints)
{
CrossPlatformMeasure(widthConstraint, heightConstraint);
CacheMeasureConstraints(widthConstraint, heightConstraint);
Expand Down Expand Up @@ -163,10 +171,27 @@ void IPlatformMeasureInvalidationController.InvalidateAncestorsMeasuresWhenMoved
_invalidateParentWhenMovedToWindow = true;
}

void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
bool IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
{
InvalidateConstraintsCache();
SetNeedsLayout();

// If we're propagating, we can stop at the first view with fixed constraints
if (isPropagating && HasFixedConstraints)
{
// We're stopping propagation here, but we have to account for the wrapper view
// which needs to be invalidated for consistency too.
if (Superview is WrapperView wrapper)
{
wrapper.SetNeedsLayout();
}

return false;
}

// If we're not propagating, then this view is the one triggering the invalidation
// and one possible cause is that constraints have changed, so we have to propagate the invalidation.
return true;
}

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = IUIViewLifeCycleEvents.UnconditionalSuppressMessage)]
Expand Down
22 changes: 13 additions & 9 deletions src/Core/src/Platform/iOS/ViewExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,16 +287,21 @@ public static void InvalidateMeasure(this UIView platformView, IView view)

internal static void InvalidateMeasure(this UIView platformView)
{
var propagate = true;

if (platformView is IPlatformMeasureInvalidationController mauiPlatformView)
{
mauiPlatformView.InvalidateMeasure();
propagate = mauiPlatformView.InvalidateMeasure();
}
else
{
platformView.SetNeedsLayout();
}

platformView.InvalidateAncestorsMeasures();
if (propagate)
{
platformView.InvalidateAncestorsMeasures();
}
}

internal static void InvalidateAncestorsMeasures(this UIView child)
Expand All @@ -322,22 +327,20 @@ internal static void InvalidateAncestorsMeasures(this UIView child)
}

// Now invalidate the parent view
var propagate = true;
var superviewMauiPlatformLayout = superview as IPlatformMeasureInvalidationController;
if (superviewMauiPlatformLayout is not null)
{
superviewMauiPlatformLayout.InvalidateMeasure(isPropagating: true);
propagate = superviewMauiPlatformLayout.InvalidateMeasure(isPropagating: true);
}
else
{
superview.SetNeedsLayout();
}

// Potential improvement: if the MAUI view (superview here) is constrained to a fixed size, we could stop propagating
// when doing this, we must pay attention to a scenario where a non-fixed-size view becomes fixed-size
if (superview is ContentView { IsPage: true } or UIScrollView)
if (!propagate)
{
// We reached the root view or a scrollable area (includes collection view), stop propagating
// The view will eventually watch its content size and invoke InvalidateAncestorsMeasures when needed
// We've been asked to stop propagation, so let's stop here
return;
}

Expand Down Expand Up @@ -1013,7 +1016,8 @@ internal static float GetDisplayDensity(this UIView? view) =>

private const nint NativeViewControlledByCrossPlatformLayout = 0x63D2A1;

internal static bool IsFinalMeasureHandledBySuperView(this UIView? view) => view?.Superview is ICrossPlatformLayoutBacking { CrossPlatformLayout: not null } or { Tag: NativeViewControlledByCrossPlatformLayout };
internal static bool IsFinalMeasureHandledBySuperView(this UIView? view)
=> view?.Superview is ICrossPlatformLayoutBacking { CrossPlatformLayout: not null } or { Tag: NativeViewControlledByCrossPlatformLayout };

internal static void MarkAsCrossPlatformLayoutBacking(this UIView view)
{
Expand Down
3 changes: 2 additions & 1 deletion src/Core/src/Platform/iOS/WrapperView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -319,10 +319,11 @@ void IPlatformMeasureInvalidationController.InvalidateAncestorsMeasuresWhenMoved
_invalidateParentWhenMovedToWindow = true;
}

void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
bool IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
{
InvalidateConstraintsCache();
SetNeedsLayout();
return true;
}

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = IUIViewLifeCycleEvents.UnconditionalSuppressMessage)]
Expand Down
1 change: 0 additions & 1 deletion src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,5 @@ override Microsoft.Maui.Handlers.EditorHandler.NeedsContainer.get -> bool
override Microsoft.Maui.Handlers.WindowHandler.DisconnectHandler(UIKit.UIWindow! platformView) -> void
*REMOVED*override Microsoft.Maui.Handlers.ScrollViewHandler.NeedsContainer.get -> bool
override Microsoft.Maui.Handlers.ImageButtonHandler.SetupContainer() -> void
override Microsoft.Maui.Handlers.PageHandler.SetVirtualView(Microsoft.Maui.IView! view) -> void
*REMOVED*override Microsoft.Maui.Platform.WrapperView.SetNeedsLayout() -> void
*REMOVED*override Microsoft.Maui.Platform.MauiView.SetNeedsLayout() -> void
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,5 @@ override Microsoft.Maui.Handlers.EditorHandler.NeedsContainer.get -> bool
override Microsoft.Maui.Handlers.WindowHandler.DisconnectHandler(UIKit.UIWindow! platformView) -> void
*REMOVED*override Microsoft.Maui.Handlers.ScrollViewHandler.NeedsContainer.get -> bool
override Microsoft.Maui.Handlers.ImageButtonHandler.SetupContainer() -> void
override Microsoft.Maui.Handlers.PageHandler.SetVirtualView(Microsoft.Maui.IView! view) -> void
*REMOVED*override Microsoft.Maui.Platform.WrapperView.SetNeedsLayout() -> void
*REMOVED*override Microsoft.Maui.Platform.MauiView.SetNeedsLayout() -> void
Loading