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

Measure invalidation performance and fixes #24532

Closed
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 @@ -20,7 +20,7 @@

namespace Microsoft.Maui.Controls.Handlers.Compatibility
{
public class ListViewRenderer : ViewRenderer<ListView, UITableView>
public class ListViewRenderer : ViewRenderer<ListView, UITableView>, IPropagatesSetNeedsLayout
{
public static PropertyMapper<ListView, ListViewRenderer> Mapper =
new PropertyMapper<ListView, ListViewRenderer>(VisualElementRendererMapper);
Expand Down Expand Up @@ -62,6 +62,33 @@ public ListViewRenderer() : base(Mapper, CommandMapper)
AutoPackage = false;
}

bool _pendingSuperViewSetNeedsLayout;

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

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

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

_pendingSuperViewSetNeedsLayout = false;
}

public override void LayoutSubviews()
{
base.LayoutSubviews();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace Microsoft.Maui.Controls.Handlers.Compatibility
{
public class TableViewRenderer : ViewRenderer<TableView, UITableView>
public class TableViewRenderer : ViewRenderer<TableView, UITableView>, IPropagatesSetNeedsLayout
{
const int DefaultRowHeight = 44;
UIView _originalBackgroundView;
Expand All @@ -33,6 +33,33 @@ public override void LayoutSubviews()
_previousFrame = Frame;
}

bool _pendingSuperViewSetNeedsLayout;

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

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

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

_pendingSuperViewSetNeedsLayout = false;
}

protected override void Dispose(bool disposing)
{
if (disposing)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Microsoft.Maui.Controls.Handlers.Compatibility
{
public class FrameRenderer : VisualElementRenderer<Frame>
public class FrameRenderer : VisualElementRenderer<Frame>, IPropagatesSetNeedsLayout
{
public static IPropertyMapper<Frame, FrameRenderer> Mapper
= new PropertyMapper<Frame, FrameRenderer>(VisualElementRendererMapper);
Expand Down
66 changes: 16 additions & 50 deletions src/Controls/src/Core/LegacyLayouts/Layout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,12 @@ private protected override IList<Element> LogicalChildrenInternalBackingStore
public override SizeRequest Measure(double widthConstraint, double heightConstraint, MeasureFlags flags = MeasureFlags.None)
{
SizeRequest size = base.Measure(widthConstraint - Padding.HorizontalThickness, heightConstraint - Padding.VerticalThickness, flags);
return new SizeRequest(new Size(size.Request.Width + Padding.HorizontalThickness, size.Request.Height + Padding.VerticalThickness),
new Size(size.Minimum.Width + Padding.HorizontalThickness, size.Minimum.Height + Padding.VerticalThickness));
var request = new Size(size.Request.Width + Padding.HorizontalThickness, size.Request.Height + Padding.VerticalThickness);
var minimum = new Size(size.Minimum.Width + Padding.HorizontalThickness, size.Minimum.Height + Padding.VerticalThickness);

DesiredSize = request;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Base method sets DesiredSize so the fact this was missing was simply wrong.


return new SizeRequest(request, minimum);
}

/// <summary>
Expand Down Expand Up @@ -294,13 +298,19 @@ public void RaiseChild(View view)
OnChildrenReordered();
}

internal virtual void InvalidateLayoutInternal()
{
_hasDoneLayout = false;
InvalidateMeasureCacheInternal();
}

/// <summary>
/// Invalidates the current layout.
/// </summary>
/// <remarks>Calling this method will invalidate the measure and triggers a new layout cycle.</remarks>
protected virtual void InvalidateLayout()
{
_hasDoneLayout = false;
InvalidateLayoutInternal();
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
if (!_hasDoneLayout)
{
Expand All @@ -321,8 +331,8 @@ protected virtual void InvalidateLayout()

internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
{
// TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
base.OnChildMeasureInvalidatedInternal(child, trigger);
}

/// <summary>
Expand All @@ -334,8 +344,7 @@ internal override void OnChildMeasureInvalidatedInternal(VisualElement child, In
/// <remarks>This method has a default implementation and application developers must call the base implementation.</remarks>
protected void OnChildMeasureInvalidated(object sender, EventArgs e)
{
InvalidationTrigger trigger = (e as InvalidationEventArgs)?.Trigger ?? InvalidationTrigger.Undefined;
OnChildMeasureInvalidated((VisualElement)sender, trigger);
InvalidateLayoutInternal();
OnChildMeasureInvalidated();
}

Expand Down Expand Up @@ -497,42 +506,6 @@ internal static void LayoutChildIntoBoundingRegion(View child, Rect region, Size
child.Layout(region);
}

internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
{
IReadOnlyList<Element> children = LogicalChildrenInternal;
int count = children.Count;
for (var index = 0; index < count; index++)
{
if (LogicalChildrenInternal[index] is VisualElement v && v.IsVisible && (!v.IsPlatformEnabled || !v.IsPlatformStateConsistent))
{
return;
}
}

if (child is View view)
{
// we can ignore the request if we are either fully constrained or when the size request changes and we were already fully constrained
if ((trigger == InvalidationTrigger.MeasureChanged && view.Constraint == LayoutConstraint.Fixed) ||
(trigger == InvalidationTrigger.SizeRequestChanged && view.ComputedConstraint == LayoutConstraint.Fixed))
{
return;
}
if (trigger == InvalidationTrigger.HorizontalOptionsChanged || trigger == InvalidationTrigger.VerticalOptionsChanged)
{
ComputeConstraintForView(view);
}
}

if (trigger == InvalidationTrigger.RendererReady)
{
InvalidateMeasureInternal(InvalidationTrigger.RendererReady);
}
else
{
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
}
}

internal override void OnIsVisibleChanged(bool oldValue, bool newValue)
{
base.OnIsVisibleChanged(oldValue, newValue);
Expand Down Expand Up @@ -645,15 +618,8 @@ bool ShouldLayoutChildren()

protected override void InvalidateMeasureOverride()
{
InvalidateLayoutInternal();
base.InvalidateMeasureOverride();

foreach (var child in ((IElementController)this).LogicalChildren)
{
if (child is IView fe)
{
fe.InvalidateMeasure();
}
}
}

protected override Size ArrangeOverride(Rect bounds)
Expand Down
8 changes: 7 additions & 1 deletion src/Controls/src/Core/LegacyLayouts/StackLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,15 @@ internal override void ComputeConstraintForView(View view)
ComputeConstraintForView(view, false);
}

internal override void InvalidateMeasureInternal(InvalidationTrigger trigger)
internal override void InvalidateLayoutInternal()
{
base.InvalidateLayoutInternal();
_layoutInformation = new LayoutInformation();
}

internal override void InvalidateMeasureInternal(InvalidationTrigger trigger)
{
InvalidateLayoutInternal();
base.InvalidateMeasureInternal(trigger);
}

Expand Down
46 changes: 20 additions & 26 deletions src/Controls/src/Core/Page/Page.cs
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,25 @@ protected override void OnBindingContextChanged()

internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
{
// TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly
// Behave like `VisualElement` except for propagation to parent
switch (trigger)
{
case InvalidationTrigger.Undefined:
InvokeMeasureInvalidated(InvalidationTrigger.MeasureChanged);
break;

default:
// When visibility changes `InvalidationTrigger.Undefined` is used,
// so here we're sure that visibility didn't change
if (child.IsVisible)
{
// We need to invalidate measures only if child is actually visible
InvokeMeasureInvalidated(InvalidationTrigger.MeasureChanged);
}
break;
}

// We still need to call the legacy OnChildMeasureInvalidated to keep the compatibility.
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
}

Expand All @@ -510,8 +528,7 @@ internal override void OnChildMeasureInvalidatedInternal(VisualElement child, In
/// <param name="e">The event arguments.</param>
protected virtual void OnChildMeasureInvalidated(object sender, EventArgs e)
{
InvalidationTrigger trigger = (e as InvalidationEventArgs)?.Trigger ?? InvalidationTrigger.Undefined;
OnChildMeasureInvalidated((VisualElement)sender, trigger);
// Nothing to do here: platform will take care of arranging the children if needed on the next layout pass
}

/// <summary>
Expand Down Expand Up @@ -583,29 +600,6 @@ protected void UpdateChildrenLayout()
}
}

internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
{
var container = this as IPageContainer<Page>;
if (container != null)
{
Page page = container.CurrentPage;
if (page != null && page.IsVisible && (!page.IsPlatformEnabled || !page.IsPlatformStateConsistent))
return;
}
else
{
var logicalChildren = this.InternalChildren;
for (var i = 0; i < logicalChildren.Count; i++)
{
var v = logicalChildren[i] as VisualElement;
if (v != null && v.IsVisible && (!v.IsPlatformEnabled || !v.IsPlatformStateConsistent))
return;
}
}

InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
PureWeen marked this conversation as resolved.
Show resolved Hide resolved
}

internal void OnAppearing(Action action)
{
if (_hasAppeared)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
#nullable enable
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.ListViewRenderer.MovedToWindow() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.ListViewRenderer.SetNeedsLayout() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.TableViewRenderer.MovedToWindow() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.TableViewRenderer.SetNeedsLayout() -> void
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
#nullable enable
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.ListViewRenderer.MovedToWindow() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.ListViewRenderer.SetNeedsLayout() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.TableViewRenderer.MovedToWindow() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.TableViewRenderer.SetNeedsLayout() -> void
Loading
Loading