Skip to content

Commit

Permalink
Measure invalidation performance and fixes (dotnet#24823)
Browse files Browse the repository at this point in the history
  • Loading branch information
albyrock87 committed Oct 19, 2024
1 parent 3c64c3f commit d73fa74
Show file tree
Hide file tree
Showing 29 changed files with 783 additions and 148 deletions.
12 changes: 8 additions & 4 deletions src/Controls/src/Core/BindableObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,16 @@ public static void SetInheritedBindingContext(BindableObject bindable, object va
else
{
bindable._inheritedContext = new WeakReference(value);
bindable.ApplyBindings(fromBindingContextChanged: true);
bindable.OnBindingContextChanged();
bindable.ApplyBindingsFromBindingContextChanged();
}
}

private protected virtual void ApplyBindingsFromBindingContextChanged()
{
ApplyBindings(fromBindingContextChanged: true);
OnBindingContextChanged();
}

/// <summary>
/// Applies all the current bindings to <see cref="BindingContext" />.
/// </summary>
Expand Down Expand Up @@ -717,8 +722,7 @@ static void BindingContextPropertyBindingChanging(BindableObject bindable, Bindi
static void BindingContextPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
bindable._inheritedContext = null;
bindable.ApplyBindings(fromBindingContextChanged: true);
bindable.OnBindingContextChanged();
bindable.ApplyBindingsFromBindingContextChanged();
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
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
43 changes: 43 additions & 0 deletions src/Controls/src/Core/Internals/InvalidationTriggerFlags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;

namespace Microsoft.Maui.Controls.Internals;

[Flags]
internal enum InvalidationTriggerFlags : ushort
{
None = 0,
ApplyingBindingContext = 1 << 0,
WillNotifyParentMeasureInvalidated = 1 << 1,
WillTriggerHorizontalOptionsChanged = 1 << 2,
WillTriggerVerticalOptionsChanged = 1 << 3,
WillTriggerMarginChanged = 1 << 4,
WillTriggerSizeRequestChanged = 1 << 5,
WillTriggerMeasureChanged = 1 << 6,
WillTriggerRendererReady = 1 << 7,
WillTriggerUndefined = 1 << 8,
}

internal static class InvalidationTriggerFlagsExtensions {
public static InvalidationTriggerFlags ToInvalidationTriggerFlags(this InvalidationTrigger trigger) {
return trigger switch {
InvalidationTrigger.MeasureChanged => InvalidationTriggerFlags.WillTriggerMeasureChanged,
InvalidationTrigger.HorizontalOptionsChanged => InvalidationTriggerFlags.WillTriggerHorizontalOptionsChanged,
InvalidationTrigger.VerticalOptionsChanged => InvalidationTriggerFlags.WillTriggerVerticalOptionsChanged,
InvalidationTrigger.SizeRequestChanged => InvalidationTriggerFlags.WillTriggerSizeRequestChanged,
InvalidationTrigger.RendererReady => InvalidationTriggerFlags.WillTriggerRendererReady,
InvalidationTrigger.MarginChanged => InvalidationTriggerFlags.WillTriggerMarginChanged,
_ => InvalidationTriggerFlags.WillTriggerUndefined,
};
}

public static InvalidationTrigger ToInvalidationTrigger(this InvalidationTriggerFlags flags) {
if ((flags & InvalidationTriggerFlags.WillTriggerUndefined) != 0) return InvalidationTrigger.Undefined;
if ((flags & InvalidationTriggerFlags.WillTriggerRendererReady) != 0) return InvalidationTrigger.RendererReady;
if ((flags & InvalidationTriggerFlags.WillTriggerMeasureChanged) != 0) return InvalidationTrigger.MeasureChanged;
if ((flags & InvalidationTriggerFlags.WillTriggerSizeRequestChanged) != 0) return InvalidationTrigger.SizeRequestChanged;
if ((flags & InvalidationTriggerFlags.WillTriggerMarginChanged) != 0) return InvalidationTrigger.MarginChanged;
if ((flags & InvalidationTriggerFlags.WillTriggerVerticalOptionsChanged) != 0) return InvalidationTrigger.VerticalOptionsChanged;
if ((flags & InvalidationTriggerFlags.WillTriggerHorizontalOptionsChanged) != 0) return InvalidationTrigger.HorizontalOptionsChanged;
return InvalidationTrigger.Undefined;
}
}
75 changes: 23 additions & 52 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;

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 @@ -319,10 +329,15 @@ protected virtual void InvalidateLayout()
/// It is suggested to still call the base method and modify its calculated results.</remarks>
protected abstract void LayoutChildren(double x, double y, double width, double height);

internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
internal override bool 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));
if (base.OnChildMeasureInvalidatedInternal(child, trigger))
{
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
return true;
}

return false;
}

/// <summary>
Expand All @@ -334,8 +349,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 +511,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 +623,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
10 changes: 8 additions & 2 deletions src/Controls/src/Core/LegacyLayouts/StackLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,16 @@ internal override void ComputeConstraintForView(View view)
ComputeConstraintForView(view, false);
}

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

internal override void InvalidateMeasureInternal(InvalidationTriggerFlags flags)
{
InvalidateLayoutInternal();
base.InvalidateMeasureInternal(flags);
}

void AlignOffAxis(LayoutInformation layout, StackOrientation orientation, double widthConstraint, double heightConstraint)
Expand Down
64 changes: 37 additions & 27 deletions src/Controls/src/Core/Page/Page.cs
Original file line number Diff line number Diff line change
Expand Up @@ -497,10 +497,44 @@ protected override void OnBindingContextChanged()
SetInheritedBindingContext(TitleView, BindingContext);
}

internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
internal override bool 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:
if (IsApplyingBindings)
{
// If we're applying bindings, we need to wait until it's done to invalidate the measure
MeasureInvalidationStatus |= InvalidationTriggerFlags.WillNotifyParentMeasureInvalidated;
return false;
}

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)
{
if (IsApplyingBindings)
{
// If we're applying bindings, we need to wait until it's done to invalidate the measure
MeasureInvalidationStatus |= InvalidationTriggerFlags.WillNotifyParentMeasureInvalidated;
return false;
}

// 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));

return true;
}

/// <summary>
Expand All @@ -510,8 +544,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 +616,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);
}

internal void OnAppearing(Action action)
{
if (_hasAppeared)
Expand Down
Loading

0 comments on commit d73fa74

Please sign in to comment.