Skip invalidation on propagation (#25652)

* Skip Invalidation unless you're my immediate child

* - fix override on test

* Fix Legacy Layouts Invalidation Propagation

* - use invalidation args to propagate depth
This commit is contained in:
Shane Neuville 2024-11-05 11:46:42 -06:00 коммит произвёл GitHub
Родитель 421e8d6c21
Коммит 1412b38245
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
10 изменённых файлов: 209 добавлений и 32 удалений

Просмотреть файл

@ -445,7 +445,7 @@ namespace Microsoft.Maui.Controls.Core.UnitTests
static void AssertEqualWithTolerance(double a, double b, double tolerance)
{
var diff = Math.Abs(a - b);
Assert.True(diff <= tolerance);
Assert.True(diff <= tolerance, $"a: {a} b: {b} tolerance: {tolerance}");
}
[Theory]

Просмотреть файл

@ -130,5 +130,10 @@ namespace Microsoft.Maui.Controls
this.ArrangeContent(bounds);
return bounds.Size;
}
private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate)
{
base.InvalidateMeasureLegacy(trigger, depth, 1);
}
}
}

Просмотреть файл

@ -10,7 +10,15 @@ namespace Microsoft.Maui.Controls
{
Trigger = trigger;
}
public InvalidationEventArgs(InvalidationTrigger trigger, int depth) : this(trigger)
{
CurrentInvalidationDepth = depth;
}
public InvalidationTrigger Trigger { get; private set; }
public int CurrentInvalidationDepth { set; get; }
}
}

Просмотреть файл

@ -341,10 +341,10 @@ namespace Microsoft.Maui.Controls.Compatibility
[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)
{
// TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger, depth));
}
/// <summary>
@ -356,8 +356,19 @@ namespace Microsoft.Maui.Controls.Compatibility
/// <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);
var depth = 0;
InvalidationTrigger trigger;
if (e is InvalidationEventArgs args)
{
trigger = args.Trigger;
depth = args.CurrentInvalidationDepth;
}
else
{
trigger = InvalidationTrigger.Undefined;
}
OnChildMeasureInvalidated((VisualElement)sender, trigger, depth);
OnChildMeasureInvalidated();
}
@ -531,7 +542,7 @@ namespace Microsoft.Maui.Controls.Compatibility
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;
@ -557,13 +568,26 @@ namespace Microsoft.Maui.Controls.Compatibility
}
}
if (trigger == InvalidationTrigger.RendererReady)
{
InvalidateMeasureInternal(InvalidationTrigger.RendererReady);
InvalidateMeasureLegacy(trigger, depth, int.MaxValue);
}
// This lets us override the rules for invalidation on MAUI controls that unfortunately still inheirt from the legacy layout
private protected virtual void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate)
{
if (depth <= depthLeveltoInvalidate)
{
if (trigger == InvalidationTrigger.RendererReady)
{
InvalidateMeasureInternal(new InvalidationEventArgs(InvalidationTrigger.RendererReady, depth));
}
else
{
InvalidateMeasureInternal(new InvalidationEventArgs(InvalidationTrigger.MeasureChanged, depth));
}
}
else
{
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
FireMeasureChanged(trigger, depth);
}
}

Просмотреть файл

@ -97,7 +97,7 @@ namespace Microsoft.Maui.Controls.Compatibility
ComputeConstraintForView(view, false);
}
internal override void InvalidateMeasureInternal(InvalidationTrigger trigger)
internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger)
{
_layoutInformation = new LayoutInformation();
base.InvalidateMeasureInternal(trigger);

Просмотреть файл

@ -500,10 +500,11 @@ namespace Microsoft.Maui.Controls
SetInheritedBindingContext(TitleView, BindingContext);
}
internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth)
{
// TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger, depth));
}
/// <summary>
@ -513,8 +514,19 @@ namespace Microsoft.Maui.Controls
/// <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);
var depth = 0;
InvalidationTrigger trigger;
if (e is InvalidationEventArgs args)
{
trigger = args.Trigger;
depth = args.CurrentInvalidationDepth;
}
else
{
trigger = InvalidationTrigger.Undefined;
}
OnChildMeasureInvalidated((VisualElement)sender, trigger, depth);
}
/// <summary>
@ -593,7 +605,7 @@ namespace Microsoft.Maui.Controls
}
}
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)
@ -613,7 +625,14 @@ namespace Microsoft.Maui.Controls
}
}
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
if (depth <= 1)
{
InvalidateMeasureInternal(new InvalidationEventArgs(InvalidationTrigger.MeasureChanged, depth));
}
else
{
FireMeasureChanged(trigger, depth);
}
}
internal void OnAppearing(Action action)

Просмотреть файл

@ -474,5 +474,10 @@ namespace Microsoft.Maui.Controls
return bounds.Size;
}
private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate)
{
base.InvalidateMeasureLegacy(trigger, depth, 1);
}
}
}

Просмотреть файл

