Skip to content

Commit

Permalink
Skip Invalidation unless you're my immediate child
Browse files Browse the repository at this point in the history
  • Loading branch information
PureWeen committed Nov 4, 2024
1 parent e8c96c5 commit 0afbdca
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 17 deletions.
27 changes: 21 additions & 6 deletions src/Controls/src/Core/LegacyLayouts/Layout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,10 +341,12 @@ protected virtual void InvalidateLayout()
[Obsolete("Use ArrangeOverride")]
protected abstract void LayoutChildren(double x, double y, double width, double height);

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

/// <summary>
Expand All @@ -356,8 +358,10 @@ 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)
{
var depth = CurrentInvalidationDepth;
CurrentInvalidationDepth = 0;
InvalidationTrigger trigger = (e as InvalidationEventArgs)?.Trigger ?? InvalidationTrigger.Undefined;
OnChildMeasureInvalidated((VisualElement)sender, trigger);
OnChildMeasureInvalidated((VisualElement)sender, trigger, depth);
OnChildMeasureInvalidated();
}

Expand Down Expand Up @@ -531,7 +535,7 @@ internal static void LayoutChildIntoBoundingRegion(View child, Rect region, Size
child.Layout(region);
}

internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger, int depth)
{
IReadOnlyList<Element> children = LogicalChildrenInternal;
int count = children.Count;
Expand All @@ -557,13 +561,24 @@ internal virtual void OnChildMeasureInvalidated(VisualElement child, Invalidatio
}
}

if (trigger == InvalidationTrigger.RendererReady)
if (depth <= 1)
{
InvalidateMeasureInternal(InvalidationTrigger.RendererReady);

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

CurrentInvalidationDepth = 0;
}
else
{
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
FireMeasureChanged(trigger, depth);
}
}

Expand Down
22 changes: 18 additions & 4 deletions src/Controls/src/Core/Page/Page.cs
Original file line number Diff line number Diff line change
Expand Up @@ -500,10 +500,13 @@ protected override void OnBindingContextChanged()
SetInheritedBindingContext(TitleView, BindingContext);
}

internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)

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

/// <summary>
Expand All @@ -514,7 +517,9 @@ internal override void OnChildMeasureInvalidatedInternal(VisualElement child, In
protected virtual void OnChildMeasureInvalidated(object sender, EventArgs e)
{
InvalidationTrigger trigger = (e as InvalidationEventArgs)?.Trigger ?? InvalidationTrigger.Undefined;
OnChildMeasureInvalidated((VisualElement)sender, trigger);
var depth = CurrentInvalidationDepth;
CurrentInvalidationDepth = 0;
OnChildMeasureInvalidated((VisualElement)sender, trigger, depth);
}

/// <summary>
Expand Down Expand Up @@ -593,7 +598,7 @@ protected void UpdateChildrenLayout()
}
}

internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger, int depth)
{
var container = this as IPageContainer<Page>;
if (container != null)
Expand All @@ -613,7 +618,16 @@ internal virtual void OnChildMeasureInvalidated(VisualElement child, Invalidatio
}
}

InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
if (depth <= 1)
{
CurrentInvalidationDepth = depth;
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
CurrentInvalidationDepth = 0;
}
else
{
FireMeasureChanged(trigger, depth);
}
}

internal void OnAppearing(Action action)
Expand Down
26 changes: 19 additions & 7 deletions src/Controls/src/Core/VisualElement/VisualElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1370,6 +1370,11 @@ public void InvalidateMeasureNonVirtual(InvalidationTrigger trigger)
}

internal virtual void InvalidateMeasureInternal(InvalidationTrigger trigger)
{
InvalidateMeasureInternal(trigger, CurrentInvalidationDepth);
}

internal void InvalidateMeasureInternal(InvalidationTrigger trigger, int depth)
{
_measureCache.Clear();

Expand All @@ -1388,11 +1393,21 @@ internal virtual void InvalidateMeasureInternal(InvalidationTrigger trigger)
break;
}

FireMeasureChanged(trigger, depth);
}

private protected void FireMeasureChanged(InvalidationTrigger trigger, int depth)
{
MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(trigger));
(Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, trigger);
(Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, trigger, ++depth);
}

// We don't want to change the execution path of Page or Layout when they are calling "InvalidationMeasure"
// If you look at page it calls OnChildMeasureInvalidated from OnChildMeasureInvalidatedInternal
// Because OnChildMeasureInvalidated is public API and the user might override it, we need to keep it as is
private protected int CurrentInvalidationDepth {get; set; }

internal virtual void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
internal virtual void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth)
{
switch (trigger)
{
Expand All @@ -1404,17 +1419,14 @@ internal virtual void OnChildMeasureInvalidatedInternal(VisualElement child, Inv
case InvalidationTrigger.RendererReady:
// Undefined happens in many cases, including when `IsVisible` changes
case InvalidationTrigger.Undefined:
MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(trigger));
(Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, trigger);
FireMeasureChanged(trigger, depth);
return;
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
MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(InvalidationTrigger.MeasureChanged));
(Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, InvalidationTrigger.MeasureChanged);
FireMeasureChanged(InvalidationTrigger.MeasureChanged, depth);
}
return;
}
Expand Down
71 changes: 71 additions & 0 deletions src/Controls/tests/Core.UnitTests/PageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -549,5 +549,76 @@ public void LogicalChildrenDontAddToPagesInternalChildren()
Assert.Contains(customControl, page.LogicalChildrenInternal);
Assert.Contains(customControl, ((IVisualTreeElement)page).GetVisualChildren());
}

[Fact]
public void MeasureInvalidatedPropagatesUpTree()
{
var label = new Label(){
IsPlatformEnabled = true
};

var scrollView = new ScrollViewCheck()
{
Content = new VerticalStackLayout()
{
Children = { new ContentView { Content = label, IsPlatformEnabled = true } },
IsPlatformEnabled = true
},
IsPlatformEnabled = true
};

var page = new InvalidatePageCheck()
{
Content = scrollView
};

var window = new TestWindow(page);

int fired = 0;
page.MeasureInvalidated += (sender, args) =>
{
fired++;
};

page.Fired = 0;
scrollView.Fired = 0;
label.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
Assert.Equal(1, fired);
Assert.Equal(0, page.Fired);
Assert.Equal(0, scrollView.Fired);
page.Content.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
Assert.Equal(1, page.Fired);
}

class ScrollViewCheck : ScrollView
{
public int Fired { get; set; }

public ScrollViewCheck()
{

}

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

class InvalidatePageCheck : ContentPage
{
public int Fired { get; set; }

public InvalidatePageCheck()
{

}

internal override void InvalidateMeasureInternal(InvalidationTrigger trigger)
{
Fired++;
}
}
}
}

0 comments on commit 0afbdca

Please sign in to comment.