@ -1,6 +1,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Layouts;
@ -148,6 +149,12 @@ namespace Microsoft.Maui.Controls
this.ArrangeContent(bounds);
return bounds.Size;
}
private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate)
{
base.InvalidateMeasureLegacy(trigger, depth, 1);
}
#nullable disable
}

Просмотреть файл

@ -279,13 +279,15 @@ namespace Microsoft.Maui.Controls
public static readonly BindableProperty BackgroundProperty = BindableProperty.Create(nameof(Background), typeof(Brush), typeof(VisualElement), Brush.Default,
propertyChanging: (bindable, oldvalue, newvalue) =>
{
if (oldvalue == null) return;
if (oldvalue == null)
return;
(bindable as VisualElement)?.StopNotifyingBackgroundChanges();
},
propertyChanged: (bindable, oldvalue, newvalue) =>
{
if (newvalue == null) return;
if (newvalue == null)
return;
(bindable as VisualElement)?.NotifyBackgroundChanges();
});
@ -318,7 +320,7 @@ namespace Microsoft.Maui.Controls
_backgroundChanged ??= (sender, e) => OnPropertyChanged(nameof(Background));
_backgroundProxy ??= new();
_backgroundProxy.Subscribe(background, _backgroundChanged);
OnParentResourcesChanged(this.GetMergedResources());
((IElementDefinition)this).AddResourcesChangedListener(background.OnParentResourcesChanged);
}
@ -1369,14 +1371,19 @@ namespace Microsoft.Maui.Controls
InvalidateMeasureInternal(trigger);
}
internal virtual void InvalidateMeasureInternal(InvalidationTrigger trigger)
internal void InvalidateMeasureInternal(InvalidationTrigger trigger)
{
InvalidateMeasureInternal(new InvalidationEventArgs(trigger, 0));
}
internal virtual void InvalidateMeasureInternal(InvalidationEventArgs eventArgs)
{
_measureCache.Clear();
// TODO ezhart Once we get InvalidateArrange sorted, HorizontalOptionsChanged and
// VerticalOptionsChanged will need to call ParentView.InvalidateArrange() instead
switch (trigger)
switch (eventArgs.Trigger)
{
case InvalidationTrigger.MarginChanged:
case InvalidationTrigger.HorizontalOptionsChanged:
@ -1388,11 +1395,28 @@ namespace Microsoft.Maui.Controls
break;
}
MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(trigger));
(Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, trigger);
FireMeasureChanged(eventArgs);
}
internal virtual void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
private protected void FireMeasureChanged(InvalidationTrigger trigger, int depth)
{
FireMeasureChanged(new InvalidationEventArgs(trigger, depth));
}
private protected void FireMeasureChanged(InvalidationEventArgs args)
{
var depth = args.CurrentInvalidationDepth;
MeasureInvalidated?.Invoke(this, args);
(Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, args.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, int depth)
{
switch (trigger)
{
@ -1404,17 +1428,14 @@ namespace Microsoft.Maui.Controls
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;
}

Просмотреть файл

@ -549,5 +549,93 @@ namespace Microsoft.Maui.Controls.Core.UnitTests
Assert.Contains(customControl, page.LogicalChildrenInternal);
Assert.Contains(customControl, ((IVisualTreeElement)page).GetVisualChildren());
}
[Fact]
public void MeasureInvalidatedPropagatesUpTree()
{
var label = new Label(){
IsPlatformEnabled = true
};
var scrollView = new ScrollViewInvalidationMeasureCheck()
{
Content = new VerticalStackLayout()
{
Children = { new ContentView { Content = label, IsPlatformEnabled = true } },
IsPlatformEnabled = true
},
IsPlatformEnabled = true
};
var page = new InvalidatePageInvalidateMeasureCheck()
{
Content = scrollView
};
var window = new TestWindow(page);
int fired = 0;
page.MeasureInvalidated += (sender, args) =>
{
fired++;
};
page.InvalidateMeasureCount = 0;
scrollView.InvalidateMeasureCount = 0;
label.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
Assert.Equal(1, fired);
Assert.Equal(0, page.InvalidateMeasureCount);
Assert.Equal(0, scrollView.InvalidateMeasureCount);
page.Content.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
Assert.Equal(1, page.InvalidateMeasureCount);
}
class LabelInvalidateMeasureCheck : Label
{
public int InvalidateMeasureCount { get; set; }
public LabelInvalidateMeasureCheck()
{
}
internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger)
{
base.InvalidateMeasureInternal(trigger);
InvalidateMeasureCount++;
}
}
class ScrollViewInvalidationMeasureCheck : ScrollView
{
public int InvalidateMeasureCount { get; set; }
public ScrollViewInvalidationMeasureCheck()
{
}
internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger)
{
base.InvalidateMeasureInternal(trigger);
InvalidateMeasureCount++;
}
}
class InvalidatePageInvalidateMeasureCheck : ContentPage
{
public int InvalidateMeasureCount { get; set; }
public InvalidatePageInvalidateMeasureCheck()
{
}
internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger)
{
base.InvalidateMeasureInternal(trigger);
InvalidateMeasureCount++;
}
}
}
}
}